From b80f5ca770d28777edc894abe56969bf77911bdb Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sun, 26 Dec 2021 21:41:33 -0500 Subject: [PATCH 01/12] Hash identifiers --- src/lib.rs | 127 +++++++++++++++++++++++++++++++++++++++++ src/parser.rs | 4 +- src/printer.rs | 29 +++++++++- src/rules/keyframes.rs | 5 +- src/selector.rs | 8 +++ src/stylesheet.rs | 42 ++++++++++++++ src/values/ident.rs | 4 +- 7 files changed, 210 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ca779f5a..4d0c9ead 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ mod tests { use crate::parser::ParserOptions; use crate::targets::Browsers; use indoc::indoc; + use std::collections::HashMap; fn test(source: &str, expected: &str) { let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); @@ -67,6 +68,27 @@ mod tests { assert_eq!(res, expected); } + fn css_modules_test(source: &str, expected: &str, expected_exports: HashMap) { + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); + stylesheet.minify(None); + let (res, _, exports) = stylesheet.to_css_module(false, false, None).unwrap(); + assert_eq!(res, expected); + assert_eq!(exports, expected_exports); + } + + macro_rules! map( + { $($key:expr => $value:expr),* } => { + { + #[allow(unused_mut)] + let mut m = HashMap::new(); + $( + m.insert($key.into(), $value.into()); + )* + m + } + }; + ); + #[test] pub fn test_border() { test(r#" @@ -7489,4 +7511,109 @@ mod tests { "#} ); } + + #[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! {}); + } } diff --git a/src/parser.rs b/src/parser.rs index f77e446c..8e4c0a4f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -70,7 +70,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. @@ -292,7 +292,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(); diff --git a/src/printer.rs b/src/printer.rs index 5a320f6c..2bd9cadd 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1,8 +1,9 @@ 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::stylesheet::CssModuleData; pub struct Printer<'a, W> { dest: &'a mut W, @@ -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.exports.insert(ident.into(), format!("{}_{}", ident, css_module.hash)); + } + + Ok(()) + } } impl<'a, W: Write + Sized> Write for Printer<'a, W> { diff --git a/src/rules/keyframes.rs b/src/rules/keyframes.rs index 5bea704a..fcbefc90 100644 --- a/src/rules/keyframes.rs +++ b/src/rules/keyframes.rs @@ -4,10 +4,11 @@ use crate::traits::{Parse, ToCss}; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::vendor_prefix::VendorPrefix; use crate::printer::Printer; +use crate::values::ident::CustomIdent; #[derive(Debug, PartialEq)] pub struct KeyframesRule { - pub name: String, + pub name: CustomIdent, pub keyframes: Vec, pub vendor_prefix: VendorPrefix, pub loc: SourceLocation @@ -40,7 +41,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(); diff --git a/src/selector.rs b/src/selector.rs index ff7dcc13..a47de0d4 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -865,6 +865,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..7dda6a9a 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -7,6 +7,18 @@ use crate::traits::ToCss; use crate::targets::Browsers; use crate::declaration::{DeclarationHandler, DeclarationBlock}; use crate::traits::Parse; +use std::collections::HashMap; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use data_encoding::Specification; + +macro_rules! hash { + ($str:expr) => {{ + let mut hasher = DefaultHasher::new(); + $str.hash(&mut hasher); + hasher.finish() + }}; +} pub use crate::parser::ParserOptions; @@ -60,6 +72,36 @@ impl StyleSheet { Ok((dest, source_map)) } + + pub fn to_css_module(&self, minify: bool, source_map: bool, targets: Option) -> Result<(String, Option, HashMap), std::fmt::Error> { + let mut dest = String::new(); + let mut source_map = if source_map { + let mut sm = SourceMap::new("/"); + sm.add_source(&self.filename); + Some(sm) + } else { + None + }; + + let encoder = { + let mut spec = Specification::new(); + spec.symbols.push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-"); + spec.encoding().unwrap() + }; + + let hash = encoder.encode(&(hash!(self.filename) as u32).to_le_bytes()); + + let mut exports = HashMap::new(); + let mut printer = Printer::new(&mut dest, source_map.as_mut(), minify, targets); + printer.css_module = Some(CssModuleData { + hash: &hash, + exports: &mut exports + }); + self.rules.to_css(&mut printer)?; + printer.newline()?; + + Ok((dest, source_map, exports)) + } } pub struct StyleAttribute { 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) } } From 441ce7cacf7be7ce5e51e1158ce9c05c77cf66c6 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sun, 26 Dec 2021 21:42:36 -0500 Subject: [PATCH 02/12] Refactor PropertyId and use for transitions --- Cargo.lock | 7 ++ Cargo.toml | 1 + src/properties/align.rs | 20 ++--- src/properties/animation.rs | 38 ++++----- src/properties/background.rs | 2 +- src/properties/border_image.rs | 4 +- src/properties/border_radius.rs | 10 +-- src/properties/custom.rs | 7 +- src/properties/flex.rs | 34 ++++---- src/properties/grid.rs | 2 +- src/properties/mod.rs | 138 ++++++++++++++++++++++++++++---- src/properties/text.rs | 16 ++-- src/properties/transform.rs | 4 +- src/properties/transition.rs | 22 ++--- src/stylesheet.rs | 5 ++ 15 files changed, 213 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b56c5fe2..f0c6e287 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,6 +280,7 @@ version = "1.0.0-alpha.7" dependencies = [ "bitflags", "cssparser", + "data-encoding", "indoc", "itertools", "parcel_selectors", diff --git a/Cargo.toml b/Cargo.toml index 0274dbeb..b4bef7dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ 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" [dev-dependencies] indoc = "1.0.3" 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/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..80629392 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -43,7 +43,7 @@ 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 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}; @@ -56,12 +56,41 @@ macro_rules! define_properties { $name: literal: $property: ident($type: ty $(, $vp: ty)?) $( / $prefix: tt )*, )+ ) => { - #[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 +99,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) } } } @@ -104,7 +217,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 +231,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)?)) } )* )+ @@ -132,12 +245,6 @@ macro_rules! define_properties { pub 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 +299,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 +597,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", 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/stylesheet.rs b/src/stylesheet.rs index 7dda6a9a..f5f8c8e9 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -140,3 +140,8 @@ impl StyleAttribute { Ok(dest) } } + +pub struct CssModuleData<'a> { + pub hash: &'a str, + pub exports: &'a mut HashMap +} From 073e0d2ee992f42adf9889fd29c572c29fbe61ff Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 27 Dec 2021 00:23:19 -0500 Subject: [PATCH 03/12] Handle :local(...) and :global(...) --- src/lib.rs | 28 ++++++++++++++++++++++++++++ src/selector.rs | 18 ++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4d0c9ead..00f1792d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7615,5 +7615,33 @@ mod tests { 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" + }); } } diff --git a/src/selector.rs b/src/selector.rs index a47de0d4..ccb588e7 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)] @@ -164,6 +164,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" => Local(Box::new(parcel_selectors::parser::Selector::parse(self, parser)?)), + "global" => 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 +299,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) } @@ -314,7 +320,7 @@ 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)) + unreachable!() } } @@ -420,6 +426,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, None), // TODO: context + Global(selector) => { + let css_module = std::mem::take(&mut dest.css_module); + selector.to_css_with_context(dest, None)?; + dest.css_module = css_module; + Ok(()) + }, + Lang(_) | Dir(_) => unreachable!(), Custom(val) => { dest.write_char(':')?; From b6028ba7170b3fc840858ef0eb76af3d5e967747 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 27 Dec 2021 14:51:30 -0500 Subject: [PATCH 04/12] Handle composes property --- Cargo.lock | 1 + Cargo.toml | 1 + src/css_modules.rs | 93 ++++++++++++++++++++++ src/lib.rs | 141 +++++++++++++++++++++++++++++++++- src/printer.rs | 6 +- src/properties/css_modules.rs | 55 +++++++++++++ src/properties/mod.rs | 5 ++ src/rules/style.rs | 67 ++++++++++------ src/selector.rs | 2 +- src/stylesheet.rs | 34 ++------ 10 files changed, 345 insertions(+), 60 deletions(-) create mode 100644 src/css_modules.rs create mode 100644 src/properties/css_modules.rs diff --git a/Cargo.lock b/Cargo.lock index f0c6e287..c846def9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,6 +283,7 @@ dependencies = [ "data-encoding", "indoc", "itertools", + "lazy_static", "parcel_selectors", "parcel_sourcemap", "serde", diff --git a/Cargo.toml b/Cargo.toml index b4bef7dc..21055e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ 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/src/css_modules.rs b/src/css_modules.rs new file mode 100644 index 00000000..d0e62d3d --- /dev/null +++ b/src/css_modules.rs @@ -0,0 +1,93 @@ +use std::collections::{HashMap, HashSet}; +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; + +lazy_static! { + static ref ENCODER: Encoding = { + let mut spec = Specification::new(); + spec.symbols.push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-"); + spec.encoding().unwrap() + }; +} + +pub struct CssModule<'a> { + pub hash: &'a str, + pub exports: &'a mut HashMap> +} + +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) => { + entry.get_mut().insert(export); + } + std::collections::hash_map::Entry::Vacant(entry) => { + let mut set = HashSet::new(); + set.insert(export); + entry.insert(set); + } + } + } + + 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) => { + match &composes.from { + None => self.add_local(&id.0, &composes.name.0), + Some(ComposesFrom::Global) => self.add_global(&id.0, &composes.name.0), + Some(ComposesFrom::File(file)) => self.add_dependency(&id.0, &composes.name.0, &file) + } + continue; + } + _ => {} + } + } + + // The composes property can only be used within a simple class selector. + return Err(()) // TODO: custom error + } + + Ok(()) + } +} + +#[derive(PartialEq, Eq, Hash, Debug, Clone)] +pub enum CssModuleExport { + Local(String), + Dependency { + name: String, + specifier: String + } +} + +pub 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/lib.rs b/src/lib.rs index 00f1792d..8b50801a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod compat; mod prefixes; pub mod vendor_prefix; pub mod targets; +mod css_modules; #[cfg(test)] mod tests { @@ -20,7 +21,8 @@ mod tests { use crate::parser::ParserOptions; use crate::targets::Browsers; use indoc::indoc; - use std::collections::HashMap; + use std::collections::{HashMap, HashSet}; + use crate::css_modules::CssModuleExport; fn test(source: &str, expected: &str) { let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); @@ -68,7 +70,7 @@ mod tests { assert_eq!(res, expected); } - fn css_modules_test(source: &str, expected: &str, expected_exports: HashMap) { + fn css_modules_test(source: &str, expected: &str, expected_exports: HashMap>) { let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); stylesheet.minify(None); let (res, _, exports) = stylesheet.to_css_module(false, false, None).unwrap(); @@ -77,12 +79,27 @@ mod tests { } macro_rules! map( - { $($key:expr => $value:expr),* } => { + { $($key:expr => $($value:literal $(from $from:literal)?)+),* } => { { #[allow(unused_mut)] let mut m = HashMap::new(); $( - m.insert($key.into(), $value.into()); + let mut v = HashSet::new(); + macro_rules! insert { + ($local:literal, $specifier:literal) => { + v.insert(CssModuleExport::Dependency { + name: $local.into(), + specifier: $specifier.into() + }); + }; + ($local:literal) => { + v.insert(CssModuleExport::Local($local.into())); + }; + } + $( + insert!($value$(, $from)?); + )+ + m.insert($key.into(), v); )* m } @@ -7510,6 +7527,21 @@ mod tests { } "#} ); + + nesting_test_no_targets( + r#" + .error, .invalid { + &:hover > .baz { color: red; } + } + "#, + indoc!{r#" + .error, .invalid { + &:hover > .baz { + color: red; + } + } + "#} + ); } #[test] @@ -7643,5 +7675,106 @@ mod tests { "#}, 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 from global; + background: white; + } + "#, indoc!{r#" + .test_EgL3uq { + background: #fff; + } + "#}, map! { + "test" => "test_EgL3uq" "foo" + }); + + 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; + 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/printer.rs b/src/printer.rs index 2bd9cadd..f36bd45f 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -3,7 +3,7 @@ use cssparser::{SourceLocation, serialize_identifier}; use parcel_sourcemap::{SourceMap, OriginalLocation}; use crate::vendor_prefix::VendorPrefix; use crate::targets::Browsers; -use crate::stylesheet::CssModuleData; +use crate::css_modules::CssModule; pub struct Printer<'a, W> { dest: &'a mut W, @@ -17,7 +17,7 @@ pub struct Printer<'a, W> { /// the vendor prefix of whatever is being printed. pub vendor_prefix: VendorPrefix, pub in_calc: bool, - pub css_module: Option> + pub css_module: Option> } impl<'a, W: Write + Sized> Printer<'a, W> { @@ -126,7 +126,7 @@ impl<'a, W: Write + Sized> Printer<'a, W> { } if let Some(css_module) = &mut self.css_module { - css_module.exports.insert(ident.into(), format!("{}_{}", ident, css_module.hash)); + css_module.add_local(&ident, &ident); } Ok(()) diff --git a/src/properties/css_modules.rs b/src/properties/css_modules.rs new file mode 100644 index 00000000..8e526bd0 --- /dev/null +++ b/src/properties/css_modules.rs @@ -0,0 +1,55 @@ +use cssparser::*; +use crate::values::ident::CustomIdent; +use crate::traits::{Parse, ToCss}; +use crate::printer::Printer; + +/// The `composes` property from CSS modules. +/// https://github.com/css-modules/css-modules/#dependencies +#[derive(Debug, Clone, PartialEq)] +pub struct Composes { + pub name: CustomIdent, + 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 name = CustomIdent::parse(input)?; + + 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 { + name, + from + }) + } +} + +impl ToCss for Composes { + fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { + self.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/mod.rs b/src/properties/mod.rs index 80629392..5e9d7470 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,6 +44,7 @@ use ui::*; use list::*; #[cfg(feature = "grid")] use grid::*; +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; @@ -670,6 +672,9 @@ define_properties! { "list-style-position": ListStylePosition(ListStylePosition), "list-style": ListStyle(ListStyle), "marker-side": MarkerSide(MarkerSide), + + // CSS modules + "composes": Composes(Composes), } impl, V: Parse> Parse for SmallVec { 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 ccb588e7..6c8577c6 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -319,7 +319,7 @@ 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 { + fn to_css(&self, _: &mut W) -> fmt::Result where W: fmt::Write { unreachable!() } } diff --git a/src/stylesheet.rs b/src/stylesheet.rs index f5f8c8e9..af0a4eae 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -7,18 +7,8 @@ use crate::traits::ToCss; use crate::targets::Browsers; use crate::declaration::{DeclarationHandler, DeclarationBlock}; use crate::traits::Parse; -use std::collections::HashMap; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; -use data_encoding::Specification; - -macro_rules! hash { - ($str:expr) => {{ - let mut hasher = DefaultHasher::new(); - $str.hash(&mut hasher); - hasher.finish() - }}; -} +use crate::css_modules::{hash, CssModule, CssModuleExport}; +use std::collections::{HashMap, HashSet}; pub use crate::parser::ParserOptions; @@ -73,7 +63,7 @@ impl StyleSheet { Ok((dest, source_map)) } - pub fn to_css_module(&self, minify: bool, source_map: bool, targets: Option) -> Result<(String, Option, HashMap), std::fmt::Error> { + pub fn to_css_module(&self, minify: bool, source_map: bool, targets: Option) -> Result<(String, Option, HashMap>), std::fmt::Error> { let mut dest = String::new(); let mut source_map = if source_map { let mut sm = SourceMap::new("/"); @@ -83,18 +73,11 @@ impl StyleSheet { None }; - let encoder = { - let mut spec = Specification::new(); - spec.symbols.push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-"); - spec.encoding().unwrap() - }; - - let hash = encoder.encode(&(hash!(self.filename) as u32).to_le_bytes()); - + let h = hash(&self.filename); let mut exports = HashMap::new(); let mut printer = Printer::new(&mut dest, source_map.as_mut(), minify, targets); - printer.css_module = Some(CssModuleData { - hash: &hash, + printer.css_module = Some(CssModule { + hash: &h, exports: &mut exports }); self.rules.to_css(&mut printer)?; @@ -140,8 +123,3 @@ impl StyleAttribute { Ok(dest) } } - -pub struct CssModuleData<'a> { - pub hash: &'a str, - pub exports: &'a mut HashMap -} From 5a63181c431172a171a2cb1bb7cf19232cb39654 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 27 Dec 2021 18:12:23 -0500 Subject: [PATCH 05/12] Move CSS modules parsing behind an option --- src/declaration.rs | 23 ++++++++++++---------- src/lib.rs | 6 +++--- src/parser.rs | 44 +++++++++++++++++++++++++----------------- src/properties/mod.rs | 9 +++++---- src/rules/keyframes.rs | 5 ++++- src/selector.rs | 7 ++++--- src/stylesheet.rs | 4 ++-- 7 files changed, 57 insertions(+), 41 deletions(-) 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 8b50801a..bd1644ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,21 +57,21 @@ 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); } 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); } fn css_modules_test(source: &str, expected: &str, expected_exports: HashMap>) { - let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { css_modules: true, ..ParserOptions::default() }).unwrap(); stylesheet.minify(None); let (res, _, exports) = stylesheet.to_css_module(false, false, None).unwrap(); assert_eq!(res, expected); diff --git a/src/parser.rs b/src/parser.rs index 8e4c0a4f..9652501c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -25,7 +25,8 @@ 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. @@ -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/properties/mod.rs b/src/properties/mod.rs index 5e9d7470..97bf5113 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -50,12 +50,13 @@ 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, PartialEq)] @@ -202,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)?)) @@ -674,7 +675,7 @@ define_properties! { "marker-side": MarkerSide(MarkerSide), // CSS modules - "composes": Composes(Composes), + "composes": Composes(Composes) if css_modules, } impl, V: Parse> Parse for SmallVec { diff --git a/src/rules/keyframes.rs b/src/rules/keyframes.rs index fcbefc90..200ffa43 100644 --- a/src/rules/keyframes.rs +++ b/src/rules/keyframes.rs @@ -5,6 +5,7 @@ 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 { @@ -165,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/selector.rs b/src/selector.rs index 6c8577c6..76f5cda9 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -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,8 +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" => Local(Box::new(parcel_selectors::parser::Selector::parse(self, parser)?)), - "global" => Global(Box::new(parcel_selectors::parser::Selector::parse(self, parser)?)), + "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()))), }; diff --git a/src/stylesheet.rs b/src/stylesheet.rs index af0a4eae..07978a1e 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -6,7 +6,6 @@ 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, CssModuleExport}; use std::collections::{HashMap, HashSet}; @@ -95,8 +94,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)? }) } From 8726d9810b7fb13efd3eb396163ffe286e1d18cf Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 27 Dec 2021 19:00:08 -0500 Subject: [PATCH 06/12] Improve API and add to node binding --- node/src/lib.rs | 29 ++++++++++-------- src/css_modules.rs | 28 +++++++++-------- src/lib.rs | 29 +++++++++--------- src/parser.rs | 14 ++++----- src/stylesheet.rs | 76 +++++++++++++++++++++++++++------------------- test.js | 30 ++++++++---------- 6 files changed, 112 insertions(+), 94 deletions(-) diff --git a/node/src/lib.rs b/node/src/lib.rs index 67809ab8..ae09ee70 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; // --------------------------------------------- @@ -54,7 +55,8 @@ struct TransformResult { #[serde(with = "serde_bytes")] code: Vec, #[serde(with = "serde_bytes")] - map: Option> + map: Option>, + css_module_exports: Option } #[cfg(not(target_arch = "wasm32"))] @@ -104,7 +106,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 +121,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 +150,9 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result>; lazy_static! { static ref ENCODER: Encoding = { @@ -15,9 +28,9 @@ lazy_static! { }; } -pub struct CssModule<'a> { +pub(crate) struct CssModule<'a> { pub hash: &'a str, - pub exports: &'a mut HashMap> + pub exports: &'a mut CssModuleExports } impl<'a> CssModule<'a> { @@ -75,16 +88,7 @@ impl<'a> CssModule<'a> { } } -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub enum CssModuleExport { - Local(String), - Dependency { - name: String, - specifier: String - } -} - -pub fn hash(s: &str) -> String { +pub(crate) fn hash(s: &str) -> String { let mut hasher = DefaultHasher::new(); s.hash(&mut hasher); let hash = hasher.finish() as u32; diff --git a/src/lib.rs b/src/lib.rs index bd1644ae..2ae58750 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,12 +13,11 @@ mod compat; mod prefixes; pub mod vendor_prefix; pub mod targets; -mod css_modules; +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, HashSet}; @@ -27,22 +26,22 @@ mod tests { 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) { @@ -59,23 +58,23 @@ mod tests { }); 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, ..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: HashMap>) { let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { css_modules: true, ..ParserOptions::default() }).unwrap(); stylesheet.minify(None); - let (res, _, exports) = stylesheet.to_css_module(false, false, None).unwrap(); - assert_eq!(res, expected); - assert_eq!(exports, expected_exports); + let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + assert_eq!(res.code, expected); + assert_eq!(res.css_module_exports.unwrap(), expected_exports); } macro_rules! map( diff --git a/src/parser.rs b/src/parser.rs index 9652501c..41e01c71 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -30,14 +30,14 @@ pub struct ParserOptions { } /// 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(), @@ -45,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, @@ -86,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 = (); @@ -176,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 = (); diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 07978a1e..5e68787c 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -6,21 +6,35 @@ use crate::printer::Printer; use crate::traits::ToCss; use crate::targets::Browsers; use crate::declaration::{DeclarationHandler, DeclarationBlock}; -use crate::css_modules::{hash, CssModule, CssModuleExport}; +use crate::css_modules::{hash, CssModule, CssModuleExports}; use std::collections::{HashMap, HashSet}; 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 css_module_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 { @@ -35,7 +49,8 @@ impl StyleSheet { Ok(StyleSheet { filename, - rules: CssRuleList(rules) + rules: CssRuleList(rules), + options }) } @@ -45,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) @@ -55,34 +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 + }); - pub fn to_css_module(&self, minify: bool, source_map: bool, targets: Option) -> Result<(String, Option, HashMap>), std::fmt::Error> { - let mut dest = String::new(); - let mut source_map = if source_map { - let mut sm = SourceMap::new("/"); - sm.add_source(&self.filename); - Some(sm) - } else { - None - }; + self.rules.to_css(&mut printer)?; + printer.newline()?; - let h = hash(&self.filename); - let mut exports = HashMap::new(); - let mut printer = Printer::new(&mut dest, source_map.as_mut(), minify, targets); - printer.css_module = Some(CssModule { - hash: &h, - exports: &mut exports - }); - self.rules.to_css(&mut printer)?; - printer.newline()?; - - Ok((dest, source_map, exports)) + Ok(ToCssResult { + code: dest, + source_map, + css_module_exports: Some(exports) + }) + } else { + self.rules.to_css(&mut printer)?; + printer.newline()?; + Ok(ToCssResult { + code: dest, + source_map, + css_module_exports: None + }) + } } } diff --git a/test.js b/test.js index adfe0ad3..da20d4f6 100644 --- a/test.js +++ b/test.js @@ -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 + }, + css_modules: true +}); console.log(res.code.toString()); +console.log(res.css_module_exports); From e95263ee63cfd279104cf8d28f53483533017c01 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 27 Dec 2021 19:08:18 -0500 Subject: [PATCH 07/12] Preserve order of names --- src/css_modules.rs | 12 ++++++------ src/lib.rs | 12 ++++++------ src/printer.rs | 2 +- src/properties/mod.rs | 2 +- src/stylesheet.rs | 2 +- src/traits.rs | 2 +- src/values/length.rs | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/css_modules.rs b/src/css_modules.rs index 7b120a18..426b6d76 100644 --- a/src/css_modules.rs +++ b/src/css_modules.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use data_encoding::{Specification, Encoding}; @@ -18,7 +18,7 @@ pub enum CssModuleExport { } } -pub type CssModuleExports = HashMap>; +pub type CssModuleExports = HashMap>; lazy_static! { static ref ENCODER: Encoding = { @@ -37,12 +37,12 @@ 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) => { - entry.get_mut().insert(export); + entry.get_mut().push(export); } std::collections::hash_map::Entry::Vacant(entry) => { - let mut set = HashSet::new(); - set.insert(export); - entry.insert(set); + let mut items = Vec::new(); + items.push(export); + entry.insert(items); } } } diff --git a/src/lib.rs b/src/lib.rs index 2ae58750..2f76b186 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,8 +20,8 @@ mod tests { use crate::stylesheet::*; use crate::targets::Browsers; use indoc::indoc; - use std::collections::{HashMap, HashSet}; - use crate::css_modules::CssModuleExport; + 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(); @@ -69,7 +69,7 @@ mod tests { assert_eq!(res.code, expected); } - fn css_modules_test(source: &str, expected: &str, expected_exports: HashMap>) { + 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(); @@ -83,16 +83,16 @@ mod tests { #[allow(unused_mut)] let mut m = HashMap::new(); $( - let mut v = HashSet::new(); + let mut v = Vec::new(); macro_rules! insert { ($local:literal, $specifier:literal) => { - v.insert(CssModuleExport::Dependency { + v.push(CssModuleExport::Dependency { name: $local.into(), specifier: $specifier.into() }); }; ($local:literal) => { - v.insert(CssModuleExport::Local($local.into())); + v.push(CssModuleExport::Local($local.into())); }; } $( diff --git a/src/printer.rs b/src/printer.rs index f36bd45f..feb25bea 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -5,7 +5,7 @@ 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, diff --git a/src/properties/mod.rs b/src/properties/mod.rs index 97bf5113..1fc509ca 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -245,7 +245,7 @@ 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::*; let mut first = true; diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 5e68787c..206d05f3 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -7,7 +7,7 @@ use crate::traits::ToCss; use crate::targets::Browsers; use crate::declaration::{DeclarationHandler, DeclarationBlock}; use crate::css_modules::{hash, CssModule, CssModuleExports}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; pub use crate::parser::ParserOptions; 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/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) From d76d02841d870ce14b95b27b56748a404ff7cfa9 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 27 Dec 2021 22:10:08 -0500 Subject: [PATCH 08/12] Fixes --- src/css_modules.rs | 8 ++++++-- src/selector.rs | 10 +++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/css_modules.rs b/src/css_modules.rs index 426b6d76..b7c839ba 100644 --- a/src/css_modules.rs +++ b/src/css_modules.rs @@ -37,11 +37,15 @@ 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) => { - entry.get_mut().push(export); + if !entry.get().contains(&export) { + entry.get_mut().push(export); + } } std::collections::hash_map::Entry::Vacant(entry) => { let mut items = Vec::new(); - items.push(export); + if !items.contains(&export) { + items.push(export); + } entry.insert(items); } } diff --git a/src/selector.rs b/src/selector.rs index 76f5cda9..11dc5af1 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -325,8 +325,8 @@ impl cssparser::ToCss for PseudoClass { } } -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) => { @@ -427,10 +427,10 @@ 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, None), // TODO: context + 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, None)?; + selector.to_css_with_context(dest, context)?; dest.css_module = css_module; Ok(()) }, @@ -872,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) From 39f5f5ff19dc45b33f55ae94e5ec305cc1713a5f Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 27 Dec 2021 23:16:07 -0500 Subject: [PATCH 09/12] Support composing multiple names at once --- src/css_modules.rs | 10 +++--- src/lib.rs | 57 +++++++++++++++++++++++++++++++++++ src/properties/css_modules.rs | 34 ++++++++++++++++++--- 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/src/css_modules.rs b/src/css_modules.rs index b7c839ba..9f3661b8 100644 --- a/src/css_modules.rs +++ b/src/css_modules.rs @@ -73,10 +73,12 @@ impl<'a> CssModule<'a> { if sel.len() == 1 { match sel.iter_raw_match_order().next().unwrap() { parcel_selectors::parser::Component::Class(ref id) => { - match &composes.from { - None => self.add_local(&id.0, &composes.name.0), - Some(ComposesFrom::Global) => self.add_global(&id.0, &composes.name.0), - Some(ComposesFrom::File(file)) => self.add_dependency(&id.0, &composes.name.0, &file) + 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; } diff --git a/src/lib.rs b/src/lib.rs index 2f76b186..014bcea9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7726,6 +7726,37 @@ mod tests { "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; @@ -7739,6 +7770,19 @@ mod tests { "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"; @@ -7752,6 +7796,19 @@ mod tests { "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; diff --git a/src/properties/css_modules.rs b/src/properties/css_modules.rs index 8e526bd0..4c2c6b00 100644 --- a/src/properties/css_modules.rs +++ b/src/properties/css_modules.rs @@ -2,12 +2,13 @@ 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 name: CustomIdent, + pub names: SmallVec<[CustomIdent; 1]>, pub from: Option } @@ -19,7 +20,14 @@ pub enum ComposesFrom { impl Parse for Composes { fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { - let name = CustomIdent::parse(input)?; + 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())) { @@ -33,15 +41,33 @@ impl Parse for Composes { }; Ok(Composes { - name, + 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 { - self.name.to_css(dest)?; + 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 { From dbff3175e547823865a778e797ecc1078fd1cd47 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Tue, 28 Dec 2021 00:22:42 -0500 Subject: [PATCH 10/12] Add to playground --- README.md | 2 +- node/src/lib.rs | 6 ++++-- playground/index.html | 5 +++++ playground/playground.js | 9 ++++++++- src/lib.rs | 2 +- src/stylesheet.rs | 6 +++--- test.js | 6 +++--- 7 files changed, 25 insertions(+), 11 deletions(-) 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/src/lib.rs b/node/src/lib.rs index ae09ee70..f419510b 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -51,12 +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>, - css_module_exports: Option + exports: Option } #[cfg(not(target_arch = "wasm32"))] @@ -99,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")] @@ -152,7 +154,7 @@ 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/lib.rs b/src/lib.rs index 014bcea9..64954959 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ mod tests { stylesheet.minify(None); let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); assert_eq!(res.code, expected); - assert_eq!(res.css_module_exports.unwrap(), expected_exports); + assert_eq!(res.exports.unwrap(), expected_exports); } macro_rules! map( diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 206d05f3..e1a67517 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -27,7 +27,7 @@ pub struct PrinterOptions { pub struct ToCssResult { pub code: String, pub source_map: Option, - pub css_module_exports: Option + pub exports: Option } impl StyleSheet { @@ -86,7 +86,7 @@ impl StyleSheet { Ok(ToCssResult { code: dest, source_map, - css_module_exports: Some(exports) + exports: Some(exports) }) } else { self.rules.to_css(&mut printer)?; @@ -94,7 +94,7 @@ impl StyleSheet { Ok(ToCssResult { code: dest, source_map, - css_module_exports: None + exports: None }) } } diff --git a/test.js b/test.js index da20d4f6..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 } @@ -51,8 +51,8 @@ let res = css.transform({ drafts: { nesting: true }, - css_modules: true + cssModules: true }); console.log(res.code.toString()); -console.log(res.css_module_exports); +console.log(res.exports); From e6ea553d475066014eba6be1300ac45bb609ebfa Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Tue, 28 Dec 2021 10:17:32 -0500 Subject: [PATCH 11/12] Update TS --- node/index.d.ts | 33 ++++++++++++++++++++++++++++++--- node/src/lib.rs | 4 ++-- playground/index.html | 2 +- playground/playground.js | 8 ++++---- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/node/index.d.ts b/node/index.d.ts index 0c6c8564..54f555b4 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. */ + module?: 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 f419510b..0cf86606 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -109,7 +109,7 @@ struct Config { pub minify: Option, pub source_map: Option, pub drafts: Option, - pub css_modules: Option + pub module: Option } #[derive(Serialize, Debug, Deserialize, Default)] @@ -124,7 +124,7 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result o.nesting, None => false }, - css_modules: config.css_modules.unwrap_or(false) + css_modules: config.module.unwrap_or(false) })?; stylesheet.minify(config.targets); // TODO: should this be conditional? let res = stylesheet.to_css(PrinterOptions { diff --git a/playground/index.html b/playground/index.html index 44d631e2..d855ad20 100644 --- a/playground/index.html +++ b/playground/index.html @@ -61,7 +61,7 @@

Parcel CSS Playground

Options

- +

Draft syntax

Targets

diff --git a/playground/playground.js b/playground/playground.js index b8690e1b..229a383a 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -26,8 +26,8 @@ function reflectPlaygroundState(playgroundState) { minify.checked = playgroundState.minify; } - if (typeof playgroundState.cssModules !== 'undefined') { - cssModules.checked = playgroundState.cssModules; + if (typeof playgroundState.module !== 'undefined') { + module.checked = playgroundState.module; } if (typeof playgroundState.nesting !== 'undefined') { @@ -51,7 +51,7 @@ function savePlaygroundState() { const playgroundState = { minify: minify.checked, nesting: nesting.checked, - cssModules: cssModules.checked, + module: module.checked, targets: getTargets(), source: source.value, }; @@ -94,7 +94,7 @@ async function update() { drafts: { nesting: nesting.checked }, - cssModules: cssModules.checked + module: module.checked }); compiled.value = dec.decode(res.code); From c8ca240e18728765be41b4e54cd49a8ede7f9d3a Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Tue, 28 Dec 2021 12:49:36 -0500 Subject: [PATCH 12/12] Fixes --- node/index.d.ts | 4 ++-- node/src/lib.rs | 4 ++-- playground/index.html | 2 +- playground/playground.js | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/node/index.d.ts b/node/index.d.ts index 54f555b4..993e62b7 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -14,7 +14,7 @@ export interface TransformOptions { /** Whether to enable various draft syntax. */ drafts?: Drafts, /** Whether to compile this file as a CSS module. */ - module?: boolean + cssModules?: boolean } export interface Drafts { @@ -33,7 +33,7 @@ export interface TransformResult { export type CSSModuleExports = { /** Maps exported (i.e. original) names to local names. */ - [name: string]: CSSModuleExport + [name: string]: CSSModuleExport[] }; export type CSSModuleExport = LocalCSSModuleExport | DependencyCSSModuleExport; diff --git a/node/src/lib.rs b/node/src/lib.rs index 0cf86606..f419510b 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -109,7 +109,7 @@ struct Config { pub minify: Option, pub source_map: Option, pub drafts: Option, - pub module: Option + pub css_modules: Option } #[derive(Serialize, Debug, Deserialize, Default)] @@ -124,7 +124,7 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result o.nesting, None => false }, - css_modules: config.module.unwrap_or(false) + css_modules: config.css_modules.unwrap_or(false) })?; stylesheet.minify(config.targets); // TODO: should this be conditional? let res = stylesheet.to_css(PrinterOptions { diff --git a/playground/index.html b/playground/index.html index d855ad20..44d631e2 100644 --- a/playground/index.html +++ b/playground/index.html @@ -61,7 +61,7 @@

Parcel CSS Playground

Options

- +

Draft syntax

Targets

diff --git a/playground/playground.js b/playground/playground.js index 229a383a..b8690e1b 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -26,8 +26,8 @@ function reflectPlaygroundState(playgroundState) { minify.checked = playgroundState.minify; } - if (typeof playgroundState.module !== 'undefined') { - module.checked = playgroundState.module; + if (typeof playgroundState.cssModules !== 'undefined') { + cssModules.checked = playgroundState.cssModules; } if (typeof playgroundState.nesting !== 'undefined') { @@ -51,7 +51,7 @@ function savePlaygroundState() { const playgroundState = { minify: minify.checked, nesting: nesting.checked, - module: module.checked, + cssModules: cssModules.checked, targets: getTargets(), source: source.value, }; @@ -94,7 +94,7 @@ async function update() { drafts: { nesting: nesting.checked }, - module: module.checked + cssModules: cssModules.checked }); compiled.value = dec.decode(res.code);