diff --git a/node/src/lib.rs b/node/src/lib.rs index 92f2e8a6..28f985f2 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -178,12 +178,25 @@ struct Config { pub minify: Option, pub source_map: Option, pub drafts: Option, - pub css_modules: Option, + pub css_modules: Option, pub analyze_dependencies: Option, pub pseudo_classes: Option, pub unused_symbols: Option>, } +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum CssModulesOption { + Bool(bool), + Config(CssModulesConfig), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CssModulesConfig { + pattern: String, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BundleConfig { @@ -192,7 +205,7 @@ struct BundleConfig { pub minify: Option, pub source_map: Option, pub drafts: Option, - pub css_modules: Option, + pub css_modules: Option, pub analyze_dependencies: Option, pub pseudo_classes: Option, pub unused_symbols: Option>, @@ -237,7 +250,17 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result Some(parcel_css::css_modules::Config::default()), + CssModulesOption::Bool(false) => None, + CssModulesOption::Config(c) => Some(parcel_css::css_modules::Config { + pattern: parcel_css::css_modules::Pattern::parse(&c.pattern).unwrap(), + }), + } + } else { + None + }, source_index: 0, }, )?; @@ -288,7 +311,17 @@ fn compile_bundle<'i>(fs: &'i FileProvider, config: &BundleConfig) -> Result Some(parcel_css::css_modules::Config::default()), + CssModulesOption::Bool(false) => None, + CssModulesOption::Config(c) => Some(parcel_css::css_modules::Config { + pattern: parcel_css::css_modules::Pattern::parse(&c.pattern).unwrap(), + }), + } + } else { + None + }, ..ParserOptions::default() }; diff --git a/src/bundler.rs b/src/bundler.rs index 145af432..6db4d4d0 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -54,17 +54,17 @@ use std::{ /// A Bundler combines a CSS file and all imported dependencies together into /// a single merged style sheet. -pub struct Bundler<'a, 's, P> { +pub struct Bundler<'a, 'o, 's, P> { source_map: Option>, fs: &'a P, source_indexes: DashMap, - stylesheets: Mutex>>, - options: ParserOptions, + stylesheets: Mutex>>, + options: ParserOptions<'o>, } #[derive(Debug)] -struct BundleStyleSheet<'i> { - stylesheet: Option>, +struct BundleStyleSheet<'i, 'o> { + stylesheet: Option>, dependencies: Vec, parent_source_index: u32, parent_dep_index: u32, @@ -175,11 +175,11 @@ impl<'i> BundleErrorKind<'i> { } } -impl<'a, 's, P: SourceProvider> Bundler<'a, 's, P> { +impl<'a, 'o, 's, P: SourceProvider> Bundler<'a, 'o, 's, P> { /// Creates a new Bundler using the given source provider. /// If a source map is given, the content of each source file included in the bundle will /// be added accordingly. - pub fn new(fs: &'a P, source_map: Option<&'s mut SourceMap>, options: ParserOptions) -> Self { + pub fn new(fs: &'a P, source_map: Option<&'s mut SourceMap>, options: ParserOptions<'o>) -> Self { Bundler { source_map: source_map.map(Mutex::new), fs, @@ -190,7 +190,7 @@ impl<'a, 's, P: SourceProvider> Bundler<'a, 's, P> { } /// Bundles the given entry file and all dependencies into a single style sheet. - pub fn bundle<'e>(&mut self, entry: &'e Path) -> Result, Error>> { + pub fn bundle<'e>(&mut self, entry: &'e Path) -> Result, Error>> { // Phase 1: load and parse all files. This is done in parallel. self.load_file( &entry, @@ -413,7 +413,7 @@ impl<'a, 's, P: SourceProvider> Bundler<'a, 's, P> { fn order(&mut self) { process(self.stylesheets.get_mut().unwrap(), 0, &mut HashSet::new()); - fn process(stylesheets: &mut Vec>, source_index: u32, visited: &mut HashSet) { + fn process(stylesheets: &mut Vec, source_index: u32, visited: &mut HashSet) { if visited.contains(&source_index) { return; } @@ -436,7 +436,11 @@ impl<'a, 's, P: SourceProvider> Bundler<'a, 's, P> { fn inline(&mut self, dest: &mut Vec>) { process(self.stylesheets.get_mut().unwrap(), 0, dest); - fn process<'a>(stylesheets: &mut Vec>, source_index: u32, dest: &mut Vec>) { + fn process<'a>( + stylesheets: &mut Vec>, + source_index: u32, + dest: &mut Vec>, + ) { let stylesheet = &mut stylesheets[source_index as usize]; let mut rules = std::mem::take(&mut stylesheet.stylesheet.as_mut().unwrap().rules.0); @@ -589,7 +593,7 @@ mod tests { &fs, None, ParserOptions { - css_modules: true, + css_modules: Some(Default::default()), ..ParserOptions::default() }, ); diff --git a/src/css_modules.rs b/src/css_modules.rs index 5645b29e..e728fc58 100644 --- a/src/css_modules.rs +++ b/src/css_modules.rs @@ -15,9 +15,108 @@ use data_encoding::{Encoding, Specification}; use lazy_static::lazy_static; use parcel_selectors::SelectorList; use serde::Serialize; +use smallvec::{smallvec, SmallVec}; use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; +use std::fmt::Write; use std::hash::{Hash, Hasher}; +use std::path::Path; + +/// Configuration for CSS modules. +#[derive(Default, Clone, Debug)] +pub struct Config<'i> { + /// The class name pattern to use. Default is `[hash]_[local]`. + pub pattern: Pattern<'i>, +} + +/// A CSS modules class name pattern. +#[derive(Clone, Debug)] +pub struct Pattern<'i> { + /// The list of segments in the pattern. + pub segments: SmallVec<[Segment<'i>; 2]>, +} + +impl<'i> Default for Pattern<'i> { + fn default() -> Self { + Pattern { + segments: smallvec![Segment::Hash, Segment::Literal("_"), Segment::Local], + } + } +} + +impl<'i> Pattern<'i> { + /// Parse a pattern from a string. + pub fn parse(mut input: &'i str) -> Result { + let mut segments = SmallVec::new(); + while !input.is_empty() { + if input.starts_with('[') { + if let Some(end_idx) = input.find(']') { + let segment = match &input[0..=end_idx] { + "[name]" => Segment::Name, + "[local]" => Segment::Local, + "[hash]" => Segment::Hash, + _ => return Err(()), + }; + segments.push(segment); + input = &input[end_idx + 1..]; + } else { + return Err(()); + } + } else { + let end_idx = input.find('[').unwrap_or_else(|| input.len()); + segments.push(Segment::Literal(&input[0..end_idx])); + input = &input[end_idx..]; + } + } + + Ok(Pattern { segments }) + } + + /// Write the substituted pattern to a destination. + pub fn write(&self, hash: &str, path: &Path, local: &str, mut write: W) -> Result<(), E> + where + W: FnMut(&str) -> Result<(), E>, + { + for segment in &self.segments { + match segment { + Segment::Literal(s) => { + write(s)?; + } + Segment::Name => { + write(path.file_stem().unwrap().to_str().unwrap())?; + } + Segment::Local => { + write(local)?; + } + Segment::Hash => { + write(hash)?; + } + } + } + Ok(()) + } + + fn write_to_string(&self, hash: &str, path: &Path, local: &str) -> Result { + let mut res = String::new(); + self.write(hash, path, local, |s| res.write_str(s))?; + Ok(res) + } +} + +/// A segment in a CSS modules class name pattern. +/// +/// See [Pattern](Pattern). +#[derive(Clone, Debug)] +pub enum Segment<'i> { + /// A literal string segment. + Literal(&'i str), + /// The base file name. + Name, + /// The original class name. + Local, + /// A hash of the file name. + Hash, +} /// A referenced name within a CSS module, e.g. via the `composes` property. /// @@ -69,15 +168,26 @@ lazy_static! { }; } -pub(crate) struct CssModule<'a> { - pub hash: &'a str, +pub(crate) struct CssModule<'a, 'b, 'c> { + pub config: &'a Config<'b>, + pub path: &'c Path, + pub hash: String, pub exports: &'a mut CssModuleExports, } -impl<'a> CssModule<'a> { +impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> { + pub fn new(config: &'a Config<'b>, filename: &'c str, exports: &'a mut CssModuleExports) -> Self { + Self { + config, + path: Path::new(filename), + hash: hash(filename, matches!(config.pattern.segments[0], Segment::Hash)), + exports, + } + } + pub fn add_local(&mut self, exported: &str, local: &str) { self.exports.entry(exported.into()).or_insert_with(|| CssModuleExport { - name: get_hashed_name(self.hash, local), + name: self.config.pattern.write_to_string(&self.hash, &self.path, local).unwrap(), composes: vec![], is_referenced: false, }); @@ -90,7 +200,7 @@ impl<'a> CssModule<'a> { } std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(CssModuleExport { - name: get_hashed_name(self.hash, name), + name: self.config.pattern.write_to_string(&self.hash, &self.path, name).unwrap(), composes: vec![], is_referenced: true, }); @@ -110,7 +220,11 @@ impl<'a> CssModule<'a> { for name in &composes.names { let reference = match &composes.from { None => CssModuleReference::Local { - name: get_hashed_name(self.hash, name.0.as_ref()), + name: self + .config + .pattern + .write_to_string(&self.hash, &self.path, name.0.as_ref()) + .unwrap(), }, Some(ComposesFrom::Global) => CssModuleReference::Global { name: name.0.as_ref().into(), @@ -140,19 +254,13 @@ impl<'a> CssModule<'a> { } } -fn get_hashed_name(hash: &str, name: &str) -> String { - // Hash must come first so that CSS grid identifiers work. - // This is because grid lines may have an implicit -start or -end appended. - format!("{}_{}", hash, name) -} - -pub(crate) fn hash(s: &str) -> String { +pub(crate) fn hash(s: &str, at_start: bool) -> String { let mut hasher = DefaultHasher::new(); s.hash(&mut hasher); let hash = hasher.finish() as u32; let hash = ENCODER.encode(&hash.to_le_bytes()); - if matches!(hash.as_bytes()[0], b'0'..=b'9') { + if at_start && matches!(hash.as_bytes()[0], b'0'..=b'9') { format!("_{}", hash) } else { hash diff --git a/src/declaration.rs b/src/declaration.rs index 6948233b..f03ca527 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -52,9 +52,9 @@ pub struct DeclarationBlock<'i> { impl<'i> DeclarationBlock<'i> { /// Parses a declaration block from CSS syntax. - pub fn parse<'t>( + pub fn parse<'a, 'o, 't>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &'a ParserOptions<'o>, ) -> Result>> { let mut important_declarations = DeclarationList::new(); let mut declarations = DeclarationList::new(); @@ -79,7 +79,10 @@ impl<'i> DeclarationBlock<'i> { } /// Parses a declaration block from a string. - pub fn parse_string(input: &'i str, options: ParserOptions) -> Result>> { + pub fn parse_string<'o>( + input: &'i str, + options: ParserOptions<'o>, + ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); let result = Self::parse(&mut parser, &options)?; @@ -371,14 +374,14 @@ impl<'i> DeclarationBlock<'i> { } } -struct PropertyDeclarationParser<'a, 'i> { +struct PropertyDeclarationParser<'a, 'o, 'i> { important_declarations: &'a mut Vec>, declarations: &'a mut Vec>, - options: &'a ParserOptions, + options: &'a ParserOptions<'o>, } /// Parse a declaration within {} block: `color: blue` -impl<'a, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a, 'i> { +impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> { type Declaration = (); type Error = ParserError<'i>; @@ -398,7 +401,7 @@ impl<'a, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a, } /// Default methods reject all at rules. -impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'i> { +impl<'a, 'o, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> { type Prelude = (); type AtRule = (); type Error = ParserError<'i>; diff --git a/src/dependencies.rs b/src/dependencies.rs index 69c6684e..5cfd3a2a 100644 --- a/src/dependencies.rs +++ b/src/dependencies.rs @@ -87,7 +87,7 @@ pub struct UrlDependency { impl UrlDependency { /// Creates a new url dependency. pub fn new(url: &Url, filename: &str) -> UrlDependency { - let placeholder = hash(&format!("{}_{}", filename, url.url)); + let placeholder = hash(&format!("{}_{}", filename, url.url), false); UrlDependency { url: url.url.to_string(), placeholder, diff --git a/src/error.rs b/src/error.rs index c4186871..c478eb10 100644 --- a/src/error.rs +++ b/src/error.rs @@ -345,6 +345,8 @@ pub enum PrinterErrorKind { InvalidComposesNesting, /// The CSS modules `composes` property cannot be used with a simple class selector. InvalidComposesSelector, + /// The CSS modules pattern must end with `[local]` for use in CSS grid. + InvalidCssModulesPatternInGrid, } impl From for PrinterError { @@ -364,6 +366,7 @@ impl fmt::Display for PrinterErrorKind { FmtError => write!(f, "Printer error"), InvalidComposesNesting => write!(f, "The `composes` property cannot be used within nested rules"), InvalidComposesSelector => write!(f, "The `composes` property cannot be used with a simple class selector"), + InvalidCssModulesPatternInGrid => write!(f, "The CSS modules `pattern` config must end with `[local]` for use in CSS grid line names."), } } } diff --git a/src/lib.rs b/src/lib.rs index 724470e7..321da0c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,12 +151,17 @@ mod tests { assert_eq!(res.code, expected); } - fn css_modules_test(source: &str, expected: &str, expected_exports: CssModuleExports) { + fn css_modules_test<'i>( + source: &'i str, + expected: &str, + expected_exports: CssModuleExports, + config: crate::css_modules::Config<'i>, + ) { let mut stylesheet = StyleSheet::parse( "test.css", &source, ParserOptions { - css_modules: true, + css_modules: Some(config), ..ParserOptions::default() }, ) @@ -15774,6 +15779,7 @@ mod tests { "circles" => "EgL3uq_circles" referenced: true, "fade" => "EgL3uq_fade" }, + Default::default(), ); #[cfg(feature = "grid")] @@ -15816,6 +15822,7 @@ mod tests { "a" => "EgL3uq_a", "b" => "EgL3uq_b" }, + Default::default(), ); #[cfg(feature = "grid")] @@ -15852,6 +15859,7 @@ mod tests { "grid" => "EgL3uq_grid", "bar" => "EgL3uq_bar" }, + Default::default(), ); css_modules_test( @@ -15866,6 +15874,7 @@ mod tests { } "#}, map! {}, + Default::default(), ); css_modules_test( @@ -15898,6 +15907,7 @@ mod tests { map! { "bar" => "EgL3uq_bar" }, + Default::default(), ); // :global(:local(.hi)) { @@ -15928,6 +15938,7 @@ mod tests { "test" => "EgL3uq_test" "EgL3uq_foo", "foo" => "EgL3uq_foo" }, + Default::default(), ); css_modules_test( @@ -15955,6 +15966,7 @@ mod tests { "b" => "EgL3uq_b" "EgL3uq_foo", "foo" => "EgL3uq_foo" }, + Default::default(), ); css_modules_test( @@ -15990,6 +16002,7 @@ mod tests { "foo" => "EgL3uq_foo", "bar" => "EgL3uq_bar" }, + Default::default(), ); css_modules_test( @@ -16007,6 +16020,7 @@ mod tests { map! { "test" => "EgL3uq_test" "foo" global: true }, + Default::default(), ); css_modules_test( @@ -16024,6 +16038,7 @@ mod tests { map! { "test" => "EgL3uq_test" "foo" global: true "bar" global: true }, + Default::default(), ); css_modules_test( @@ -16041,6 +16056,7 @@ mod tests { map! { "test" => "EgL3uq_test" "foo" from "foo.css" }, + Default::default(), ); css_modules_test( @@ -16058,6 +16074,7 @@ mod tests { map! { "test" => "EgL3uq_test" "foo" from "foo.css" "bar" from "foo.css" }, + Default::default(), ); css_modules_test( @@ -16086,7 +16103,56 @@ mod tests { "test" => "EgL3uq_test" "EgL3uq_foo" "foo" from "foo.css" "bar" from "bar.css", "foo" => "EgL3uq_foo" }, + Default::default(), ); + + css_modules_test( + r#" + .foo { + color: red; + } + "#, + indoc! {r#" + .test-EgL3uq-foo { + color: red; + } + "#}, + map! { + "foo" => "test-EgL3uq-foo" + }, + crate::css_modules::Config { + pattern: crate::css_modules::Pattern::parse("test-[hash]-[local]").unwrap(), + }, + ); + + let stylesheet = StyleSheet::parse( + "test.css", + r#" + .grid { + grid-template-areas: "foo"; + } + + .foo { + grid-area: foo; + } + + .bar { + grid-column-start: foo-start; + } + "#, + ParserOptions { + css_modules: Some(crate::css_modules::Config { + pattern: crate::css_modules::Pattern::parse("test-[local]-[hash]").unwrap(), + }), + ..ParserOptions::default() + }, + ) + .unwrap(); + if let Err(err) = stylesheet.to_css(PrinterOptions::default()) { + assert_eq!(err.kind, PrinterErrorKind::InvalidCssModulesPatternInGrid); + } else { + unreachable!() + } } #[test] @@ -16149,7 +16215,7 @@ mod tests { "test.css", &source, ParserOptions { - css_modules: true, + css_modules: Some(Default::default()), ..ParserOptions::default() }, ) @@ -17866,14 +17932,14 @@ mod tests { dep_test( ".foo { background: image-set('./img12x.png', './img21x.png' 2x)}", - ".foo{background:image-set(\"hXFI8W\",\"_5TkpBa\" 2x)}", - vec![("./img12x.png", "hXFI8W"), ("./img21x.png", "_5TkpBa")], + ".foo{background:image-set(\"hXFI8W\",\"5TkpBa\" 2x)}", + vec![("./img12x.png", "hXFI8W"), ("./img21x.png", "5TkpBa")], ); dep_test( ".foo { background: image-set(url(./img12x.png), url('./img21x.png') 2x)}", - ".foo{background:image-set(\"hXFI8W\",\"_5TkpBa\" 2x)}", - vec![("./img12x.png", "hXFI8W"), ("./img21x.png", "_5TkpBa")], + ".foo{background:image-set(\"hXFI8W\",\"5TkpBa\" 2x)}", + vec![("./img12x.png", "hXFI8W"), ("./img21x.png", "5TkpBa")], ); dep_test( @@ -17890,8 +17956,8 @@ mod tests { dep_test( ".foo { --test: url(\"http://example.com/foo.png\") }", - ".foo{--test:url(\"_3X1zSW\")}", - vec![("http://example.com/foo.png", "_3X1zSW")], + ".foo{--test:url(\"3X1zSW\")}", + vec![("http://example.com/foo.png", "3X1zSW")], ); dep_test( diff --git a/src/main.rs b/src/main.rs index 9fca837e..ac2d3316 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,7 +59,7 @@ pub fn main() -> Result<(), std::io::Error> { let filename = filename.to_str().unwrap(); let options = ParserOptions { nesting: cli_args.nesting, - css_modules: cli_args.css_modules.is_some(), + css_modules: cli_args.css_modules.as_ref().map(|_| Default::default()), custom_media: cli_args.custom_media, ..ParserOptions::default() }; diff --git a/src/parser.rs b/src/parser.rs index 89e5a95f..f7746970 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -32,13 +32,13 @@ use std::collections::HashMap; /// CSS parsing options. #[derive(Default, Clone, Debug)] -pub struct ParserOptions { +pub struct ParserOptions<'i> { /// Whether the enable the [CSS nesting](https://www.w3.org/TR/css-nesting-1/) draft syntax. pub nesting: bool, /// Whether to enable the [custom media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) draft syntax. pub custom_media: bool, /// Whether the enable [CSS modules](https://github.com/css-modules/css-modules). - pub css_modules: bool, + pub css_modules: Option>, /// The source index to assign to all parsed rules. Impacts the source map when /// the style sheet is serialized. pub source_index: u32, @@ -54,15 +54,15 @@ enum State { } /// The parser for the top-level rules in a stylesheet. -pub struct TopLevelRuleParser<'a, 'i> { +pub struct TopLevelRuleParser<'a, 'o, 'i> { default_namespace: Option>, namespace_prefixes: HashMap, CowArcStr<'i>>, - options: &'a ParserOptions, + options: &'a ParserOptions<'o>, state: State, } -impl<'a, 'b, 'i> TopLevelRuleParser<'a, 'i> { - pub fn new(options: &'a ParserOptions) -> TopLevelRuleParser<'a, 'i> { +impl<'a, 'o, 'b, 'i> TopLevelRuleParser<'a, 'o, 'i> { + pub fn new(options: &'a ParserOptions<'o>) -> Self { TopLevelRuleParser { default_namespace: None, namespace_prefixes: HashMap::new(), @@ -71,7 +71,7 @@ impl<'a, 'b, 'i> TopLevelRuleParser<'a, 'i> { } } - fn nested<'x: 'b>(&'x mut self) -> NestedRuleParser<'_, 'i> { + fn nested<'x: 'b>(&'x mut self) -> NestedRuleParser<'_, 'o, 'i> { NestedRuleParser { default_namespace: &mut self.default_namespace, namespace_prefixes: &mut self.namespace_prefixes, @@ -125,7 +125,7 @@ pub enum AtRulePrelude<'i> { Property(DashedIdent<'i>), } -impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a, 'i> { +impl<'a, 'o, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i> { type Prelude = AtRulePrelude<'i>; type AtRule = (SourcePosition, CssRule<'i>); type Error = ParserError<'i>; @@ -262,7 +262,7 @@ impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a, 'i> { } } -impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser<'a, 'i> { +impl<'a, 'o, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i> { type Prelude = SelectorList<'i, Selectors>; type QualifiedRule = (SourcePosition, CssRule<'i>); type Error = ParserError<'i>; @@ -289,13 +289,13 @@ impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser<'a, 'i> { } #[derive(Clone)] -struct NestedRuleParser<'a, 'i> { +struct NestedRuleParser<'a, 'o, 'i> { default_namespace: &'a Option>, namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a ParserOptions, + options: &'a ParserOptions<'o>, } -impl<'a, 'b, 'i> NestedRuleParser<'a, 'i> { +impl<'a, 'o, 'b, 'i> NestedRuleParser<'a, 'o, 'i> { fn parse_nested_rules<'t>(&mut self, input: &mut Parser<'i, 't>) -> CssRuleList<'i> { let nested_parser = NestedRuleParser { default_namespace: self.default_namespace, @@ -328,7 +328,7 @@ impl<'a, 'b, 'i> NestedRuleParser<'a, 'i> { } } -impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'i> { +impl<'a, 'o, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'o, 'i> { type Prelude = AtRulePrelude<'i>; type AtRule = CssRule<'i>; type Error = ParserError<'i>; @@ -555,7 +555,7 @@ impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'i> { } } -impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'i> { +impl<'a, 'o, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'o, 'i> { type Prelude = SelectorList<'i, Selectors>; type QualifiedRule = CssRule<'i>; type Error = ParserError<'i>; @@ -568,7 +568,7 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'i> { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: false, - css_modules: self.options.css_modules, + css_modules: self.options.css_modules.is_some(), }; SelectorList::parse(&selector_parser, input, NestingRequirement::None) } @@ -595,11 +595,11 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'i> { } } -fn parse_declarations_and_nested_rules<'a, 'i, 't>( +fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't>( input: &mut Parser<'i, 't>, default_namespace: &'a Option>, namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a ParserOptions, + options: &'a ParserOptions<'o>, ) -> Result<(DeclarationBlock<'i>, CssRuleList<'i>), ParseError<'i, ParserError<'i>>> { let mut important_declarations = DeclarationList::new(); let mut declarations = DeclarationList::new(); @@ -643,17 +643,17 @@ fn parse_declarations_and_nested_rules<'a, 'i, 't>( )) } -pub struct StyleRuleParser<'a, 'i> { +pub struct StyleRuleParser<'a, 'o, 'i> { default_namespace: &'a Option>, namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a ParserOptions, + options: &'a ParserOptions<'o>, declarations: &'a mut DeclarationList<'i>, important_declarations: &'a mut DeclarationList<'i>, rules: &'a mut CssRuleList<'i>, } /// Parse a declaration within {} block: `color: blue` -impl<'a, 'i> cssparser::DeclarationParser<'i> for StyleRuleParser<'a, 'i> { +impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for StyleRuleParser<'a, 'o, 'i> { type Declaration = (); type Error = ParserError<'i>; @@ -676,7 +676,7 @@ impl<'a, 'i> cssparser::DeclarationParser<'i> for StyleRuleParser<'a, 'i> { } } -impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a, 'i> { +impl<'a, 'o, 'i> AtRuleParser<'i> for StyleRuleParser<'a, 'o, 'i> { type Prelude = AtRulePrelude<'i>; type AtRule = (); type Error = ParserError<'i>; @@ -700,7 +700,7 @@ impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a, 'i> { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: true, - css_modules: self.options.css_modules + css_modules: self.options.css_modules.is_some() }; let selectors = SelectorList::parse(&selector_parser, input, NestingRequirement::Contained)?; Ok(AtRulePrelude::Nest(selectors)) @@ -777,12 +777,12 @@ impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a, 'i> { } #[inline] -fn parse_nested_at_rule<'a, 'i, 't>( +fn parse_nested_at_rule<'a, 'o, 'i, 't>( input: &mut Parser<'i, 't>, source_index: u32, default_namespace: &'a Option>, namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a ParserOptions, + options: &'a ParserOptions<'o>, ) -> Result, ParseError<'i, ParserError<'i>>> { let loc = input.current_source_location(); let loc = Location { @@ -814,7 +814,7 @@ fn parse_nested_at_rule<'a, 'i, 't>( Ok(rules) } -impl<'a, 'b, 'i> QualifiedRuleParser<'i> for StyleRuleParser<'a, 'i> { +impl<'a, 'o, 'b, 'i> QualifiedRuleParser<'i> for StyleRuleParser<'a, 'o, 'i> { type Prelude = SelectorList<'i, Selectors>; type QualifiedRule = (); type Error = ParserError<'i>; @@ -827,7 +827,7 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for StyleRuleParser<'a, 'i> { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: true, - css_modules: self.options.css_modules, + css_modules: self.options.css_modules.is_some(), }; SelectorList::parse(&selector_parser, input, NestingRequirement::Prefixed) } diff --git a/src/printer.rs b/src/printer.rs index 7bd2936e..b836f061 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -6,7 +6,7 @@ use crate::error::{Error, ErrorLocation, PrinterError, PrinterErrorKind}; use crate::rules::Location; use crate::targets::Browsers; use crate::vendor_prefix::VendorPrefix; -use cssparser::serialize_identifier; +use cssparser::{serialize_identifier, serialize_name}; use parcel_sourcemap::{OriginalLocation, SourceMap}; /// Options that control how CSS is serialized to a string. @@ -57,11 +57,11 @@ pub struct PseudoClasses<'a> { /// /// `Printer` also includes helper functions that assist with writing output /// that respects options such as `minify`, and `css_modules`. -pub struct Printer<'a, W> { - pub(crate) sources: Option<&'a Vec>, +pub struct Printer<'a, 'b, 'c, W> { + pub(crate) sources: Option<&'c Vec>, dest: &'a mut W, source_map: Option<&'a mut SourceMap>, - pub(crate) source_index: u32, + pub(crate) loc: Location, indent: u8, line: u32, col: u32, @@ -71,19 +71,23 @@ pub struct Printer<'a, W> { /// the vendor prefix of whatever is being printed. pub(crate) vendor_prefix: VendorPrefix, pub(crate) in_calc: bool, - pub(crate) css_module: Option>, + pub(crate) css_module: Option>, pub(crate) dependencies: Option>, pub(crate) pseudo_classes: Option>, } -impl<'a, W: std::fmt::Write + Sized> Printer<'a, W> { +impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { /// Create a new Printer wrapping the given destination. - pub fn new(dest: &'a mut W, options: PrinterOptions<'a>) -> Printer<'a, W> { + pub fn new(dest: &'a mut W, options: PrinterOptions<'a>) -> Self { Printer { sources: None, dest, source_map: options.source_map, - source_index: 0, + loc: Location { + source_index: 0, + line: 0, + column: 1, + }, indent: 0, line: 0, col: 0, @@ -102,9 +106,9 @@ impl<'a, W: std::fmt::Write + Sized> Printer<'a, W> { } /// Returns the current source filename that is being printed. - pub fn filename(&self) -> &str { + pub fn filename(&self) -> &'c str { if let Some(sources) = self.sources { - if let Some(f) = sources.get(self.source_index as usize) { + if let Some(f) = sources.get(self.loc.source_index as usize) { f } else { "unknown.css" @@ -200,7 +204,7 @@ impl<'a, W: std::fmt::Write + Sized> Printer<'a, W> { /// Adds a mapping to the source map, if any. pub fn add_mapping(&mut self, loc: Location) { - self.source_index = loc.source_index; + self.loc = loc; if let Some(map) = &mut self.source_map { map.add_mapping( self.line, @@ -219,19 +223,26 @@ impl<'a, W: std::fmt::Write + Sized> Printer<'a, W> { /// as appropriate. If the `css_modules` option was enabled, then a hash /// is added, and the mapping is added to the CSS module. pub fn write_ident(&mut self, ident: &str) -> Result<(), PrinterError> { - let hash = if let Some(css_module) = &self.css_module { - Some(css_module.hash) + let css_module = self.css_module.as_ref(); + if let Some(css_module) = css_module { + let dest = &mut self.dest; + let mut first = true; + css_module + .config + .pattern + .write(&css_module.hash, &css_module.path, ident, |s| { + self.col += s.len() as u32; + if first { + first = false; + serialize_identifier(s, dest) + } else { + serialize_name(s, dest) + } + })?; } else { - None - }; - - if let Some(hash) = hash { - serialize_identifier(hash, self)?; - self.write_char('_')?; + serialize_identifier(ident, self)?; } - serialize_identifier(ident, self)?; - if let Some(css_module) = &mut self.css_module { css_module.add_local(&ident, &ident); } @@ -252,7 +263,7 @@ impl<'a, W: std::fmt::Write + Sized> Printer<'a, W> { } } -impl<'a, W: std::fmt::Write + Sized> std::fmt::Write for Printer<'a, W> { +impl<'a, 'b, 'c, W: std::fmt::Write + Sized> std::fmt::Write for Printer<'a, 'b, 'c, W> { fn write_str(&mut self, s: &str) -> std::fmt::Result { self.col += s.len() as u32; self.dest.write_str(s) diff --git a/src/properties/grid.rs b/src/properties/grid.rs index 16ec5991..9fc0e166 100644 --- a/src/properties/grid.rs +++ b/src/properties/grid.rs @@ -4,7 +4,7 @@ use crate::context::PropertyHandlerContext; use crate::declaration::{DeclarationBlock, DeclarationList}; -use crate::error::{ParserError, PrinterError}; +use crate::error::{Error, ErrorLocation, ParserError, PrinterError, PrinterErrorKind}; use crate::macros::{define_shorthand, impl_shorthand}; use crate::printer::Printer; use crate::properties::{Property, PropertyId}; @@ -374,11 +374,33 @@ where } else { dest.write_char(' ')?; } - name.to_css(dest)?; + write_ident(&name.0, dest)?; } dest.write_char(']') } +fn write_ident(name: &str, dest: &mut Printer) -> Result<(), PrinterError> +where + W: std::fmt::Write, +{ + if let Some(css_module) = &mut dest.css_module { + if let Some(last) = css_module.config.pattern.segments.last() { + if !matches!(last, crate::css_modules::Segment::Local) { + return Err(Error { + kind: PrinterErrorKind::InvalidCssModulesPatternInGrid, + loc: Some(ErrorLocation { + filename: dest.filename().into(), + line: dest.loc.line, + column: dest.loc.column, + }), + }); + } + } + } + dest.write_ident(name)?; + Ok(()) +} + impl<'i> Parse<'i> for TrackList<'i> { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { let mut line_names = Vec::new(); @@ -663,7 +685,7 @@ impl GridTemplateAreas { if i > 0 && (!last_was_null || !dest.minify) { dest.write_char(' ')?; } - dest.write_ident(string)?; + write_ident(string, dest)?; last_was_null = false; } else { if i > 0 && (last_was_null || !dest.minify) { @@ -1266,12 +1288,12 @@ impl ToCss for GridLine<'_> { { match self { GridLine::Auto => dest.write_str("auto"), - GridLine::Ident(id) => id.to_css(dest), + GridLine::Ident(id) => write_ident(&id.0, dest), GridLine::Line(line_number, id) => { line_number.to_css(dest)?; if let Some(id) = id { dest.write_char(' ')?; - id.to_css(dest)?; + write_ident(&id.0, dest)?; } Ok(()) } @@ -1285,7 +1307,7 @@ impl ToCss for GridLine<'_> { } if let Some(id) = id { - id.to_css(dest)?; + write_ident(&id.0, dest)?; } Ok(()) } diff --git a/src/properties/mod.rs b/src/properties/mod.rs index dcbb18bb..d3490af5 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -547,7 +547,7 @@ macro_rules! define_properties { match property_id { $( $(#[$meta])* - PropertyId::$property$((vp_name!($vp, prefix)))? $(if options.$condition)? => { + PropertyId::$property$((vp_name!($vp, prefix)))? $(if options.$condition.is_some())? => { if let Ok(c) = <$type>::parse(input) { if input.expect_exhausted().is_ok() { return Ok(Property::$property(c $(, vp_name!($vp, prefix))?)) diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 18c8eb79..55b0cd44 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -197,14 +197,17 @@ impl<'i> CssRule<'i> { /// Parse a single rule. pub fn parse<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'i>, ) -> Result>> { let (_, rule) = parse_one_rule(input, &mut TopLevelRuleParser::new(&options))?; Ok(rule) } /// Parse a single rule from a string. - pub fn parse_string(input: &'i str, options: ParserOptions) -> Result>> { + pub fn parse_string( + input: &'i str, + options: ParserOptions<'i>, + ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); Self::parse(&mut parser, &options) diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 6eb6561c..b0fc2dba 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -5,7 +5,7 @@ use crate::compat::Feature; use crate::context::{DeclarationContext, PropertyHandlerContext}; -use crate::css_modules::{hash, CssModule, CssModuleExports}; +use crate::css_modules::{CssModule, CssModuleExports}; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::dependencies::Dependency; use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterError, PrinterErrorKind}; @@ -58,7 +58,7 @@ pub use crate::printer::PseudoClasses; /// ``` #[derive(Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct StyleSheet<'i> { +pub struct StyleSheet<'i, 'o> { /// A list of top-level rules within the style sheet. #[cfg_attr(feature = "serde", serde(borrow))] pub rules: CssRuleList<'i>, @@ -67,7 +67,7 @@ pub struct StyleSheet<'i> { pub sources: Vec, #[cfg_attr(feature = "serde", serde(skip))] /// The options the style sheet was originally parsed with. - options: ParserOptions, + options: ParserOptions<'o>, } /// Options for the `minify` function of a [StyleSheet](StyleSheet) @@ -94,9 +94,9 @@ pub struct ToCssResult { pub dependencies: Option>, } -impl<'i> StyleSheet<'i> { +impl<'i, 'o> StyleSheet<'i, 'o> { /// Creates a new style sheet with the given source filenames and rules. - pub fn new(sources: Vec, rules: CssRuleList, options: ParserOptions) -> StyleSheet { + pub fn new(sources: Vec, rules: CssRuleList<'i>, options: ParserOptions<'o>) -> StyleSheet<'i, 'o> { StyleSheet { sources, rules, @@ -105,11 +105,7 @@ impl<'i> StyleSheet<'i> { } /// Parse a style sheet from a string. - pub fn parse( - filename: &str, - code: &'i str, - options: ParserOptions, - ) -> Result, Error>> { + pub fn parse(filename: &str, code: &'i str, options: ParserOptions<'o>) -> Result>> { let filename = String::from(filename); let mut input = ParserInput::new(&code); let mut parser = Parser::new(&mut input); @@ -184,13 +180,9 @@ impl<'i> StyleSheet<'i> { printer.sources = Some(&self.sources); - if self.options.css_modules { - let h = hash(printer.filename()); + if let Some(config) = &self.options.css_modules { let mut exports = HashMap::new(); - printer.css_module = Some(CssModule { - hash: &h, - exports: &mut exports, - }); + printer.css_module = Some(CssModule::new(config, printer.filename(), &mut exports)); self.rules.to_css(&mut printer)?; printer.newline()?; diff --git a/test.js b/test.js index 699ae573..f02013de 100644 --- a/test.js +++ b/test.js @@ -37,26 +37,21 @@ let res = css.transform({ opera: 10 << 16 | 5 << 8 }, code: Buffer.from(` - @import "foo.css"; - @import "bar.css" print; - @import "baz.css" supports(display: grid); - .foo { - composes: bar; - composes: baz from "baz.css"; color: pink; } .bar { color: red; - background: url(test.jpg); } `), drafts: { - nesting: true + // nesting: true + }, + cssModules: { + pattern: '[name]-[hash]-[local]' }, - cssModules: true, - analyzeDependencies: true + // analyzeDependencies: true }); console.log(res.code.toString());