diff --git a/Cargo.lock b/Cargo.lock index b56c5fe2..c846def9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,12 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + [[package]] name = "derive_more" version = "0.99.16" @@ -274,8 +280,10 @@ version = "1.0.0-alpha.7" dependencies = [ "bitflags", "cssparser", + "data-encoding", "indoc", "itertools", + "lazy_static", "parcel_selectors", "parcel_sourcemap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 0274dbeb..21055e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,8 @@ itertools = "0.10.1" smallvec = { version = "1.7.0", features = ["union"] } bitflags = "1.3.2" parcel_sourcemap = "2.0.0" +data-encoding = "2.3.2" +lazy_static = "1.4.0" [dev-dependencies] indoc = "1.0.3" diff --git a/README.md b/README.md index c44ed0b2..a01e41dc 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ let {code, map} = css.transform({ filename: 'style.css', code: Buffer.from('.foo { color: red }'), minify: true, - source_map: true, + sourceMap: true, targets: { // Semver versions are represented using a single 24-bit number, with one component per byte. // e.g. to represent 13.2.0, the following could be used. diff --git a/node/index.d.ts b/node/index.d.ts index 0c6c8564..993e62b7 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -8,11 +8,13 @@ export interface TransformOptions { /** Whether to enable minification. */ minify?: boolean, /** Whether to output a source map. */ - source_map?: boolean, + sourceMap?: boolean, /** The browser targets for the generated code. */ targets?: Targets, /** Whether to enable various draft syntax. */ - drafts?: Drafts + drafts?: Drafts, + /** Whether to compile this file as a CSS module. */ + cssModules?: boolean } export interface Drafts { @@ -24,7 +26,32 @@ export interface TransformResult { /** The transformed code. */ code: Buffer, /** The generated source map, if enabled. */ - map: Buffer | void + map: Buffer | void, + /** CSS module exports, if enabled. */ + exports: CSSModuleExports | void +} + +export type CSSModuleExports = { + /** Maps exported (i.e. original) names to local names. */ + [name: string]: CSSModuleExport[] +}; + +export type CSSModuleExport = LocalCSSModuleExport | DependencyCSSModuleExport; + +export interface LocalCSSModuleExport { + type: 'local', + /** The local (compiled) name for this export. */ + value: string +} + +export interface DependencyCSSModuleExport { + type: 'dependency', + value: { + /** The name to reference within the dependency. */ + name: string, + /** The dependency specifier for the referenced file. */ + specifier: string + } } /** diff --git a/node/src/lib.rs b/node/src/lib.rs index 67809ab8..f419510b 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -3,8 +3,9 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; use serde::{Serialize, Deserialize}; -use parcel_css::stylesheet::{StyleSheet, StyleAttribute, ParserOptions}; +use parcel_css::stylesheet::{StyleSheet, StyleAttribute, ParserOptions, PrinterOptions}; use parcel_css::targets::Browsers; +use parcel_css::css_modules::CssModuleExports; // --------------------------------------------- @@ -50,11 +51,13 @@ struct SourceMapJson<'a> { } #[derive(Serialize)] +#[serde(rename_all = "camelCase")] struct TransformResult { #[serde(with = "serde_bytes")] code: Vec, #[serde(with = "serde_bytes")] - map: Option> + map: Option>, + exports: Option } #[cfg(not(target_arch = "wasm32"))] @@ -97,6 +100,7 @@ fn init(mut exports: JsObject) -> napi::Result<()> { // --------------------------------------------- #[derive(Serialize, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] struct Config { pub filename: String, #[serde(with = "serde_bytes")] @@ -104,7 +108,8 @@ struct Config { pub targets: Option, pub minify: Option, pub source_map: Option, - pub drafts: Option + pub drafts: Option, + pub css_modules: Option } #[derive(Serialize, Debug, Deserialize, Default)] @@ -118,16 +123,17 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result o.nesting, None => false - } + }, + css_modules: config.css_modules.unwrap_or(false) })?; stylesheet.minify(config.targets); // TODO: should this be conditional? - let (res, source_map) = stylesheet.to_css( - config.minify.unwrap_or(false), - config.source_map.unwrap_or(false), - config.targets - )?; + let res = stylesheet.to_css(PrinterOptions { + minify: config.minify.unwrap_or(false), + source_map: config.source_map.unwrap_or(false), + targets: config.targets + })?; - let map = if let Some(mut source_map) = source_map { + let map = if let Some(mut source_map) = res.source_map { source_map.set_source_content(0, code)?; let mut vlq_output: Vec = Vec::new(); source_map.write_vlq(&mut vlq_output)?; @@ -146,8 +152,9 @@ fn compile<'i>(code: &'i str, config: &Config) -> ResultParcel CSS Playground

Options

+

Draft syntax

Targets

diff --git a/playground/playground.js b/playground/playground.js index 14265c23..b8690e1b 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -26,6 +26,10 @@ function reflectPlaygroundState(playgroundState) { minify.checked = playgroundState.minify; } + if (typeof playgroundState.cssModules !== 'undefined') { + cssModules.checked = playgroundState.cssModules; + } + if (typeof playgroundState.nesting !== 'undefined') { nesting.checked = playgroundState.nesting; } @@ -47,6 +51,7 @@ function savePlaygroundState() { const playgroundState = { minify: minify.checked, nesting: nesting.checked, + cssModules: cssModules.checked, targets: getTargets(), source: source.value, }; @@ -88,10 +93,12 @@ async function update() { targets: Object.keys(targets).length === 0 ? null : targets, drafts: { nesting: nesting.checked - } + }, + cssModules: cssModules.checked }); compiled.value = dec.decode(res.code); + console.log(res.exports) savePlaygroundState(); } diff --git a/src/css_modules.rs b/src/css_modules.rs new file mode 100644 index 00000000..9f3661b8 --- /dev/null +++ b/src/css_modules.rs @@ -0,0 +1,103 @@ +use std::collections::HashMap; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use data_encoding::{Specification, Encoding}; +use lazy_static::lazy_static; +use crate::properties::css_modules::{Composes, ComposesFrom}; +use parcel_selectors::SelectorList; +use crate::selector::Selectors; +use serde::Serialize; + +#[derive(PartialEq, Eq, Hash, Debug, Clone, Serialize)] +#[serde(tag = "type", content = "value", rename_all = "lowercase")] +pub enum CssModuleExport { + Local(String), + Dependency { + name: String, + specifier: String + } +} + +pub type CssModuleExports = HashMap>; + +lazy_static! { + static ref ENCODER: Encoding = { + let mut spec = Specification::new(); + spec.symbols.push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-"); + spec.encoding().unwrap() + }; +} + +pub(crate) struct CssModule<'a> { + pub hash: &'a str, + pub exports: &'a mut CssModuleExports +} + +impl<'a> CssModule<'a> { + pub fn add_export(&mut self, name: String, export: CssModuleExport) { + match self.exports.entry(name) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + if !entry.get().contains(&export) { + entry.get_mut().push(export); + } + } + std::collections::hash_map::Entry::Vacant(entry) => { + let mut items = Vec::new(); + if !items.contains(&export) { + items.push(export); + } + entry.insert(items); + } + } + } + + pub fn add_local(&mut self, exported: &str, local: &str) { + let local = CssModuleExport::Local(format!("{}_{}", local, self.hash)); + self.add_export(exported.into(), local); + } + + pub fn add_global(&mut self, exported: &str, global: &str) { + self.add_export(exported.into(), CssModuleExport::Local(global.into())) + } + + pub fn add_dependency(&mut self, exported: &str, name: &str, specifier: &str) { + let dependency = CssModuleExport::Dependency { + name: name.into(), + specifier: specifier.into() + }; + self.add_export(exported.into(), dependency) + } + + pub fn handle_composes(&mut self, selectors: &SelectorList, composes: &Composes) -> Result<(), ()> { + for sel in &selectors.0 { + if sel.len() == 1 { + match sel.iter_raw_match_order().next().unwrap() { + parcel_selectors::parser::Component::Class(ref id) => { + for name in &composes.names { + match &composes.from { + None => self.add_local(&id.0, &name.0), + Some(ComposesFrom::Global) => self.add_global(&id.0, &name.0), + Some(ComposesFrom::File(file)) => self.add_dependency(&id.0, &name.0, &file) + } + } + continue; + } + _ => {} + } + } + + // The composes property can only be used within a simple class selector. + return Err(()) // TODO: custom error + } + + Ok(()) + } +} + +pub(crate) fn hash(s: &str) -> String { + let mut hasher = DefaultHasher::new(); + s.hash(&mut hasher); + let hash = hasher.finish() as u32; + + ENCODER.encode(&hash.to_le_bytes()) +} diff --git a/src/declaration.rs b/src/declaration.rs index b7442dd0..df99e02e 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -1,6 +1,6 @@ use cssparser::*; use crate::properties::Property; -use crate::traits::{PropertyHandler, Parse, ToCss}; +use crate::traits::{PropertyHandler, ToCss}; use crate::printer::Printer; use crate::properties::{ align::AlignHandler, @@ -22,15 +22,16 @@ use crate::properties::{ grid::GridHandler, }; use crate::targets::Browsers; +use crate::parser::ParserOptions; #[derive(Debug, PartialEq)] pub struct DeclarationBlock { pub declarations: Vec } -impl Parse for DeclarationBlock { - fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { - let mut parser = DeclarationListParser::new(input, PropertyDeclarationParser); +impl DeclarationBlock { + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>, options: &ParserOptions) -> Result> { + let mut parser = DeclarationListParser::new(input, PropertyDeclarationParser { options }); let mut declarations = vec![]; while let Some(decl) = parser.next() { if let Ok(decl) = decl { @@ -82,10 +83,12 @@ impl DeclarationBlock { } } -struct PropertyDeclarationParser; +struct PropertyDeclarationParser<'a> { + options: &'a ParserOptions +} /// Parse a declaration within {} block: `color: blue` -impl<'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser { +impl<'a, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a> { type Declaration = Declaration; type Error = (); @@ -94,12 +97,12 @@ impl<'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser { name: CowRcStr<'i>, input: &mut cssparser::Parser<'i, 't>, ) -> Result> { - Declaration::parse(name, input) + Declaration::parse(name, input, self.options) } } /// Default methods reject all at rules. -impl<'i> AtRuleParser<'i> for PropertyDeclarationParser { +impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> { type Prelude = (); type AtRule = Declaration; type Error = (); @@ -112,8 +115,8 @@ pub struct Declaration { } impl Declaration { - pub fn parse<'i, 't>(name: CowRcStr<'i>, input: &mut Parser<'i, 't>) -> Result> { - let property = input.parse_until_before(Delimiter::Bang, |input| Property::parse(name, input))?; + pub fn parse<'i, 't>(name: CowRcStr<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions) -> Result> { + let property = input.parse_until_before(Delimiter::Bang, |input| Property::parse(name, input, options))?; let important = input.try_parse(|input| { input.expect_delim('!')?; input.expect_ident_matching("important") diff --git a/src/lib.rs b/src/lib.rs index ca779f5a..64954959 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,33 +13,35 @@ mod compat; mod prefixes; pub mod vendor_prefix; pub mod targets; +pub mod css_modules; #[cfg(test)] mod tests { use crate::stylesheet::*; - use crate::parser::ParserOptions; use crate::targets::Browsers; use indoc::indoc; + use std::collections::HashMap; + use crate::css_modules::{CssModuleExports, CssModuleExport}; fn test(source: &str, expected: &str) { let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); stylesheet.minify(None); - let (res, _) = stylesheet.to_css(false, false, None).unwrap(); - assert_eq!(res, expected); + let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + assert_eq!(res.code, expected); } fn minify_test(source: &str, expected: &str) { let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); stylesheet.minify(None); - let (res, _) = stylesheet.to_css(true, false, None).unwrap(); - assert_eq!(res, expected); + let res = stylesheet.to_css(PrinterOptions { minify: true, ..PrinterOptions::default() }).unwrap(); + assert_eq!(res.code, expected); } fn prefix_test(source: &str, expected: &str, targets: Browsers) { let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); stylesheet.minify(Some(targets)); - let (res, _) = stylesheet.to_css(false, false, Some(targets)).unwrap(); - assert_eq!(res, expected); + let res = stylesheet.to_css(PrinterOptions { targets: Some(targets), ..PrinterOptions::default() }).unwrap(); + assert_eq!(res.code, expected); } fn attr_test(source: &str, expected: &str, minify: bool) { @@ -54,19 +56,55 @@ mod tests { chrome: Some(95 << 16), ..Browsers::default() }); - let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { nesting: true }).unwrap(); + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { nesting: true, ..ParserOptions::default() }).unwrap(); stylesheet.minify(targets); - let (res, _) = stylesheet.to_css(false, false, targets).unwrap(); - assert_eq!(res, expected); + let res = stylesheet.to_css(PrinterOptions { targets, ..PrinterOptions::default() }).unwrap(); + assert_eq!(res.code, expected); } fn nesting_test_no_targets(source: &str, expected: &str) { - let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { nesting: true }).unwrap(); + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { nesting: true, ..ParserOptions::default() }).unwrap(); stylesheet.minify(None); - let (res, _) = stylesheet.to_css(false, false, None).unwrap(); - assert_eq!(res, expected); + let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + assert_eq!(res.code, expected); } + fn css_modules_test(source: &str, expected: &str, expected_exports: CssModuleExports) { + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { css_modules: true, ..ParserOptions::default() }).unwrap(); + stylesheet.minify(None); + let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + assert_eq!(res.code, expected); + assert_eq!(res.exports.unwrap(), expected_exports); + } + + macro_rules! map( + { $($key:expr => $($value:literal $(from $from:literal)?)+),* } => { + { + #[allow(unused_mut)] + let mut m = HashMap::new(); + $( + let mut v = Vec::new(); + macro_rules! insert { + ($local:literal, $specifier:literal) => { + v.push(CssModuleExport::Dependency { + name: $local.into(), + specifier: $specifier.into() + }); + }; + ($local:literal) => { + v.push(CssModuleExport::Local($local.into())); + }; + } + $( + insert!($value$(, $from)?); + )+ + m.insert($key.into(), v); + )* + m + } + }; + ); + #[test] pub fn test_border() { test(r#" @@ -7488,5 +7526,311 @@ mod tests { } "#} ); + + nesting_test_no_targets( + r#" + .error, .invalid { + &:hover > .baz { color: red; } + } + "#, + indoc!{r#" + .error, .invalid { + &:hover > .baz { + color: red; + } + } + "#} + ); + } + + #[test] + fn test_css_modules() { + css_modules_test(r#" + .foo { + color: red; + } + + #id { + animation: 2s test; + } + + @keyframes test { + from { color: red } + to { color: yellow } + } + + @counter-style circles { + symbols: Ⓐ Ⓑ Ⓒ; + } + + ul { + list-style: circles; + } + "#, indoc!{r#" + .foo_EgL3uq { + color: red; + } + + #id_EgL3uq { + animation: test_EgL3uq 2s; + } + + @keyframes test_EgL3uq { + from { + color: red; + } + + to { + color: #ff0; + } + } + + @counter-style circles_EgL3uq { + symbols: Ⓐ Ⓑ Ⓒ; + } + + ul { + list-style: circles_EgL3uq; + } + "#}, map! { + "foo" => "foo_EgL3uq", + "id" => "id_EgL3uq", + "test" => "test_EgL3uq", + "circles" => "circles_EgL3uq" + }); + + #[cfg(feature = "grid")] + css_modules_test(r#" + body { + grid: [header-top] "a a a" [header-bottom] + [main-top] "b b b" 1fr [main-bottom] + / auto 1fr auto; + } + + header { + grid-area: a; + } + + main { + grid-row: main-top / main-bottom; + } + "#, indoc!{r#" + body { + grid: [header-top_EgL3uq] "a_EgL3uq a_EgL3uq a_EgL3uq" [header-bottom_EgL3uq] + [main-top_EgL3uq] "b_EgL3uq b_EgL3uq b_EgL3uq" 1fr [main-bottom_EgL3uq] + / auto 1fr auto; + } + + header { + grid-area: a_EgL3uq; + } + + main { + grid-row: main-top_EgL3uq / main-bottom_EgL3uq; + } + "#}, map! { + "header-top" => "header-top_EgL3uq", + "header-bottom" => "header-bottom_EgL3uq", + "main-top" => "main-top_EgL3uq", + "main-bottom" => "main-bottom_EgL3uq", + "a" => "a_EgL3uq", + "b" => "b_EgL3uq" + }); + + css_modules_test(r#" + test { + transition-property: opacity; + } + "#, indoc!{r#" + test { + transition-property: opacity; + } + "#}, map! {}); + + css_modules_test(r#" + :global(.foo) { + color: red; + } + + :local(.bar) { + color: yellow; + } + + .bar :global(.baz) { + color: purple; + } + "#, indoc!{r#" + .foo { + color: red; + } + + .bar_EgL3uq { + color: #ff0; + } + + .bar_EgL3uq .baz { + color: purple; + } + "#}, map! { + "bar" => "bar_EgL3uq" + }); + + + // :global(:local(.hi)) { + // color: green; + // } + + + css_modules_test(r#" + .test { + composes: foo; + background: white; + } + + .foo { + color: red; + } + "#, indoc!{r#" + .test_EgL3uq { + background: #fff; + } + + .foo_EgL3uq { + color: red; + } + "#}, map! { + "test" => "test_EgL3uq" "foo_EgL3uq", + "foo" => "foo_EgL3uq" + }); + + css_modules_test(r#" + .a, .b { + composes: foo; + background: white; + } + + .foo { + color: red; + } + "#, indoc!{r#" + .a_EgL3uq, .b_EgL3uq { + background: #fff; + } + + .foo_EgL3uq { + color: red; + } + "#}, map! { + "a" => "a_EgL3uq" "foo_EgL3uq", + "b" => "b_EgL3uq" "foo_EgL3uq", + "foo" => "foo_EgL3uq" + }); + + css_modules_test(r#" + .test { + composes: foo bar; + background: white; + } + + .foo { + color: red; + } + + .bar { + color: yellow; + } + "#, indoc!{r#" + .test_EgL3uq { + background: #fff; + } + + .foo_EgL3uq { + color: red; + } + + .bar_EgL3uq { + color: #ff0; + } + "#}, map! { + "test" => "test_EgL3uq" "foo_EgL3uq" "bar_EgL3uq", + "foo" => "foo_EgL3uq", + "bar" => "bar_EgL3uq" + }); + + css_modules_test(r#" + .test { + composes: foo from global; + background: white; + } + "#, indoc!{r#" + .test_EgL3uq { + background: #fff; + } + "#}, map! { + "test" => "test_EgL3uq" "foo" + }); + + css_modules_test(r#" + .test { + composes: foo bar from global; + background: white; + } + "#, indoc!{r#" + .test_EgL3uq { + background: #fff; + } + "#}, map! { + "test" => "test_EgL3uq" "foo" "bar" + }); + + css_modules_test(r#" + .test { + composes: foo from "foo.css"; + background: white; + } + "#, indoc!{r#" + .test_EgL3uq { + background: #fff; + } + "#}, map! { + "test" => "test_EgL3uq" "foo" from "foo.css" + }); + + css_modules_test(r#" + .test { + composes: foo bar from "foo.css"; + background: white; + } + "#, indoc!{r#" + .test_EgL3uq { + background: #fff; + } + "#}, map! { + "test" => "test_EgL3uq" "foo" from "foo.css" "bar" from "foo.css" + }); + + css_modules_test(r#" + .test { + composes: foo; + composes: foo from "foo.css"; + composes: bar from "bar.css"; + background: white; + } + + .foo { + color: red; + } + "#, indoc!{r#" + .test_EgL3uq { + background: #fff; + } + + .foo_EgL3uq { + color: red; + } + "#}, map! { + "test" => "test_EgL3uq" "foo_EgL3uq" "foo" from "foo.css" "bar" from "bar.css", + "foo" => "foo_EgL3uq" + }); } } diff --git a/src/parser.rs b/src/parser.rs index f77e446c..41e01c71 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -25,18 +25,19 @@ use std::collections::HashMap; #[derive(Default)] pub struct ParserOptions { - pub nesting: bool + pub nesting: bool, + pub css_modules: bool } /// The parser for the top-level rules in a stylesheet. -pub struct TopLevelRuleParser { +pub struct TopLevelRuleParser<'a> { default_namespace: Option, namespace_prefixes: HashMap, - options: ParserOptions + options: &'a ParserOptions } -impl<'b> TopLevelRuleParser { - pub fn new(options: ParserOptions) -> TopLevelRuleParser { +impl<'a, 'b> TopLevelRuleParser<'a> { + pub fn new(options: &'a ParserOptions) -> TopLevelRuleParser<'a> { TopLevelRuleParser { default_namespace: None, namespace_prefixes: HashMap::new(), @@ -44,7 +45,7 @@ impl<'b> TopLevelRuleParser { } } - fn nested<'a: 'b>(&'a mut self) -> NestedRuleParser { + fn nested<'x: 'b>(&'x mut self) -> NestedRuleParser { NestedRuleParser { default_namespace: &mut self.default_namespace, namespace_prefixes: &mut self.namespace_prefixes, @@ -70,7 +71,7 @@ pub enum AtRulePrelude { /// A @viewport rule prelude. Viewport, /// A @keyframes rule, with its animation name and vendor prefix if exists. - Keyframes(String, VendorPrefix), + Keyframes(CustomIdent, VendorPrefix), /// A @page rule prelude. Page(Vec), /// A @-moz-document rule. @@ -85,7 +86,7 @@ pub enum AtRulePrelude { Nest(SelectorList) } -impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser { +impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a> { type Prelude = AtRulePrelude; type AtRule = (SourcePosition, CssRule); type Error = (); @@ -175,7 +176,7 @@ impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser { } } -impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser { +impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser<'a> { type Prelude = SelectorList; type QualifiedRule = (SourcePosition, CssRule); type Error = (); @@ -292,7 +293,7 @@ impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a> { ref t => return Err(location.new_unexpected_token_error(t.clone())), }; - Ok(AtRulePrelude::Keyframes(name.into(), prefix)) + Ok(AtRulePrelude::Keyframes(CustomIdent(name.into()), prefix)) }, "page" => { let selectors = input.try_parse(|input| input.parse_comma_separated(PageSelector::parse)).unwrap_or_default(); @@ -350,7 +351,7 @@ impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a> { AtRulePrelude::CounterStyle(name) => { Ok(CssRule::CounterStyle(CounterStyleRule { name, - declarations: DeclarationBlock::parse(input)?, + declarations: DeclarationBlock::parse(input, self.options)?, loc })) }, @@ -391,7 +392,7 @@ impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a> { AtRulePrelude::Page(selectors) => { Ok(CssRule::Page(PageRule { selectors, - declarations: DeclarationBlock::parse(input)?, + declarations: DeclarationBlock::parse(input, self.options)?, loc })) }, @@ -422,7 +423,8 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a> { let selector_parser = SelectorParser { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes, - is_nesting_allowed: false + is_nesting_allowed: false, + css_modules: self.options.css_modules }; match SelectorList::parse(&selector_parser, input, NestingRequirement::None) { Ok(x) => Ok(x), @@ -438,9 +440,9 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a> { ) -> Result> { let loc = start.source_location(); let (declarations, rules) = if self.options.nesting { - parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes)? + parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)? } else { - (DeclarationBlock::parse(input)?, CssRuleList(vec![])) + (DeclarationBlock::parse(input, self.options)?, CssRuleList(vec![])) }; Ok(CssRule::Style(StyleRule { selectors, @@ -461,13 +463,15 @@ pub enum DeclarationOrRule { fn parse_declarations_and_nested_rules<'a, 'i, 't>( input: &mut Parser<'i, 't>, default_namespace: &'a Option, - namespace_prefixes: &'a HashMap + namespace_prefixes: &'a HashMap, + options: &'a ParserOptions ) -> Result<(DeclarationBlock, CssRuleList), ParseError<'i, ()>> { let mut declarations = vec![]; let mut rules = vec![]; let parser = StyleRuleParser { default_namespace, - namespace_prefixes + namespace_prefixes, + options }; let mut declaration_parser = DeclarationListParser::new(input, parser); @@ -509,7 +513,8 @@ fn parse_declarations_and_nested_rules<'a, 'i, 't>( pub struct StyleRuleParser<'a> { default_namespace: &'a Option, - namespace_prefixes: &'a HashMap + namespace_prefixes: &'a HashMap, + options: &'a ParserOptions } /// Parse a declaration within {} block: `color: blue` @@ -522,7 +527,7 @@ impl<'a, 'i> cssparser::DeclarationParser<'i> for StyleRuleParser<'a> { name: CowRcStr<'i>, input: &mut cssparser::Parser<'i, 't>, ) -> Result> { - Ok(DeclarationOrRule::Declaration(Declaration::parse(name, input)?)) + Ok(DeclarationOrRule::Declaration(Declaration::parse(name, input, self.options)?)) } } @@ -549,7 +554,8 @@ impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a> { let selector_parser = SelectorParser { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes, - is_nesting_allowed: true + is_nesting_allowed: true, + css_modules: self.options.css_modules }; let selectors = match SelectorList::parse(&selector_parser, input, NestingRequirement::Contained) { Ok(x) => x, @@ -572,19 +578,19 @@ impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a> { AtRulePrelude::Media(query) => { Ok(DeclarationOrRule::Rule(CssRule::Media(MediaRule { query, - rules: parse_nested_at_rule(input, self.default_namespace, self.namespace_prefixes)?, + rules: parse_nested_at_rule(input, self.default_namespace, self.namespace_prefixes, self.options)?, loc }))) }, AtRulePrelude::Supports(condition) => { Ok(DeclarationOrRule::Rule(CssRule::Supports(SupportsRule { condition, - rules: parse_nested_at_rule(input, self.default_namespace, self.namespace_prefixes)?, + rules: parse_nested_at_rule(input, self.default_namespace, self.namespace_prefixes, self.options)?, loc }))) }, AtRulePrelude::Nest(selectors) => { - let (declarations, rules) = parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes)?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)?; Ok(DeclarationOrRule::Rule(CssRule::Nesting(NestingRule { style: StyleRule { selectors, @@ -608,13 +614,14 @@ impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a> { fn parse_nested_at_rule<'a, 'i, 't>( input: &mut Parser<'i, 't>, default_namespace: &'a Option, - namespace_prefixes: &'a HashMap + namespace_prefixes: &'a HashMap, + options: &'a ParserOptions ) -> Result> { let loc = input.current_source_location(); // Declarations can be immediately within @media and @supports blocks that are nested within a parent style rule. // These act the same way as if they were nested within a `& { ... }` block. - let (declarations, mut rules) = parse_declarations_and_nested_rules(input, default_namespace, namespace_prefixes)?; + let (declarations, mut rules) = parse_declarations_and_nested_rules(input, default_namespace, namespace_prefixes, options)?; if declarations.declarations.len() > 0 { rules.0.insert(0, CssRule::Style(StyleRule { @@ -641,7 +648,8 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for StyleRuleParser<'a> { let selector_parser = SelectorParser { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes, - is_nesting_allowed: true + is_nesting_allowed: true, + css_modules: self.options.css_modules }; match SelectorList::parse(&selector_parser, input, NestingRequirement::Prefixed) { Ok(x) => Ok(x), @@ -656,7 +664,7 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for StyleRuleParser<'a> { input: &mut Parser<'i, 't>, ) -> Result> { let loc = start.source_location(); - let (declarations, rules) = parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes)?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)?; Ok(DeclarationOrRule::Rule(CssRule::Style(StyleRule { selectors, vendor_prefix: VendorPrefix::empty(), diff --git a/src/printer.rs b/src/printer.rs index 5a320f6c..feb25bea 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1,10 +1,11 @@ use std::fmt::*; -use cssparser::SourceLocation; +use cssparser::{SourceLocation, serialize_identifier}; use parcel_sourcemap::{SourceMap, OriginalLocation}; use crate::vendor_prefix::VendorPrefix; use crate::targets::Browsers; +use crate::css_modules::CssModule; -pub struct Printer<'a, W> { +pub(crate) struct Printer<'a, W> { dest: &'a mut W, source_map: Option<&'a mut SourceMap>, indent: u8, @@ -15,7 +16,8 @@ pub struct Printer<'a, W> { /// Vendor prefix override. When non-empty, it overrides /// the vendor prefix of whatever is being printed. pub vendor_prefix: VendorPrefix, - pub in_calc: bool + pub in_calc: bool, + pub css_module: Option> } impl<'a, W: Write + Sized> Printer<'a, W> { @@ -34,7 +36,8 @@ impl<'a, W: Write + Sized> Printer<'a, W> { minify, targets, vendor_prefix: VendorPrefix::empty(), - in_calc: false + in_calc: false, + css_module: None } } @@ -108,6 +111,26 @@ impl<'a, W: Write + Sized> Printer<'a, W> { })) } } + + pub fn write_ident(&mut self, ident: &str) -> Result { + serialize_identifier(ident, self)?; + let hash = if let Some(css_module) = &self.css_module { + Some(css_module.hash) + } else { + None + }; + + if let Some(hash) = hash { + self.write_char('_')?; + self.write_str(hash)?; + } + + if let Some(css_module) = &mut self.css_module { + css_module.add_local(&ident, &ident); + } + + Ok(()) + } } impl<'a, W: Write + Sized> Write for Printer<'a, W> { diff --git a/src/properties/align.rs b/src/properties/align.rs index 5a96b5c5..5ace6068 100644 --- a/src/properties/align.rs +++ b/src/properties/align.rs @@ -1048,19 +1048,19 @@ impl AlignHandler { #[inline] fn is_align_property(property_id: &PropertyId) -> bool { match property_id { - PropertyId::AlignContent | - PropertyId::FlexLinePack | - PropertyId::JustifyContent | - PropertyId::BoxPack | - PropertyId::FlexPack | + PropertyId::AlignContent(_) | + PropertyId::FlexLinePack(_) | + PropertyId::JustifyContent(_) | + PropertyId::BoxPack(_) | + PropertyId::FlexPack(_) | PropertyId::PlaceContent | - PropertyId::AlignSelf | - PropertyId::FlexItemAlign | + PropertyId::AlignSelf(_) | + PropertyId::FlexItemAlign(_) | PropertyId::JustifySelf | PropertyId::PlaceSelf | - PropertyId::AlignItems | - PropertyId::BoxAlign | - PropertyId::FlexAlign | + PropertyId::AlignItems(_) | + PropertyId::BoxAlign(_) | + PropertyId::FlexAlign(_) | PropertyId::JustifyItems | PropertyId::PlaceItems | PropertyId::RowGap | diff --git a/src/properties/animation.rs b/src/properties/animation.rs index 0e87a1c9..17afee35 100644 --- a/src/properties/animation.rs +++ b/src/properties/animation.rs @@ -1,6 +1,6 @@ use cssparser::*; use crate::traits::{Parse, ToCss, PropertyHandler}; -use crate::values::{time::Time, easing::EasingFunction}; +use crate::values::{time::Time, easing::EasingFunction, ident::CustomIdent}; use crate::targets::Browsers; use crate::prefixes::Feature; use crate::properties::{Property, PropertyId, VendorPrefix}; @@ -14,7 +14,7 @@ use smallvec::SmallVec; #[derive(Debug, Clone, PartialEq)] pub enum AnimationName { None, - String(String) + Ident(CustomIdent) } impl Parse for AnimationName { @@ -29,7 +29,7 @@ impl Parse for AnimationName { Token::QuotedString(ref s) => s.as_ref(), ref t => return Err(location.new_unexpected_token_error(t.clone())), }; - Ok(AnimationName::String(name.into())) + Ok(AnimationName::Ident(CustomIdent(name.into()))) } } @@ -37,7 +37,7 @@ impl ToCss for AnimationName { fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { match self { AnimationName::None => dest.write_str("none"), - AnimationName::String(s) => serialize_identifier(&s, dest) + AnimationName::Ident(s) => s.to_css(dest) } } } @@ -156,13 +156,13 @@ impl ToCss for Animation { self.name.to_css(dest)?; match &self.name { AnimationName::None => return Ok(()), - AnimationName::String(name) => { + AnimationName::Ident(name) => { if self.duration != 0.0 || self.delay != 0.0 { dest.write_char(' ')?; self.duration.to_css(dest)?; } - if (self.timing_function != EasingFunction::Ease && self.timing_function != EasingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0)) || EasingFunction::is_ident(&name) { + if (self.timing_function != EasingFunction::Ease && self.timing_function != EasingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0)) || EasingFunction::is_ident(&name.0) { dest.write_char(' ')?; self.timing_function.to_css(dest)?; } @@ -172,22 +172,22 @@ impl ToCss for Animation { self.delay.to_css(dest)?; } - if self.iteration_count != AnimationIterationCount::Number(1.0) || name == "infinite" { + if self.iteration_count != AnimationIterationCount::Number(1.0) || name.0 == "infinite" { dest.write_char(' ')?; self.iteration_count.to_css(dest)?; } - if self.direction != AnimationDirection::Normal || AnimationDirection::from_str(&name).is_some() { + if self.direction != AnimationDirection::Normal || AnimationDirection::from_str(&name.0).is_some() { dest.write_char(' ')?; self.direction.to_css(dest)?; } - if self.fill_mode != AnimationFillMode::None || AnimationFillMode::from_str(&name).is_some() { + if self.fill_mode != AnimationFillMode::None || AnimationFillMode::from_str(&name.0).is_some() { dest.write_char(' ')?; self.fill_mode.to_css(dest)?; } - if self.play_state != AnimationPlayState::Running || AnimationPlayState::from_str(&name).is_some() { + if self.play_state != AnimationPlayState::Running || AnimationPlayState::from_str(&name.0).is_some() { dest.write_char(' ')?; self.play_state.to_css(dest)?; } @@ -392,15 +392,15 @@ impl AnimationHandler { #[inline] fn is_animation_property(property_id: &PropertyId) -> bool { match property_id { - PropertyId::AnimationName | - PropertyId::AnimationDuration | - PropertyId::AnimationTimingFunction | - PropertyId::AnimationIterationCount | - PropertyId::AnimationDirection | - PropertyId::AnimationPlayState | - PropertyId::AnimationDelay | - PropertyId::AnimationFillMode | - PropertyId::Animation => true, + PropertyId::AnimationName(_) | + PropertyId::AnimationDuration(_) | + PropertyId::AnimationTimingFunction(_) | + PropertyId::AnimationIterationCount(_) | + PropertyId::AnimationDirection(_) | + PropertyId::AnimationPlayState(_) | + PropertyId::AnimationDelay(_) | + PropertyId::AnimationFillMode(_) | + PropertyId::Animation(_) => true, _ => false } } diff --git a/src/properties/background.rs b/src/properties/background.rs index 7b118d4f..30e68132 100644 --- a/src/properties/background.rs +++ b/src/properties/background.rs @@ -702,7 +702,7 @@ fn is_background_property(property_id: &PropertyId) -> bool { PropertyId::BackgroundSize | PropertyId::BackgroundAttachment | PropertyId::BackgroundOrigin | - PropertyId::BackgroundClip | + PropertyId::BackgroundClip(_) | PropertyId::Background => true, _ => false } diff --git a/src/properties/border_image.rs b/src/properties/border_image.rs index 82915f26..b18fdbb9 100644 --- a/src/properties/border_image.rs +++ b/src/properties/border_image.rs @@ -293,7 +293,7 @@ impl PropertyHandler for BorderImageHandler { // Even if we weren't able to parse the value (e.g. due to var() references), // we can still add vendor prefixes to the property itself. - let prop = if val.property_id == PropertyId::BorderImage { + let prop = if matches!(val.property_id, PropertyId::BorderImage(_)) { Property::Unparsed(val.get_prefixed(self.targets, Feature::BorderImage)) } else { property.clone() @@ -392,7 +392,7 @@ fn is_border_image_property(property_id: &PropertyId) -> bool { PropertyId::BorderImageWidth | PropertyId::BorderImageOutset | PropertyId::BorderImageRepeat | - PropertyId::BorderImage => true, + PropertyId::BorderImage(_) => true, _ => false } } diff --git a/src/properties/border_radius.rs b/src/properties/border_radius.rs index 59939fef..9a2ef749 100644 --- a/src/properties/border_radius.rs +++ b/src/properties/border_radius.rs @@ -202,11 +202,11 @@ fn is_border_radius_property(property_id: &PropertyId) -> bool { } match property_id { - PropertyId::BorderTopLeftRadius | - PropertyId::BorderTopRightRadius | - PropertyId::BorderBottomLeftRadius | - PropertyId::BorderBottomRightRadius | - PropertyId::BorderRadius => true, + PropertyId::BorderTopLeftRadius(_) | + PropertyId::BorderTopRightRadius(_) | + PropertyId::BorderBottomLeftRadius(_) | + PropertyId::BorderBottomRightRadius(_) | + PropertyId::BorderRadius(_) => true, _ => false } } diff --git a/src/properties/css_modules.rs b/src/properties/css_modules.rs new file mode 100644 index 00000000..4c2c6b00 --- /dev/null +++ b/src/properties/css_modules.rs @@ -0,0 +1,81 @@ +use cssparser::*; +use crate::values::ident::CustomIdent; +use crate::traits::{Parse, ToCss}; +use crate::printer::Printer; +use smallvec::SmallVec; + +/// The `composes` property from CSS modules. +/// https://github.com/css-modules/css-modules/#dependencies +#[derive(Debug, Clone, PartialEq)] +pub struct Composes { + pub names: SmallVec<[CustomIdent; 1]>, + pub from: Option +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ComposesFrom { + Global, + File(String) +} + +impl Parse for Composes { + fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { + let mut names = SmallVec::new(); + while let Ok(name) = input.try_parse(parse_one_ident) { + names.push(name); + } + + if names.is_empty() { + return Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) + } + + let from = if input.try_parse(|input| input.expect_ident_matching("from")).is_ok() { + if let Ok(file) = input.try_parse(|input| input.expect_string().map(|s| s.as_ref().to_owned())) { + Some(ComposesFrom::File(file)) + } else { + input.expect_ident_matching("global")?; + Some(ComposesFrom::Global) + } + } else { + None + }; + + Ok(Composes { + names, + from + }) + } +} + +fn parse_one_ident<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { + let name = CustomIdent::parse(input)?; + if name.0.eq_ignore_ascii_case("from") { + return Err(input.new_error_for_next_token()) + } + + Ok(name) +} + +impl ToCss for Composes { + fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { + let mut first = true; + for name in &self.names { + if first { + first = false; + } else { + dest.write_char(' ')?; + } + name.to_css(dest)?; + } + + if let Some(from) = &self.from { + dest.write_str(" from ")?; + match from { + ComposesFrom::Global => dest.write_str("global")?, + ComposesFrom::File(file) => serialize_string(&file, dest)? + } + } + + Ok(()) + } +} diff --git a/src/properties/custom.rs b/src/properties/custom.rs index aedfa677..0db7ab61 100644 --- a/src/properties/custom.rs +++ b/src/properties/custom.rs @@ -26,29 +26,26 @@ impl CustomProperty { #[derive(Debug, Clone, PartialEq)] pub struct UnparsedProperty { pub property_id: PropertyId, - pub vendor_prefix: VendorPrefix, pub value: String } impl UnparsedProperty { pub fn parse<'i, 't>( property_id: PropertyId, - vendor_prefix: VendorPrefix, input: &mut Parser<'i, 't> ) -> Result> { let value = parse_unknown_value(input)?; Ok(UnparsedProperty { property_id, - vendor_prefix, value }) } pub fn get_prefixed(&self, targets: Option, feature: Feature) -> UnparsedProperty { let mut clone = self.clone(); - if self.vendor_prefix.contains(VendorPrefix::None) { + if self.property_id.prefix().contains(VendorPrefix::None) { if let Some(targets) = targets { - clone.vendor_prefix = feature.prefixes_for(targets) + clone.property_id = clone.property_id.with_prefix(feature.prefixes_for(targets)) } } clone diff --git a/src/properties/flex.rs b/src/properties/flex.rs index 36e661ef..e612671c 100644 --- a/src/properties/flex.rs +++ b/src/properties/flex.rs @@ -699,23 +699,23 @@ impl FlexHandler { #[inline] fn is_flex_property(property_id: &PropertyId) -> bool { match property_id { - PropertyId::FlexDirection | - PropertyId::BoxOrient | - PropertyId::BoxDirection | - PropertyId::FlexWrap | - PropertyId::BoxLines | - PropertyId::FlexFlow | - PropertyId::FlexGrow | - PropertyId::BoxFlex | - PropertyId::FlexPositive | - PropertyId::FlexShrink | - PropertyId::FlexNegative | - PropertyId::FlexBasis | - PropertyId::FlexPreferredSize | - PropertyId::Flex | - PropertyId::Order | - PropertyId::BoxOrdinalGroup | - PropertyId::FlexOrder => true, + PropertyId::FlexDirection(_) | + PropertyId::BoxOrient(_) | + PropertyId::BoxDirection(_) | + PropertyId::FlexWrap(_) | + PropertyId::BoxLines(_) | + PropertyId::FlexFlow(_) | + PropertyId::FlexGrow(_) | + PropertyId::BoxFlex(_) | + PropertyId::FlexPositive(_) | + PropertyId::FlexShrink(_) | + PropertyId::FlexNegative(_) | + PropertyId::FlexBasis(_) | + PropertyId::FlexPreferredSize(_) | + PropertyId::Flex(_) | + PropertyId::Order(_) | + PropertyId::BoxOrdinalGroup(_) | + PropertyId::FlexOrder(_) => true, _ => false } } diff --git a/src/properties/grid.rs b/src/properties/grid.rs index d4d9c0ca..21023b16 100644 --- a/src/properties/grid.rs +++ b/src/properties/grid.rs @@ -551,7 +551,7 @@ impl GridTemplateAreas { if i > 0 && (!last_was_null || !dest.minify) { dest.write_char(' ')?; } - dest.write_str(string)?; + dest.write_ident(string)?; last_was_null = false; } else { if i > 0 && (last_was_null || !dest.minify) { diff --git a/src/properties/mod.rs b/src/properties/mod.rs index c0005095..1fc509ca 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -21,6 +21,7 @@ pub mod ui; pub mod list; #[cfg(feature = "grid")] pub mod grid; +pub mod css_modules; use cssparser::*; use custom::*; @@ -43,25 +44,56 @@ use ui::*; use list::*; #[cfg(feature = "grid")] use grid::*; -use crate::values::{image::*, length::*, position::*, alpha::*, size::*, rect::*, color::*, time::Time, ident::CustomIdent, easing::EasingFunction}; +use css_modules::*; +use crate::values::{image::*, length::*, position::*, alpha::*, size::*, rect::*, color::*, time::Time, easing::EasingFunction}; use crate::traits::{Parse, ToCss}; use crate::printer::Printer; use smallvec::{SmallVec, smallvec}; use crate::vendor_prefix::VendorPrefix; +use crate::parser::ParserOptions; macro_rules! define_properties { ( $( $(#[$meta: meta])* - $name: literal: $property: ident($type: ty $(, $vp: ty)?) $( / $prefix: tt )*, + $name: literal: $property: ident($type: ty $(, $vp: ty)?) $( / $prefix: tt )* $( if $condition: ident )?, )+ ) => { - #[derive(Debug, Clone, Copy, PartialEq)] + #[derive(Debug, Clone, PartialEq)] pub enum PropertyId { $( $(#[$meta])* - $property, + $property$(($vp))?, )+ + All, + Custom(String) + } + + macro_rules! vp_name { + ($x: ty, $n: ident) => { + $n + }; + } + + impl Parse for PropertyId { + fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { + let name = input.expect_ident()?; + match name.as_ref() { + $( + $(#[$meta])* + $name => Ok(PropertyId::$property$((<$vp>::None))?), + $( + // TODO: figure out how to handle attributes on prefixed properties... + concat!("-", $prefix, "-", $name) => { + let prefix = VendorPrefix::from_str($prefix); + Ok(PropertyId::$property(prefix)) + } + )* + )+ + "all" => Ok(PropertyId::All), + name => Ok(PropertyId::Custom(name.into())) + } + } } impl ToCss for PropertyId { @@ -70,8 +102,92 @@ macro_rules! define_properties { match self { $( $(#[$meta])* - $property => dest.write_str(&$name), + $property$((vp_name!($vp, prefix)))? => { + // TODO: this assumes there is only one prefix. How should we handle multiple? + $( + macro_rules! write_prefix { + ($v: ty) => { + prefix.to_css(dest)?; + }; + } + + write_prefix!($vp); + )? + dest.write_str(&$name) + }, + )+ + All => dest.write_str("all"), + Custom(name) => dest.write_str(&name) + } + } + } + + impl PropertyId { + fn prefix(&self) -> VendorPrefix { + use PropertyId::*; + match self { + $( + $(#[$meta])* + $property$((vp_name!($vp, prefix)))? => { + $( + macro_rules! return_prefix { + ($v: ty) => { + return *prefix; + }; + } + + return_prefix!($vp); + )? + #[allow(unreachable_code)] + VendorPrefix::None + }, + )+ + _ => VendorPrefix::None + } + } + + fn with_prefix(&self, prefix: VendorPrefix) -> PropertyId { + use PropertyId::*; + match self { + $( + $(#[$meta])* + $property$((vp_name!($vp, _p)))? => { + macro_rules! get_prefixed { + ($v: ty) => { + PropertyId::$property(prefix) + }; + () => { + PropertyId::$property + } + } + + get_prefixed!($($vp)?) + }, + )+ + _ => self.clone() + } + } + + fn to_css_with_prefix(&self, dest: &mut Printer, prefix: VendorPrefix) -> std::fmt::Result where W: std::fmt::Write { + use PropertyId::*; + match self { + $( + $(#[$meta])* + $property$((vp_name!($vp, _p)))? => { + $( + macro_rules! write_prefix { + ($v: ty) => { + prefix.to_css(dest)?; + }; + } + + write_prefix!($vp); + )? + dest.write_str(&$name) + }, )+ + All => dest.write_str("all"), + Custom(name) => dest.write_str(&name) } } } @@ -87,12 +203,12 @@ macro_rules! define_properties { } impl Property { - pub fn parse<'i, 't>(name: CowRcStr<'i>, input: &mut Parser<'i, 't>) -> Result> { + pub fn parse<'i, 't>(name: CowRcStr<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions) -> Result> { let state = input.state(); match name.as_ref() { $( $(#[$meta])* - $name => { + $name $(if options.$condition)? => { if let Ok(c) = <$type>::parse(input) { if input.expect_exhausted().is_ok() { return Ok(Property::$property(c, $(<$vp>::None)?)) @@ -104,7 +220,7 @@ macro_rules! define_properties { // and stored as an enum rather than a string. This lets property handlers more easily deal with it. // Ideally we'd only do this if var() or env() references were seen, but err on the safe side for now. input.reset(&state); - return Ok(Property::Unparsed(UnparsedProperty::parse(PropertyId::$property, VendorPrefix::None, input)?)) + return Ok(Property::Unparsed(UnparsedProperty::parse(PropertyId::$property$((<$vp>::None))?, input)?)) } $( @@ -118,7 +234,7 @@ macro_rules! define_properties { } input.reset(&state); - return Ok(Property::Unparsed(UnparsedProperty::parse(PropertyId::$property, prefix, input)?)) + return Ok(Property::Unparsed(UnparsedProperty::parse(PropertyId::$property(prefix), input)?)) } )* )+ @@ -129,15 +245,9 @@ macro_rules! define_properties { return Ok(Property::Custom(CustomProperty::parse(name, input)?)) } - pub fn to_css(&self, dest: &mut Printer, important: bool) -> std::fmt::Result where W: std::fmt::Write { + pub(crate) fn to_css(&self, dest: &mut Printer, important: bool) -> std::fmt::Result where W: std::fmt::Write { use Property::*; - macro_rules! vp_name { - ($x: ty, $n: ident) => { - $n - }; - } - let mut first = true; macro_rules! start { () => { @@ -192,10 +302,9 @@ macro_rules! define_properties { Unparsed(unparsed) => { macro_rules! write { ($p: expr) => { - if unparsed.vendor_prefix.contains($p) { + if unparsed.property_id.prefix().contains($p) { start!(); - $p.to_css(dest)?; - unparsed.property_id.to_css(dest)?; + unparsed.property_id.to_css_with_prefix(dest, $p)?; dest.delim(':', false)?; dest.write_str(unparsed.value.as_ref())?; if important { @@ -491,7 +600,7 @@ define_properties! { "font": Font(Font), "vertical-align": VerticalAlign(VerticalAlign), - "transition-property": TransitionProperty(SmallVec<[CustomIdent; 1]>, VendorPrefix) / "webkit" / "moz" / "ms", + "transition-property": TransitionProperty(SmallVec<[PropertyId; 1]>, VendorPrefix) / "webkit" / "moz" / "ms", "transition-duration": TransitionDuration(SmallVec<[Time; 1]>, VendorPrefix) / "webkit" / "moz" / "ms", "transition-delay": TransitionDelay(SmallVec<[Time; 1]>, VendorPrefix) / "webkit" / "moz" / "ms", "transition-timing-function": TransitionTimingFunction(SmallVec<[EasingFunction; 1]>, VendorPrefix) / "webkit" / "moz" / "ms", @@ -564,6 +673,9 @@ define_properties! { "list-style-position": ListStylePosition(ListStylePosition), "list-style": ListStyle(ListStyle), "marker-side": MarkerSide(MarkerSide), + + // CSS modules + "composes": Composes(Composes) if css_modules, } impl, V: Parse> Parse for SmallVec { diff --git a/src/properties/text.rs b/src/properties/text.rs index f423afe9..762649f9 100644 --- a/src/properties/text.rs +++ b/src/properties/text.rs @@ -962,11 +962,11 @@ impl ToCss for TextShadow { #[inline] fn is_text_decoration_property(property_id: &PropertyId) -> bool { match property_id { - PropertyId::TextDecorationLine | + PropertyId::TextDecorationLine(_) | PropertyId::TextDecorationThickness | - PropertyId::TextDecorationStyle | - PropertyId::TextDecorationColor | - PropertyId::TextDecoration => true, + PropertyId::TextDecorationStyle(_) | + PropertyId::TextDecorationColor(_) | + PropertyId::TextDecoration(_) => true, _ => false } } @@ -974,10 +974,10 @@ fn is_text_decoration_property(property_id: &PropertyId) -> bool { #[inline] fn is_text_emphasis_property(property_id: &PropertyId) -> bool { match property_id { - PropertyId::TextEmphasisStyle | - PropertyId::TextEmphasisColor | - PropertyId::TextEmphasis | - PropertyId::TextEmphasisPosition => true, + PropertyId::TextEmphasisStyle(_) | + PropertyId::TextEmphasisColor(_) | + PropertyId::TextEmphasis(_) | + PropertyId::TextEmphasisPosition(_) => true, _ => false } } \ No newline at end of file diff --git a/src/properties/transform.rs b/src/properties/transform.rs index eff14f85..9a6037e8 100644 --- a/src/properties/transform.rs +++ b/src/properties/transform.rs @@ -1440,9 +1440,9 @@ impl PropertyHandler for TransformHandler { Translate(val) => individual_property!(translate, val), Rotate(val) => individual_property!(rotate, val), Scale(val) => individual_property!(scale, val), - Unparsed(val) if matches!(val.property_id, PropertyId::Transform | PropertyId::Translate | PropertyId::Rotate | PropertyId::Scale) => { + Unparsed(val) if matches!(val.property_id, PropertyId::Transform(_) | PropertyId::Translate | PropertyId::Rotate | PropertyId::Scale) => { self.flush(dest); - let prop = if val.property_id == PropertyId::Transform { + let prop = if matches!(val.property_id, PropertyId::Transform(_)) { Property::Unparsed(val.get_prefixed(self.targets, Feature::Transform)) } else { property.clone() diff --git a/src/properties/transition.rs b/src/properties/transition.rs index 1cfb0626..741c1acf 100644 --- a/src/properties/transition.rs +++ b/src/properties/transition.rs @@ -1,6 +1,6 @@ use cssparser::*; use crate::traits::{Parse, ToCss, PropertyHandler}; -use crate::values::{ident::CustomIdent, time::Time, easing::EasingFunction}; +use crate::values::{time::Time, easing::EasingFunction}; use super::{Property, PropertyId}; use crate::vendor_prefix::VendorPrefix; use crate::declaration::DeclarationList; @@ -13,7 +13,7 @@ use crate::prefixes::Feature; /// https://www.w3.org/TR/2018/WD-css-transitions-1-20181011/#transition-shorthand-property #[derive(Debug, Clone, PartialEq)] pub struct Transition { - pub property: CustomIdent, + pub property: PropertyId, pub duration: Time, pub delay: Time, pub timing_function: EasingFunction @@ -49,7 +49,7 @@ impl Parse for Transition { } if property.is_none() { - if let Ok(value) = input.try_parse(CustomIdent::parse) { + if let Ok(value) = input.try_parse(PropertyId::parse) { property = Some(value); continue } @@ -59,7 +59,7 @@ impl Parse for Transition { } Ok(Transition { - property: property.unwrap_or(CustomIdent("all".into())), + property: property.unwrap_or(PropertyId::All), duration: duration.unwrap_or(Time::Seconds(0.0)), delay: delay.unwrap_or(Time::Seconds(0.0)), timing_function: timing_function.unwrap_or(EasingFunction::Ease) @@ -92,7 +92,7 @@ impl ToCss for Transition { #[derive(Default)] pub(crate) struct TransitionHandler { targets: Option, - properties: Option<(SmallVec<[CustomIdent; 1]>, VendorPrefix)>, + properties: Option<(SmallVec<[PropertyId; 1]>, VendorPrefix)>, durations: Option<(SmallVec<[Time; 1]>, VendorPrefix)>, delays: Option<(SmallVec<[Time; 1]>, VendorPrefix)>, timing_functions: Option<(SmallVec<[EasingFunction; 1]>, VendorPrefix)>, @@ -153,7 +153,7 @@ impl PropertyHandler for TransitionHandler { TransitionDelay(val, vp) => property!(TransitionDelay, delays, val, vp), TransitionTimingFunction(val, vp) => property!(TransitionTimingFunction, timing_functions, val, vp), Transition(val, vp) => { - let properties: SmallVec<[CustomIdent; 1]> = val.iter().map(|b| b.property.clone()).collect(); + let properties: SmallVec<[PropertyId; 1]> = val.iter().map(|b| b.property.clone()).collect(); property!(TransitionProperty, properties, &properties, vp); let durations: SmallVec<[Time; 1]> = val.iter().map(|b| b.duration.clone()).collect(); @@ -258,11 +258,11 @@ impl TransitionHandler { #[inline] fn is_transition_property(property_id: &PropertyId) -> bool { match property_id { - PropertyId::TransitionProperty | - PropertyId::TransitionDuration | - PropertyId::TransitionDelay | - PropertyId::TransitionTimingFunction | - PropertyId::Transition => true, + PropertyId::TransitionProperty(_) | + PropertyId::TransitionDuration(_) | + PropertyId::TransitionDelay(_) | + PropertyId::TransitionTimingFunction(_) | + PropertyId::Transition(_) => true, _ => false } } diff --git a/src/rules/keyframes.rs b/src/rules/keyframes.rs index 5bea704a..200ffa43 100644 --- a/src/rules/keyframes.rs +++ b/src/rules/keyframes.rs @@ -4,10 +4,12 @@ use crate::traits::{Parse, ToCss}; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::vendor_prefix::VendorPrefix; use crate::printer::Printer; +use crate::values::ident::CustomIdent; +use crate::parser::ParserOptions; #[derive(Debug, PartialEq)] pub struct KeyframesRule { - pub name: String, + pub name: CustomIdent, pub keyframes: Vec, pub vendor_prefix: VendorPrefix, pub loc: SourceLocation @@ -40,7 +42,7 @@ impl ToCss for KeyframesRule { dest.write_char('@')?; VendorPrefix::$prefix.to_css(dest)?; dest.write_str("keyframes ")?; - serialize_identifier(&self.name, dest)?; + self.name.to_css(dest)?; dest.whitespace()?; dest.write_char('{')?; dest.indent(); @@ -164,9 +166,11 @@ impl<'a, 'i> QualifiedRuleParser<'i> for KeyframeListParser { _: &ParserState, input: &mut Parser<'i, 't>, ) -> Result> { + // For now there are no options that apply within @keyframes + let options = ParserOptions::default(); Ok(Keyframe { selectors, - declarations: DeclarationBlock::parse(input)? + declarations: DeclarationBlock::parse(input, &options)? }) } } diff --git a/src/rules/style.rs b/src/rules/style.rs index 97dfdb4c..33a826d8 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -67,52 +67,71 @@ impl ToCssWithContext for StyleRule { impl StyleRule { fn to_css_base(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { // If supported, or there are no targets, preserve nesting. Otherwise, write nested rules after parent. - if self.rules.0.is_empty() || (dest.targets.is_none() || Feature::CssNesting.is_compatible(dest.targets.unwrap())) { + let supports_nesting = self.rules.0.is_empty() || dest.targets.is_none() || Feature::CssNesting.is_compatible(dest.targets.unwrap()); + let len = self.declarations.declarations.len(); + let has_declarations = supports_nesting || len > 0 || self.rules.0.is_empty(); + + if has_declarations { dest.add_mapping(self.loc); self.selectors.to_css_with_context(dest, context)?; dest.whitespace()?; dest.write_char('{')?; dest.indent(); - let len = self.declarations.declarations.len(); + for (i, decl) in self.declarations.declarations.iter().enumerate() { + // The CSS modules `composes` property is handled specially, and omitted during printing. + // We need to add the classes it references to the list for the selectors in this rule. + if let crate::properties::Property::Composes(composes) = &decl.property { + if let Some(css_module) = &mut dest.css_module { + css_module.handle_composes(&self.selectors, &composes) + .map_err(|_| std::fmt::Error)?; // TODO: error + continue; + } + } + dest.newline()?; decl.to_css(dest)?; if i != len - 1 || !dest.minify { dest.write_char(';')?; } } + } - if !dest.minify && len > 0 && !self.rules.0.is_empty() { - dest.write_char('\n')?; - dest.newline()?; - } - - self.rules.to_css(dest)?; - - dest.dedent(); - dest.newline()?; - dest.write_char('}')?; - } else { - let has_declarations = self.declarations.declarations.len() > 0 || self.rules.0.is_empty(); + macro_rules! newline { + () => { + if !dest.minify && (supports_nesting || len > 0) && !self.rules.0.is_empty() { + if len > 0 { + dest.write_char('\n')?; + } + dest.newline()?; + } + }; + } - // If there are any declarations in the rule, or no child rules, write the parent. - if has_declarations { - dest.add_mapping(self.loc); - self.selectors.to_css_with_context(dest, context)?; - self.declarations.to_css(dest)?; - if !dest.minify && !self.rules.0.is_empty() { - dest.write_char('\n')?; + macro_rules! end { + () => { + if has_declarations { + dest.dedent(); dest.newline()?; + dest.write_char('}')?; } - } + }; + } - // Write nested rules after the parent. + // Write nested rules after the parent. + if supports_nesting { + newline!(); + self.rules.to_css(dest)?; + end!(); + } else { + end!(); + newline!(); self.rules.to_css_with_context(dest, Some(&StyleContext { rule: self, parent: context }))?; } - + Ok(()) } } diff --git a/src/selector.rs b/src/selector.rs index ff7dcc13..11dc5af1 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -10,7 +10,7 @@ use crate::targets::Browsers; use crate::rules::{ToCssWithContext, StyleContext}; use std::collections::HashMap; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Selectors; #[derive(Debug, Clone, PartialEq, Eq, Default)] @@ -67,7 +67,8 @@ impl SelectorImpl for Selectors { pub struct SelectorParser<'a> { pub default_namespace: &'a Option, pub namespace_prefixes: &'a HashMap, - pub is_nesting_allowed: bool + pub is_nesting_allowed: bool, + pub css_modules: bool } impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a> { @@ -164,6 +165,8 @@ impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a> { let pseudo_class = match_ignore_ascii_case! { &name, "lang" => Lang(parser.expect_ident_or_string()?.as_ref().into()), "dir" => Dir(parser.expect_ident_or_string()?.as_ref().into()), + "local" if self.css_modules => Local(Box::new(parcel_selectors::parser::Selector::parse(self, parser)?)), + "global" if self.css_modules => Global(Box::new(parcel_selectors::parser::Selector::parse(self, parser)?)), _ => return Err(parser.new_custom_error(parcel_selectors::parser::SelectorParseErrorKind::UnexpectedIdent(name.clone()))), }; @@ -297,6 +300,10 @@ pub enum PseudoClass { // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill Autofill(VendorPrefix), + // CSS modules + Local(Box>), + Global(Box>), + Custom(String) } @@ -313,13 +320,13 @@ impl parcel_selectors::parser::NonTSPseudoClass for PseudoClass { } impl cssparser::ToCss for PseudoClass { - fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { - ToCss::to_css(self, &mut Printer::new(dest, None, false, None)) + fn to_css(&self, _: &mut W) -> fmt::Result where W: fmt::Write { + unreachable!() } } -impl ToCss for PseudoClass { - fn to_css(&self, dest: &mut Printer) -> fmt::Result where W: fmt::Write { +impl ToCssWithContext for PseudoClass { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> fmt::Result where W: fmt::Write { use PseudoClass::*; match &self { Lang(lang) => { @@ -420,6 +427,14 @@ impl ToCss for PseudoClass { // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill Autofill(prefix) => write_prefixed!(prefix, "autofill"), + Local(selector) => selector.to_css_with_context(dest, context), + Global(selector) => { + let css_module = std::mem::take(&mut dest.css_module); + selector.to_css_with_context(dest, context)?; + dest.css_module = css_module; + Ok(()) + }, + Lang(_) | Dir(_) => unreachable!(), Custom(val) => { dest.write_char(':')?; @@ -857,7 +872,7 @@ impl ToCssWithContext for Component { dest.write_str(")") }, NonTSPseudoClass(pseudo) => { - pseudo.to_css(dest) + pseudo.to_css_with_context(dest, context) }, PseudoElement(pseudo) => { pseudo.to_css(dest) @@ -865,6 +880,14 @@ impl ToCssWithContext for Component { Nesting => { serialize_nesting(dest, context, false) }, + Class(ref class) => { + dest.write_char('.')?; + dest.write_ident(&class.0) + } + ID(ref id) => { + dest.write_char('#')?; + dest.write_ident(&id.0) + } _ => { cssparser::ToCss::to_css(self, dest) } diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 4c049a79..e1a67517 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -6,20 +6,35 @@ use crate::printer::Printer; use crate::traits::ToCss; use crate::targets::Browsers; use crate::declaration::{DeclarationHandler, DeclarationBlock}; -use crate::traits::Parse; +use crate::css_modules::{hash, CssModule, CssModuleExports}; +use std::collections::HashMap; pub use crate::parser::ParserOptions; pub struct StyleSheet { pub filename: String, - pub rules: CssRuleList + pub rules: CssRuleList, + options: ParserOptions +} + +#[derive(Default)] +pub struct PrinterOptions { + pub minify: bool, + pub source_map: bool, + pub targets: Option +} + +pub struct ToCssResult { + pub code: String, + pub source_map: Option, + pub exports: Option } impl StyleSheet { pub fn parse<'i>(filename: String, code: &'i str, options: ParserOptions) -> Result> { let mut input = ParserInput::new(&code); let mut parser = Parser::new(&mut input); - let rule_list_parser = RuleListParser::new_for_stylesheet(&mut parser, TopLevelRuleParser::new(options)); + let rule_list_parser = RuleListParser::new_for_stylesheet(&mut parser, TopLevelRuleParser::new(&options)); let mut rules = vec![]; for rule in rule_list_parser { @@ -34,7 +49,8 @@ impl StyleSheet { Ok(StyleSheet { filename, - rules: CssRuleList(rules) + rules: CssRuleList(rules), + options }) } @@ -44,9 +60,9 @@ impl StyleSheet { self.rules.minify(targets, &mut handler, &mut important_handler); } - pub fn to_css(&self, minify: bool, source_map: bool, targets: Option) -> Result<(String, Option), std::fmt::Error> { + pub fn to_css(&self, options: PrinterOptions) -> Result { let mut dest = String::new(); - let mut source_map = if source_map { + let mut source_map = if options.source_map { let mut sm = SourceMap::new("/"); sm.add_source(&self.filename); Some(sm) @@ -54,11 +70,33 @@ impl StyleSheet { None }; - let mut printer = Printer::new(&mut dest, source_map.as_mut(), minify, targets); - self.rules.to_css(&mut printer)?; - printer.newline()?; + let mut printer = Printer::new(&mut dest, source_map.as_mut(), options.minify, options.targets); - Ok((dest, source_map)) + if self.options.css_modules { + let h = hash(&self.filename); + let mut exports = HashMap::new(); + printer.css_module = Some(CssModule { + hash: &h, + exports: &mut exports + }); + + self.rules.to_css(&mut printer)?; + printer.newline()?; + + Ok(ToCssResult { + code: dest, + source_map, + exports: Some(exports) + }) + } else { + self.rules.to_css(&mut printer)?; + printer.newline()?; + Ok(ToCssResult { + code: dest, + source_map, + exports: None + }) + } } } @@ -70,8 +108,9 @@ impl StyleAttribute { pub fn parse<'i>(code: &'i str) -> Result> { let mut input = ParserInput::new(&code); let mut parser = Parser::new(&mut input); + let options = ParserOptions::default(); Ok(StyleAttribute { - declarations: DeclarationBlock::parse(&mut parser)? + declarations: DeclarationBlock::parse(&mut parser, &options)? }) } diff --git a/src/traits.rs b/src/traits.rs index 55a33db3..8580aa5c 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -13,7 +13,7 @@ pub trait Parse: Sized { } /// Trait for things the can serialize themselves in CSS syntax. -pub trait ToCss { +pub(crate) trait ToCss { /// Serialize `self` in CSS syntax, writing to `dest`. fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write; diff --git a/src/values/ident.rs b/src/values/ident.rs index 66a62c8c..2321327f 100644 --- a/src/values/ident.rs +++ b/src/values/ident.rs @@ -3,7 +3,7 @@ use crate::traits::{Parse, ToCss}; use crate::printer::Printer; /// https://www.w3.org/TR/css-values-4/#custom-idents -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct CustomIdent(pub String); impl Parse for CustomIdent { @@ -25,6 +25,6 @@ impl Parse for CustomIdent { impl ToCss for CustomIdent { fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { - serialize_identifier(&self.0, dest) + dest.write_ident(&self.0) } } diff --git a/src/values/length.rs b/src/values/length.rs index 6501411e..6c7ca733 100644 --- a/src/values/length.rs +++ b/src/values/length.rs @@ -123,7 +123,7 @@ impl ToCss for LengthValue { } } -pub fn serialize_dimension(value: f32, unit: &str, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { +pub(crate) fn serialize_dimension(value: f32, unit: &str, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { use cssparser::ToCss; let int_value = if value.fract() == 0.0 { Some(value as i32) diff --git a/test.js b/test.js index adfe0ad3..8b9e475b 100644 --- a/test.js +++ b/test.js @@ -6,7 +6,7 @@ if (process.argv[process.argv.length - 1] !== __filename) { filename: process.argv[process.argv.length - 1], code: fs.readFileSync(process.argv[process.argv.length - 1]), minify: true, - source_map: true, + sourceMap: true, targets: { chrome: 95 << 16 } @@ -39,24 +39,20 @@ let res = css.transform({ code: Buffer.from(` .foo { - display: grid; - - & h1, & h2, &.bar { - color: red; - } - - @media (orientation: landscape) { - grid-auto-flow: column; - - & h1, & h2, &.bar { - color: red; - } - } + composes: bar; + composes: baz from "baz.css"; + color: pink; + } - @nest :not(&), .bar & { - color: blue; - } + .bar { + color: red; } -`)}); +`), + drafts: { + nesting: true + }, + cssModules: true +}); console.log(res.code.toString()); +console.log(res.exports);