From ff38a3586e229dbf34f367eed3fd3637a19e6f54 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 11 Jan 2023 21:29:18 -0500 Subject: [PATCH 01/12] Support standalone selector and rule parsing and printing --- node/ast.d.ts | 12 +- selectors/parser.rs | 130 +++--- selectors/serialization.rs | 17 +- src/declaration.rs | 2 +- src/lib.rs | 2 +- src/parser.rs | 2 - src/printer.rs | 28 +- src/properties/custom.rs | 18 +- src/properties/mod.rs | 4 +- src/rules/container.rs | 11 +- src/rules/layer.rs | 12 +- src/rules/media.rs | 13 +- src/rules/mod.rs | 97 ++--- src/rules/nesting.rs | 13 +- src/rules/style.rs | 30 +- src/rules/supports.rs | 11 +- src/selector.rs | 785 +++++++++++++++++++------------------ 17 files changed, 577 insertions(+), 610 deletions(-) diff --git a/node/ast.d.ts b/node/ast.d.ts index b6b4be69..871addbd 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -6039,22 +6039,16 @@ export type SelectorComponent = | ( | { type: "namespace"; - value: "none"; + kind: "none"; } | { type: "namespace"; - value: "any"; - } - | { - type: "namespace"; - url: string; - value: "default"; + kind: "any"; } | { type: "namespace"; + kind: "named"; prefix: string; - url: string; - value: "some"; } ) | { diff --git a/selectors/parser.rs b/selectors/parser.rs index 5796a070..9b66523b 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -2009,21 +2009,19 @@ where }), QNamePrefix::ExplicitNoNamespace => sink.push(Component::ExplicitNoNamespace), QNamePrefix::ExplicitAnyNamespace => { - match parser.default_namespace() { - // Element type selectors that have no namespace - // component (no namespace separator) represent elements - // without regard to the element's namespace (equivalent - // to "*|") unless a default namespace has been declared - // for namespaced selectors (e.g. in CSS, in the style - // sheet). If a default namespace has been declared, - // such selectors will represent only elements in the - // default namespace. - // -- Selectors § 6.1.1 - // So we'll have this act the same as the - // QNamePrefix::ImplicitAnyNamespace case. - None => {} - Some(_) => sink.push(Component::ExplicitAnyNamespace), - } + // Element type selectors that have no namespace + // component (no namespace separator) represent elements + // without regard to the element's namespace (equivalent + // to "*|") unless a default namespace has been declared + // for namespaced selectors (e.g. in CSS, in the style + // sheet). If a default namespace has been declared, + // such selectors will represent only elements in the + // default namespace. + // -- Selectors § 6.1.1 + // So we'll have this act the same as the + // QNamePrefix::ImplicitAnyNamespace case. + // For lightning css this logic was removed, should be handled when matching. + sink.push(Component::ExplicitAnyNamespace) } QNamePrefix::ImplicitNoNamespace => { unreachable!() // Not returned with in_attr_selector = false @@ -3117,38 +3115,38 @@ pub mod tests { ); // When the default namespace is not set, *| should be elided. // https://github.com/servo/servo/pull/17537 - assert_eq!( - parse_expected("*|e", Some("e")), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![Component::LocalName(LocalName { - name: DummyAtom::from("e"), - lower_name: DummyAtom::from("e"), - })], - specificity(0, 0, 1), - Default::default(), - )])) - ); + // assert_eq!( + // parse_expected("*|e", Some("e")), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![Component::LocalName(LocalName { + // name: DummyAtom::from("e"), + // lower_name: DummyAtom::from("e"), + // })], + // specificity(0, 0, 1), + // Default::default(), + // )])) + // ); // When the default namespace is set, *| should _not_ be elided (as foo // is no longer equivalent to *|foo--the former is only for foo in the // default namespace). // https://github.com/servo/servo/issues/16020 - assert_eq!( - parse_ns( - "*|e", - &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) - ), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![ - Component::ExplicitAnyNamespace, - Component::LocalName(LocalName { - name: DummyAtom::from("e"), - lower_name: DummyAtom::from("e"), - }), - ], - specificity(0, 0, 1), - Default::default(), - )])) - ); + // assert_eq!( + // parse_ns( + // "*|e", + // &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) + // ), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![ + // Component::ExplicitAnyNamespace, + // Component::LocalName(LocalName { + // name: DummyAtom::from("e"), + // lower_name: DummyAtom::from("e"), + // }), + // ], + // specificity(0, 0, 1), + // Default::default(), + // )])) + // ); assert_eq!( parse("*"), Ok(SelectorList::from_vec(vec![Selector::from_vec( @@ -3165,14 +3163,14 @@ pub mod tests { Default::default(), )])) ); - assert_eq!( - parse_expected("*|*", Some("*")), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![Component::ExplicitUniversalType], - specificity(0, 0, 0), - Default::default(), - )])) - ); + // assert_eq!( + // parse_expected("*|*", Some("*")), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![Component::ExplicitUniversalType], + // specificity(0, 0, 0), + // Default::default(), + // )])) + // ); assert_eq!( parse_ns( "*|*", @@ -3517,21 +3515,21 @@ pub mod tests { ); // *| should be elided if there is no default namespace. // https://github.com/servo/servo/pull/17537 - assert_eq!( - parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![Component::Negation( - vec![Selector::from_vec( - vec![Component::ExplicitUniversalType], - specificity(0, 0, 0), - Default::default() - )] - .into_boxed_slice() - )], - specificity(0, 0, 0), - Default::default(), - )])) - ); + // assert_eq!( + // parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![Component::Negation( + // vec![Selector::from_vec( + // vec![Component::ExplicitUniversalType], + // specificity(0, 0, 0), + // Default::default() + // )] + // .into_boxed_slice() + // )], + // specificity(0, 0, 0), + // Default::default(), + // )])) + // ); assert!(parse("::slotted()").is_err()); assert!(parse("::slotted(div)").is_ok()); diff --git a/selectors/serialization.rs b/selectors/serialization.rs index 0696d68b..dff9ceff 100644 --- a/selectors/serialization.rs +++ b/selectors/serialization.rs @@ -9,6 +9,7 @@ use crate::{ }; use std::borrow::Cow; +use cssparser::CowRcStr; #[cfg(feature = "jsonschema")] use schemars::JsonSchema; @@ -61,12 +62,11 @@ enum SerializedComponent<'i, 's, Impl: SelectorImpl<'s>, PseudoClass, PseudoElem #[derive(serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] -#[serde(tag = "value", rename_all = "kebab-case")] +#[serde(tag = "kind", rename_all = "kebab-case")] enum Namespace<'i> { None, Any, - Default { url: Cow<'i, str> }, - Some { prefix: Cow<'i, str>, url: Cow<'i, str> }, + Named { prefix: Cow<'i, str> }, } #[derive(serde::Serialize, serde::Deserialize)] @@ -288,12 +288,10 @@ where Component::ExplicitUniversalType => SerializedComponent::Universal, Component::ExplicitAnyNamespace => SerializedComponent::Namespace(Namespace::Any), Component::ExplicitNoNamespace => SerializedComponent::Namespace(Namespace::None), - Component::DefaultNamespace(url) => SerializedComponent::Namespace(Namespace::Default { - url: url.as_ref().into(), - }), - Component::Namespace(prefix, url) => SerializedComponent::Namespace(Namespace::Some { + // can't actually happen anymore. + Component::DefaultNamespace(_url) => SerializedComponent::Namespace(Namespace::Any), + Component::Namespace(prefix, _url) => SerializedComponent::Namespace(Namespace::Named { prefix: prefix.as_ref().into(), - url: url.as_ref().into(), }), Component::LocalName(name) => SerializedComponent::Type { name: name.name.as_ref().into(), @@ -440,8 +438,7 @@ where SerializedComponent::Namespace(n) => match n { Namespace::Any => Component::ExplicitAnyNamespace, Namespace::None => Component::ExplicitNoNamespace, - Namespace::Default { url } => Component::DefaultNamespace(url.into()), - Namespace::Some { prefix, url } => Component::Namespace(prefix.into(), url.into()), + Namespace::Named { prefix } => Component::Namespace(prefix.into(), CowRcStr::from("").into()), }, SerializedComponent::Type { name } => { let name: Impl::LocalName = name.into(); diff --git a/src/declaration.rs b/src/declaration.rs index c596c443..a9afde50 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -441,7 +441,7 @@ pub(crate) fn parse_declaration<'i, 't, T>( input: &mut cssparser::Parser<'i, 't>, declarations: &mut DeclarationList<'i>, important_declarations: &mut DeclarationList<'i>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result<(), cssparser::ParseError<'i, ParserError<'i>>> { let property = input.parse_until_before(Delimiter::Bang, |input| { Property::parse(PropertyId::from(CowArcStr::from(name)), input, options) diff --git a/src/lib.rs b/src/lib.rs index a18f00a4..4531d5d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5640,7 +5640,7 @@ mod tests { minify_test("a:is(.foo) { color: yellow }", "a.foo{color:#ff0}"); minify_test("a:is([data-test]) { color: yellow }", "a[data-test]{color:#ff0}"); minify_test(".foo:is(a) { color: yellow }", ".foo:is(a){color:#ff0}"); - minify_test(".foo:is(*|a) { color: yellow }", ".foo:is(a){color:#ff0}"); + minify_test(".foo:is(*|a) { color: yellow }", ".foo:is(*|a){color:#ff0}"); minify_test(".foo:is(*) { color: yellow }", ".foo:is(*){color:#ff0}"); minify_test( "@namespace svg url(http://www.w3.org/2000/svg); .foo:is(svg|a) { color: yellow }", diff --git a/src/parser.rs b/src/parser.rs index 8da21db3..0906beff 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1082,8 +1082,6 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for StyleRuleP input: &mut Parser<'i, 't>, ) -> Result> { let selector_parser = SelectorParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: true, options: &self.options, }; diff --git a/src/printer.rs b/src/printer.rs index 4dfce5de..b6b141d0 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -3,7 +3,8 @@ use crate::css_modules::CssModule; use crate::dependencies::{Dependency, DependencyOptions}; use crate::error::{Error, ErrorLocation, PrinterError, PrinterErrorKind}; -use crate::rules::Location; +use crate::rules::{Location, StyleContext}; +use crate::selector::SelectorList; use crate::targets::Browsers; use crate::vendor_prefix::VendorPrefix; use cssparser::{serialize_identifier, serialize_name}; @@ -82,6 +83,7 @@ pub struct Printer<'a, 'b, 'c, W> { pub(crate) dependencies: Option>, pub(crate) remove_imports: bool, pub(crate) pseudo_classes: Option>, + context: Option<&'a StyleContext<'a, 'b>>, } impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { @@ -114,6 +116,7 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { }, remove_imports: matches!(&options.analyze_dependencies, Some(d) if d.remove_imports), pseudo_classes: options.pseudo_classes, + context: None, } } @@ -326,6 +329,29 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { }), } } + + pub(crate) fn with_context) -> Result>( + &mut self, + selectors: &SelectorList, + f: F, + ) -> Result { + let parent = std::mem::take(&mut self.context); + let ctx = StyleContext { + selectors: unsafe { std::mem::transmute(selectors) }, + parent, + }; + + // I can't figure out what lifetime to use here to convince the compiler that + // the reference doesn't live beyond the function call. + self.context = Some(unsafe { std::mem::transmute(&ctx) }); + let res = f(self); + self.context = parent; + res + } + + pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'b>> { + self.context.clone() + } } impl<'a, 'b, 'c, W: std::fmt::Write + Sized> std::fmt::Write for Printer<'a, 'b, 'c, W> { diff --git a/src/properties/custom.rs b/src/properties/custom.rs index 3ee6d9cd..f5c85edf 100644 --- a/src/properties/custom.rs +++ b/src/properties/custom.rs @@ -49,7 +49,7 @@ impl<'i> CustomProperty<'i> { pub fn parse<'t, T>( name: CustomPropertyName<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result>> { let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { TokenList::parse(input, options, 0) @@ -148,7 +148,7 @@ impl<'i> UnparsedProperty<'i> { pub fn parse<'t, T>( property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result>> { let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { TokenList::parse(input, options, 0) @@ -260,7 +260,7 @@ impl<'i> TokenOrValue<'i> { impl<'i, T> ParseWithOptions<'i, T> for TokenList<'i> { fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result>> { TokenList::parse(input, options, 0) } @@ -269,7 +269,7 @@ impl<'i, T> ParseWithOptions<'i, T> for TokenList<'i> { impl<'i> TokenList<'i> { pub(crate) fn parse<'t, T>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, depth: usize, ) -> Result>> { let mut tokens = vec![]; @@ -294,7 +294,7 @@ impl<'i> TokenList<'i> { fn parse_into<'t, T>( input: &mut Parser<'i, 't>, tokens: &mut Vec>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, depth: usize, ) -> Result<(), ParseError<'i, ParserError<'i>>> { if depth > 500 { @@ -1054,7 +1054,7 @@ pub struct Variable<'i> { impl<'i> Variable<'i> { fn parse<'t, T>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, depth: usize, ) -> Result>> { let name = DashedIdentReference::parse_with_options(input, options)?; @@ -1205,7 +1205,7 @@ impl<'i> ToCss for EnvironmentVariableName<'i> { impl<'i> EnvironmentVariable<'i> { pub(crate) fn parse<'t, T>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, depth: usize, ) -> Result>> { input.expect_function_matching("env")?; @@ -1214,7 +1214,7 @@ impl<'i> EnvironmentVariable<'i> { pub(crate) fn parse_nested<'t, T>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, depth: usize, ) -> Result>> { let name = EnvironmentVariableName::parse(input)?; @@ -1342,7 +1342,7 @@ impl<'i> UnresolvedColor<'i> { fn parse<'t, T>( f: &CowArcStr<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result>> { let parser = ComponentParser::new(false); match_ignore_ascii_case! { &*f, diff --git a/src/properties/mod.rs b/src/properties/mod.rs index dbe5dd0c..345b4881 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -673,7 +673,7 @@ macro_rules! define_properties { impl<'i> Property<'i> { /// Parses a CSS property by name. - pub fn parse<'t, T>(property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions) -> Result, ParseError<'i, ParserError<'i>>> { + pub fn parse<'t, T>(property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i, T>) -> Result, ParseError<'i, ParserError<'i>>> { let state = input.state(); match property_id { @@ -714,7 +714,7 @@ macro_rules! define_properties { } /// Parses a CSS property from a string. - pub fn parse_string(property_id: PropertyId<'i>, input: &'i str, options: ParserOptions) -> Result>> { + pub fn parse_string(property_id: PropertyId<'i>, input: &'i str, options: ParserOptions<'_, 'i, T>) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); Self::parse(property_id, &mut parser, &options) diff --git a/src/rules/container.rs b/src/rules/container.rs index 2aaa7bef..1851dc47 100644 --- a/src/rules/container.rs +++ b/src/rules/container.rs @@ -8,7 +8,6 @@ use crate::error::{MinifyError, ParserError, PrinterError}; use crate::media_query::MediaCondition; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::traits::{Parse, ToCss}; use crate::values::ident::CustomIdent; #[cfg(feature = "visitor")] @@ -70,12 +69,8 @@ impl<'i, T> ContainerRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for ContainerRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for ContainerRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -97,7 +92,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for ContainerRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/rules/layer.rs b/src/rules/layer.rs index 76e4b6e2..c955c266 100644 --- a/src/rules/layer.rs +++ b/src/rules/layer.rs @@ -1,6 +1,6 @@ //! The `@layer` rule. -use super::{CssRuleList, Location, MinifyContext, StyleContext, ToCssWithContext}; +use super::{CssRuleList, Location, MinifyContext}; use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; @@ -140,12 +140,8 @@ impl<'i, T> LayerBlockRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for LayerBlockRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for LayerBlockRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -161,7 +157,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for LayerBlockRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/rules/media.rs b/src/rules/media.rs index 8857bade..f3898c9a 100644 --- a/src/rules/media.rs +++ b/src/rules/media.rs @@ -6,7 +6,6 @@ use crate::error::{MinifyError, PrinterError}; use crate::media_query::MediaList; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::traits::ToCss; #[cfg(feature = "visitor")] use crate::visitor::Visit; @@ -43,18 +42,14 @@ impl<'i, T> MediaRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for MediaRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for MediaRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { // If the media query always matches, we can just output the nested rules. if dest.minify && self.query.always_matches() { - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; return Ok(()); } @@ -66,7 +61,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for MediaRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/rules/mod.rs b/src/rules/mod.rs index b16dc8cf..885ac9ef 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -62,21 +62,21 @@ use crate::context::PropertyHandlerContext; use crate::declaration::DeclarationHandler; use crate::dependencies::{Dependency, ImportDependency}; use crate::error::{MinifyError, ParserError, PrinterError, PrinterErrorKind}; -use crate::parser::{DefaultAtRule, TopLevelRuleParser}; +use crate::parser::{parse_nested_at_rule, DefaultAtRule, NestedRuleParser, TopLevelRuleParser}; use crate::prefixes::Feature; use crate::printer::Printer; use crate::rules::keyframes::KeyframesName; -use crate::selector::{downlevel_selectors, get_prefix, is_equivalent}; +use crate::selector::{downlevel_selectors, get_prefix, is_equivalent, SelectorList}; use crate::stylesheet::ParserOptions; use crate::targets::Browsers; -use crate::traits::ToCss; +use crate::traits::{AtRuleParser, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] use crate::visitor::{Visit, VisitTypes, Visitor}; use container::ContainerRule; use counter_style::CounterStyleRule; -use cssparser::{parse_one_rule, AtRuleParser, ParseError, Parser, ParserInput}; +use cssparser::{parse_one_rule, ParseError, Parser, ParserInput}; use custom_media::CustomMediaRule; use document::MozDocumentRule; use font_face::FontFaceRule; @@ -92,19 +92,10 @@ use supports::SupportsRule; use unknown::UnknownAtRule; use viewport::ViewportRule; -pub(crate) trait ToCssWithContext<'a, 'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: std::fmt::Write; -} - -pub(crate) struct StyleContext<'a, 'i, T> { - pub rule: &'a StyleRule<'i, T>, - pub parent: Option<&'a StyleContext<'a, 'i, T>>, +#[derive(Clone)] +pub(crate) struct StyleContext<'a, 'i> { + pub selectors: &'a SelectorList<'i>, + pub parent: Option<&'a StyleContext<'a, 'i>>, } /// A source location. @@ -319,34 +310,30 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for CssRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { - CssRule::Media(media) => media.to_css_with_context(dest, context), + CssRule::Media(media) => media.to_css(dest), CssRule::Import(import) => import.to_css(dest), - CssRule::Style(style) => style.to_css_with_context(dest, context), + CssRule::Style(style) => style.to_css(dest), CssRule::Keyframes(keyframes) => keyframes.to_css(dest), CssRule::FontFace(font_face) => font_face.to_css(dest), CssRule::FontPaletteValues(f) => f.to_css(dest), CssRule::Page(font_face) => font_face.to_css(dest), - CssRule::Supports(supports) => supports.to_css_with_context(dest, context), + CssRule::Supports(supports) => supports.to_css(dest), CssRule::CounterStyle(counter_style) => counter_style.to_css(dest), CssRule::Namespace(namespace) => namespace.to_css(dest), CssRule::MozDocument(document) => document.to_css(dest), - CssRule::Nesting(nesting) => nesting.to_css_with_context(dest, context), + CssRule::Nesting(nesting) => nesting.to_css(dest), CssRule::Viewport(viewport) => viewport.to_css(dest), CssRule::CustomMedia(custom_media) => custom_media.to_css(dest), CssRule::LayerStatement(layer) => layer.to_css(dest), - CssRule::LayerBlock(layer) => layer.to_css_with_context(dest, context), + CssRule::LayerBlock(layer) => layer.to_css(dest), CssRule::Property(property) => property.to_css(dest), - CssRule::Container(container) => container.to_css_with_context(dest, context), + CssRule::Container(container) => container.to_css(dest), CssRule::Unknown(unknown) => unknown.to_css(dest), CssRule::Custom(rule) => rule.to_css(dest).map_err(|_| PrinterError { kind: PrinterErrorKind::FmtError, @@ -361,7 +348,7 @@ impl<'i, T> CssRule<'i, T> { /// Parse a single rule. pub fn parse<'t, P: AtRuleParser<'i, AtRule = T>>( input: &mut Parser<'i, 't>, - options: &mut ParserOptions<'_, 'i, P>, + options: &ParserOptions<'_, 'i, P>, ) -> Result>> { let (_, rule) = parse_one_rule(input, &mut TopLevelRuleParser::new(options))?; Ok(rule) @@ -370,20 +357,11 @@ impl<'i, T> CssRule<'i, T> { /// Parse a single rule from a string. pub fn parse_string>( input: &'i str, - mut options: ParserOptions<'_, 'i, P>, + options: ParserOptions<'_, 'i, P>, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); - Self::parse(&mut parser, &mut options) - } -} - -impl<'i, T: ToCss> ToCss for CssRule<'i, T> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - self.to_css_with_context(dest, None) + Self::parse(&mut parser, &options) } } @@ -395,6 +373,26 @@ pub struct CssRuleList<'i, R = DefaultAtRule>( #[cfg_attr(feature = "serde", serde(borrow))] pub Vec>, ); +impl<'i, T> CssRuleList<'i, T> { + /// Parse a rule list. + pub fn parse<'t, P: AtRuleParser<'i, AtRule = T>>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, P>, + ) -> Result>> { + let mut nested_parser = NestedRuleParser { options }; + nested_parser.parse_nested_rules(input) + } + + /// Parse a style block, with both declarations and rules. + /// Resulting declarations are returned in a nested style rule. + pub fn parse_style_block<'t, P: AtRuleParser<'i, AtRule = T>>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, P>, + ) -> Result>> { + parse_nested_at_rule(input, options) + } +} + // Manually implemented to avoid circular child types. #[cfg(feature = "visitor")] impl<'i, T: Visit<'i, T, V>, V: Visitor<'i, T>> Visit<'i, T, V> for CssRuleList<'i, T> { @@ -678,21 +676,8 @@ fn merge_style_rules<'i, T>( false } -impl<'i, T: ToCss> ToCss for CssRuleList<'i, T> { +impl<'a, 'i, T: ToCss> ToCss for CssRuleList<'i, T> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - self.to_css_with_context(dest, None) - } -} - -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for CssRuleList<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -734,7 +719,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for CssRuleList<'i, T> { } dest.newline()?; } - rule.to_css_with_context(dest, context)?; + rule.to_css(dest)?; last_without_block = matches!( rule, CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..) diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs index b834a27f..d2696202 100644 --- a/src/rules/nesting.rs +++ b/src/rules/nesting.rs @@ -6,7 +6,6 @@ use super::MinifyContext; use crate::error::{MinifyError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::traits::ToCss; #[cfg(feature = "visitor")] use crate::visitor::Visit; @@ -34,20 +33,16 @@ impl<'i, T> NestingRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for NestingRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for NestingRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { #[cfg(feature = "sourcemap")] dest.add_mapping(self.loc); - if context.is_none() { + if dest.context().is_none() { dest.write_str("@nest ")?; } - self.style.to_css_with_context(dest, context) + self.style.to_css(dest) } } diff --git a/src/rules/style.rs b/src/rules/style.rs index d9c1a114..f68d9891 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -11,7 +11,7 @@ use crate::error::ParserError; use crate::error::{MinifyError, PrinterError, PrinterErrorKind}; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{CssRuleList, StyleContext, ToCssWithContext}; +use crate::rules::CssRuleList; use crate::selector::{is_compatible, is_unused, SelectorList}; use crate::targets::Browsers; use crate::traits::ToCss; @@ -156,17 +156,13 @@ where } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for StyleRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for StyleRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { if self.vendor_prefix.is_empty() { - self.to_css_base(dest, context) + self.to_css_base(dest) } else { let mut first_rule = true; macro_rules! prefix { @@ -182,7 +178,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for StyleRule<'i, T> { dest.newline()?; } dest.vendor_prefix = VendorPrefix::$prefix; - self.to_css_base(dest, context)?; + self.to_css_base(dest)?; } }; } @@ -200,11 +196,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for StyleRule<'i, T> { } impl<'a, 'i, T: ToCss> StyleRule<'i, T> { - fn to_css_base( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> + fn to_css_base(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -218,7 +210,7 @@ impl<'a, 'i, T: ToCss> StyleRule<'i, T> { if has_declarations { #[cfg(feature = "sourcemap")] dest.add_mapping(self.loc); - self.selectors.to_css_with_context(dest, context)?; + self.selectors.to_css(dest)?; dest.whitespace()?; dest.write_char('{')?; dest.indent(); @@ -286,13 +278,7 @@ impl<'a, 'i, T: ToCss> StyleRule<'i, T> { } else { end!(); newline!(); - self.rules.to_css_with_context( - dest, - Some(&StyleContext { - rule: self, - parent: context, - }), - )?; + dest.with_context(&self.selectors, |dest| self.rules.to_css(dest))?; } Ok(()) diff --git a/src/rules/supports.rs b/src/rules/supports.rs index 2481f584..365ba11b 100644 --- a/src/rules/supports.rs +++ b/src/rules/supports.rs @@ -6,7 +6,6 @@ use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; use crate::properties::PropertyId; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::targets::Browsers; use crate::traits::{Parse, ToCss}; use crate::values::string::CowArcStr; @@ -48,12 +47,8 @@ impl<'i, T> SupportsRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for SupportsRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for SupportsRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -65,7 +60,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for SupportsRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/selector.rs b/src/selector.rs index fb175229..614ee74a 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -2,13 +2,12 @@ use crate::compat::Feature; use crate::error::{ParserError, PrinterError}; -use crate::parser::DefaultAtRule; use crate::printer::Printer; use crate::properties::custom::TokenList; -use crate::rules::{StyleContext, ToCssWithContext}; +use crate::rules::StyleContext; use crate::stylesheet::{ParserOptions, PrinterOptions}; use crate::targets::Browsers; -use crate::traits::{Parse, ToCss}; +use crate::traits::{Parse, ParseWithOptions, ToCss}; use crate::values::ident::Ident; use crate::values::string::CSSString; use crate::vendor_prefix::VendorPrefix; @@ -21,7 +20,6 @@ use parcel_selectors::{ attr::{AttrSelectorOperator, ParsedAttrSelectorOperation, ParsedCaseSensitivity}, parser::SelectorImpl, }; -use std::collections::HashMap; use std::collections::HashSet; use std::fmt; @@ -62,14 +60,11 @@ impl<'i> SelectorImpl<'i> for Selectors { fn to_css(selectors: &SelectorList<'i>, dest: &mut W) -> std::fmt::Result { let mut printer = Printer::new(dest, PrinterOptions::default()); - serialize_selector_list::<_, _, DefaultAtRule>(selectors.0.iter(), &mut printer, None, false) - .map_err(|_| std::fmt::Error) + serialize_selector_list(selectors.0.iter(), &mut printer, None, false).map_err(|_| std::fmt::Error) } } pub(crate) struct SelectorParser<'a, 'o, 'i, T> { - pub default_namespace: &'a Option>, - pub namespace_prefixes: &'a HashMap, CowArcStr<'i>>, pub is_nesting_allowed: bool, pub options: &'a ParserOptions<'o, 'i, T>, } @@ -303,11 +298,11 @@ impl<'a, 'o, 'i, T> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, } fn default_namespace(&self) -> Option> { - self.default_namespace.clone() + None } fn namespace_for_prefix(&self, prefix: &Ident<'i>) -> Option> { - self.namespace_prefixes.get(&prefix.0).cloned() + Some(prefix.0.clone()) } #[inline] @@ -560,178 +555,176 @@ impl<'i> cssparser::ToCss for PseudoClass<'i> { } } -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for PseudoClass<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: fmt::Write, - { - use PseudoClass::*; - match &self { - Lang { languages: lang } => { - dest.write_str(":lang(")?; - let mut first = true; - for lang in lang { - if first { - first = false; - } else { - dest.delim(',', false)?; - } - serialize_identifier(lang, dest)?; +fn serialize_pseudo_class<'a, 'i, W>( + pseudo_class: &PseudoClass<'i>, + dest: &mut Printer, + context: Option<&StyleContext>, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + use PseudoClass::*; + match pseudo_class { + Lang { languages: lang } => { + dest.write_str(":lang(")?; + let mut first = true; + for lang in lang { + if first { + first = false; + } else { + dest.delim(',', false)?; } - return dest.write_str(")"); + serialize_identifier(lang, dest)?; } - Dir { direction: dir } => { - dest.write_str(":dir(")?; - dir.to_css(dest)?; - return dest.write_str(")"); - } - _ => {} + return dest.write_str(")"); } - - macro_rules! write_prefixed { - ($prefix: ident, $val: expr) => {{ - dest.write_char(':')?; - // If the printer has a vendor prefix override, use that. - let vp = if !dest.vendor_prefix.is_empty() { - dest.vendor_prefix - } else { - *$prefix - }; - vp.to_css(dest)?; - dest.write_str($val) - }}; + Dir { direction: dir } => { + dest.write_str(":dir(")?; + dir.to_css(dest)?; + return dest.write_str(")"); } + _ => {} + } - macro_rules! pseudo { - ($key: ident, $s: literal) => {{ - let class = if let Some(pseudo_classes) = &dest.pseudo_classes { - pseudo_classes.$key - } else { - None - }; - - if let Some(class) = class { - dest.write_char('.')?; - dest.write_ident(class) - } else { - dest.write_str($s) - } - }}; - } - - match &self { - // https://drafts.csswg.org/selectors-4/#useraction-pseudos - Hover => pseudo!(hover, ":hover"), - Active => pseudo!(active, ":active"), - Focus => pseudo!(focus, ":focus"), - FocusVisible => pseudo!(focus_visible, ":focus-visible"), - FocusWithin => pseudo!(focus_within, ":focus-within"), - - // https://drafts.csswg.org/selectors-4/#time-pseudos - Current => dest.write_str(":current"), - Past => dest.write_str(":past"), - Future => dest.write_str(":future"), + macro_rules! write_prefixed { + ($prefix: ident, $val: expr) => {{ + dest.write_char(':')?; + // If the printer has a vendor prefix override, use that. + let vp = if !dest.vendor_prefix.is_empty() { + dest.vendor_prefix + } else { + *$prefix + }; + vp.to_css(dest)?; + dest.write_str($val) + }}; + } - // https://drafts.csswg.org/selectors-4/#resource-pseudos - Playing => dest.write_str(":playing"), - Paused => dest.write_str(":paused"), - Seeking => dest.write_str(":seeking"), - Buffering => dest.write_str(":buffering"), - Stalled => dest.write_str(":stalled"), - Muted => dest.write_str(":muted"), - VolumeLocked => dest.write_str(":volume-locked"), + macro_rules! pseudo { + ($key: ident, $s: literal) => {{ + let class = if let Some(pseudo_classes) = &dest.pseudo_classes { + pseudo_classes.$key + } else { + None + }; - // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class - Fullscreen(prefix) => { - dest.write_char(':')?; - let vp = if !dest.vendor_prefix.is_empty() { - dest.vendor_prefix - } else { - *prefix - }; - vp.to_css(dest)?; - if vp == VendorPrefix::WebKit || vp == VendorPrefix::Moz { - dest.write_str("full-screen") - } else { - dest.write_str("fullscreen") - } + if let Some(class) = class { + dest.write_char('.')?; + dest.write_ident(class) + } else { + dest.write_str($s) } + }}; + } - // https://drafts.csswg.org/selectors-4/#the-defined-pseudo - Defined => dest.write_str(":defined"), - - // https://drafts.csswg.org/selectors-4/#location - AnyLink(prefix) => write_prefixed!(prefix, "any-link"), - Link => dest.write_str(":link"), - LocalLink => dest.write_str(":local-link"), - Target => dest.write_str(":target"), - TargetWithin => dest.write_str(":target-within"), - Visited => dest.write_str(":visited"), - - // https://drafts.csswg.org/selectors-4/#input-pseudos - Enabled => dest.write_str(":enabled"), - Disabled => dest.write_str(":disabled"), - ReadOnly(prefix) => write_prefixed!(prefix, "read-only"), - ReadWrite(prefix) => write_prefixed!(prefix, "read-write"), - PlaceholderShown(prefix) => write_prefixed!(prefix, "placeholder-shown"), - Default => dest.write_str(":default"), - Checked => dest.write_str(":checked"), - Indeterminate => dest.write_str(":indeterminate"), - Blank => dest.write_str(":blank"), - Valid => dest.write_str(":valid"), - Invalid => dest.write_str(":invalid"), - InRange => dest.write_str(":in-range"), - OutOfRange => dest.write_str(":out-of-range"), - Required => dest.write_str(":required"), - Optional => dest.write_str(":optional"), - UserValid => dest.write_str(":user-valid"), - UserInvalid => dest.write_str(":user-invalid"), - - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill - Autofill(prefix) => write_prefixed!(prefix, "autofill"), - - Local { selector } => selector.to_css_with_context(dest, context), - Global { selector } => { - let css_module = std::mem::take(&mut dest.css_module); - selector.to_css_with_context(dest, context)?; - dest.css_module = css_module; - Ok(()) + match pseudo_class { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + Hover => pseudo!(hover, ":hover"), + Active => pseudo!(active, ":active"), + Focus => pseudo!(focus, ":focus"), + FocusVisible => pseudo!(focus_visible, ":focus-visible"), + FocusWithin => pseudo!(focus_within, ":focus-within"), + + // https://drafts.csswg.org/selectors-4/#time-pseudos + Current => dest.write_str(":current"), + Past => dest.write_str(":past"), + Future => dest.write_str(":future"), + + // https://drafts.csswg.org/selectors-4/#resource-pseudos + Playing => dest.write_str(":playing"), + Paused => dest.write_str(":paused"), + Seeking => dest.write_str(":seeking"), + Buffering => dest.write_str(":buffering"), + Stalled => dest.write_str(":stalled"), + Muted => dest.write_str(":muted"), + VolumeLocked => dest.write_str(":volume-locked"), + + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + Fullscreen(prefix) => { + dest.write_char(':')?; + let vp = if !dest.vendor_prefix.is_empty() { + dest.vendor_prefix + } else { + *prefix + }; + vp.to_css(dest)?; + if vp == VendorPrefix::WebKit || vp == VendorPrefix::Moz { + dest.write_str("full-screen") + } else { + dest.write_str("fullscreen") } + } - // https://webkit.org/blog/363/styling-scrollbars/ - WebKitScrollbar(s) => { - use WebKitScrollbarPseudoClass::*; - dest.write_str(match s { - Horizontal => ":horizontal", - Vertical => ":vertical", - Decrement => ":decrement", - Increment => ":increment", - Start => ":start", - End => ":end", - DoubleButton => ":double-button", - SingleButton => ":single-button", - NoButton => ":no-button", - CornerPresent => ":corner-present", - WindowInactive => ":window-inactive", - }) - } + // https://drafts.csswg.org/selectors-4/#the-defined-pseudo + Defined => dest.write_str(":defined"), + + // https://drafts.csswg.org/selectors-4/#location + AnyLink(prefix) => write_prefixed!(prefix, "any-link"), + Link => dest.write_str(":link"), + LocalLink => dest.write_str(":local-link"), + Target => dest.write_str(":target"), + TargetWithin => dest.write_str(":target-within"), + Visited => dest.write_str(":visited"), + + // https://drafts.csswg.org/selectors-4/#input-pseudos + Enabled => dest.write_str(":enabled"), + Disabled => dest.write_str(":disabled"), + ReadOnly(prefix) => write_prefixed!(prefix, "read-only"), + ReadWrite(prefix) => write_prefixed!(prefix, "read-write"), + PlaceholderShown(prefix) => write_prefixed!(prefix, "placeholder-shown"), + Default => dest.write_str(":default"), + Checked => dest.write_str(":checked"), + Indeterminate => dest.write_str(":indeterminate"), + Blank => dest.write_str(":blank"), + Valid => dest.write_str(":valid"), + Invalid => dest.write_str(":invalid"), + InRange => dest.write_str(":in-range"), + OutOfRange => dest.write_str(":out-of-range"), + Required => dest.write_str(":required"), + Optional => dest.write_str(":optional"), + UserValid => dest.write_str(":user-valid"), + UserInvalid => dest.write_str(":user-invalid"), + + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + Autofill(prefix) => write_prefixed!(prefix, "autofill"), + + Local { selector } => serialize_selector(selector, dest, context, false), + Global { selector } => { + let css_module = std::mem::take(&mut dest.css_module); + serialize_selector(selector, dest, context, false)?; + dest.css_module = css_module; + Ok(()) + } - Lang { languages: _ } | Dir { direction: _ } => unreachable!(), - Custom { name } => { - dest.write_char(':')?; - return dest.write_str(&name); - } - CustomFunction { name, arguments: args } => { - dest.write_char(':')?; - dest.write_str(name)?; - dest.write_char('(')?; - args.to_css(dest, false)?; - dest.write_char(')') - } + // https://webkit.org/blog/363/styling-scrollbars/ + WebKitScrollbar(s) => { + use WebKitScrollbarPseudoClass::*; + dest.write_str(match s { + Horizontal => ":horizontal", + Vertical => ":vertical", + Decrement => ":decrement", + Increment => ":increment", + Start => ":start", + End => ":end", + DoubleButton => ":double-button", + SingleButton => ":single-button", + NoButton => ":no-button", + CornerPresent => ":corner-present", + WindowInactive => ":window-inactive", + }) + } + + Lang { languages: _ } | Dir { direction: _ } => unreachable!(), + Custom { name } => { + dest.write_char(':')?; + return dest.write_str(&name); + } + CustomFunction { name, arguments: args } => { + dest.write_char(':')?; + dest.write_str(name)?; + dest.write_char('(')?; + args.to_css(dest, false)?; + dest.write_char(')') } } } @@ -872,99 +865,101 @@ impl<'i> cssparser::ToCss for PseudoElement<'i> { } } -impl<'i> ToCss for PseudoElement<'i> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: fmt::Write, - { - use PseudoElement::*; +fn serialize_pseudo_element<'a, 'i, W>( + pseudo_element: &PseudoElement, + dest: &mut Printer, + context: Option<&StyleContext>, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + use PseudoElement::*; + + macro_rules! write_prefix { + ($prefix: ident) => {{ + dest.write_str("::")?; + // If the printer has a vendor prefix override, use that. + let vp = if !dest.vendor_prefix.is_empty() { + dest.vendor_prefix + } else { + *$prefix + }; + vp.to_css(dest)?; + vp + }}; + } - macro_rules! write_prefix { - ($prefix: ident) => {{ - dest.write_str("::")?; - // If the printer has a vendor prefix override, use that. - let vp = if !dest.vendor_prefix.is_empty() { - dest.vendor_prefix - } else { - *$prefix - }; - vp.to_css(dest)?; - vp - }}; - } + macro_rules! write_prefixed { + ($prefix: ident, $val: expr) => {{ + write_prefix!($prefix); + dest.write_str($val) + }}; + } - macro_rules! write_prefixed { - ($prefix: ident, $val: expr) => {{ - write_prefix!($prefix); - dest.write_str($val) - }}; + match pseudo_element { + // CSS2 pseudo elements support a single colon syntax in addition + // to the more correct double colon for other pseudo elements. + // We use that here because it's supported everywhere and is shorter. + After => dest.write_str(":after"), + Before => dest.write_str(":before"), + FirstLine => dest.write_str(":first-line"), + FirstLetter => dest.write_str(":first-letter"), + Marker => dest.write_str("::marker"), + Selection(prefix) => write_prefixed!(prefix, "selection"), + Cue => dest.write_str("::cue"), + CueRegion => dest.write_str("::cue-region"), + CueFunction { selector } => { + dest.write_str("::cue(")?; + serialize_selector(selector, dest, context, false)?; + dest.write_char(')') } - - match &self { - // CSS2 pseudo elements support a single colon syntax in addition - // to the more correct double colon for other pseudo elements. - // We use that here because it's supported everywhere and is shorter. - After => dest.write_str(":after"), - Before => dest.write_str(":before"), - FirstLine => dest.write_str(":first-line"), - FirstLetter => dest.write_str(":first-letter"), - Marker => dest.write_str("::marker"), - Selection(prefix) => write_prefixed!(prefix, "selection"), - Cue => dest.write_str("::cue"), - CueRegion => dest.write_str("::cue-region"), - CueFunction { selector } => { - dest.write_str("::cue(")?; - serialize_selector::<_, DefaultAtRule>(selector, dest, None, false)?; - dest.write_char(')') - } - CueRegionFunction(selector) => { - dest.write_str("::cue-region(")?; - serialize_selector::<_, DefaultAtRule>(selector, dest, None, false)?; - dest.write_char(')') - } - Placeholder(prefix) => { - let vp = write_prefix!(prefix); - if vp == VendorPrefix::WebKit || vp == VendorPrefix::Ms { - dest.write_str("input-placeholder") - } else { - dest.write_str("placeholder") - } - } - Backdrop(prefix) => write_prefixed!(prefix, "backdrop"), - FileSelectorButton(prefix) => { - let vp = write_prefix!(prefix); - if vp == VendorPrefix::WebKit { - dest.write_str("file-upload-button") - } else if vp == VendorPrefix::Ms { - dest.write_str("browse") - } else { - dest.write_str("file-selector-button") - } - } - WebKitScrollbar(s) => { - use WebKitScrollbarPseudoElement::*; - dest.write_str(match s { - Scrollbar => "::-webkit-scrollbar", - Button => "::-webkit-scrollbar-button", - Track => "::-webkit-scrollbar-track", - TrackPiece => "::-webkit-scrollbar-track-piece", - Thumb => "::-webkit-scrollbar-thumb", - Corner => "::-webkit-scrollbar-corner", - Resizer => "::-webkit-resizer", - }) - } - Custom { name: val } => { - dest.write_str("::")?; - return dest.write_str(val); + CueRegionFunction(selector) => { + dest.write_str("::cue-region(")?; + serialize_selector(selector, dest, context, false)?; + dest.write_char(')') + } + Placeholder(prefix) => { + let vp = write_prefix!(prefix); + if vp == VendorPrefix::WebKit || vp == VendorPrefix::Ms { + dest.write_str("input-placeholder") + } else { + dest.write_str("placeholder") } - CustomFunction { name, arguments: args } => { - dest.write_str("::")?; - dest.write_str(name)?; - dest.write_char('(')?; - args.to_css(dest, false)?; - dest.write_char(')') + } + Backdrop(prefix) => write_prefixed!(prefix, "backdrop"), + FileSelectorButton(prefix) => { + let vp = write_prefix!(prefix); + if vp == VendorPrefix::WebKit { + dest.write_str("file-upload-button") + } else if vp == VendorPrefix::Ms { + dest.write_str("browse") + } else { + dest.write_str("file-selector-button") } } + WebKitScrollbar(s) => { + use WebKitScrollbarPseudoElement::*; + dest.write_str(match s { + Scrollbar => "::-webkit-scrollbar", + Button => "::-webkit-scrollbar-button", + Track => "::-webkit-scrollbar-track", + TrackPiece => "::-webkit-scrollbar-track-piece", + Thumb => "::-webkit-scrollbar-thumb", + Corner => "::-webkit-scrollbar-corner", + Resizer => "::-webkit-resizer", + }) + } + Custom { name: val } => { + dest.write_str("::")?; + return dest.write_str(val); + } + CustomFunction { name, arguments: args } => { + dest.write_str("::")?; + dest.write_str(name)?; + dest.write_char('(')?; + args.to_css(dest, false)?; + dest.write_char(')') + } } } @@ -1030,25 +1025,12 @@ impl<'i> PseudoElement<'i> { } } -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for SelectorList<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: fmt::Write, - { - serialize_selector_list(self.0.iter(), dest, context, false) - } -} - -impl<'i> ToCss for SelectorList<'i> { +impl<'a, 'i> ToCss for SelectorList<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where - W: std::fmt::Write, + W: fmt::Write, { - serialize_selector_list::<_, _, DefaultAtRule>(self.0.iter(), dest, None, false) + serialize_selector_list(self.0.iter(), dest, dest.context(), false) } } @@ -1068,23 +1050,19 @@ impl ToCss for Combinator { } // Copied from the selectors crate and modified to override to_css implementation. -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for Selector<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i> ToCss for Selector<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: fmt::Write, { - serialize_selector(self, dest, context, false) + serialize_selector(self, dest, dest.context(), false) } } -fn serialize_selector<'a, 'i, W, T>( +fn serialize_selector<'a, 'i, W>( selector: &Selector<'i>, dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, + context: Option<&StyleContext>, mut is_relative: bool, ) -> Result<(), PrinterError> where @@ -1169,7 +1147,7 @@ where } for simple in iter { - simple.to_css_with_context(dest, context)?; + serialize_component(simple, dest, context)?; } if swap_nesting { @@ -1199,15 +1177,15 @@ where // This ensures that the compiled selector is valid. e.g. (div.foo is valid, .foodiv is not). let nesting = iter.next().unwrap(); let local = iter.next().unwrap(); - local.to_css_with_context(dest, context)?; + serialize_component(local, dest, context)?; // Also check the next item in case of namespaces. if first_non_namespace > first_index { let local = iter.next().unwrap(); - local.to_css_with_context(dest, context)?; + serialize_component(local, dest, context)?; } - nesting.to_css_with_context(dest, context)?; + serialize_component(nesting, dest, context)?; } else if has_leading_nesting && context.is_some() { // Nesting selector may serialize differently if it is leading, due to type selectors. iter.next(); @@ -1224,7 +1202,7 @@ where continue; } } - simple.to_css_with_context(dest, context)?; + serialize_component(simple, dest, context)?; } } @@ -1248,115 +1226,113 @@ where Ok(()) } -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for Component<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: fmt::Write, - { - match &self { - Component::Combinator(ref c) => c.to_css(dest), - Component::AttributeInNoNamespace { - ref local_name, - operator, - ref value, - case_sensitivity, - .. - } => { - dest.write_char('[')?; - cssparser::ToCss::to_css(local_name, dest)?; - cssparser::ToCss::to_css(operator, dest)?; - - if dest.minify { - // Serialize as both an identifier and a string and choose the shorter one. - let mut id = String::new(); - serialize_identifier(&value, &mut id)?; - - let s = value.to_css_string(Default::default())?; - - if id.len() > 0 && id.len() < s.len() { - dest.write_str(&id)?; - } else { - dest.write_str(&s)?; - } +fn serialize_component<'a, 'i, W>( + component: &Component, + dest: &mut Printer, + context: Option<&StyleContext>, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + match component { + Component::Combinator(ref c) => c.to_css(dest), + Component::AttributeInNoNamespace { + ref local_name, + operator, + ref value, + case_sensitivity, + .. + } => { + dest.write_char('[')?; + cssparser::ToCss::to_css(local_name, dest)?; + cssparser::ToCss::to_css(operator, dest)?; + + if dest.minify { + // Serialize as both an identifier and a string and choose the shorter one. + let mut id = String::new(); + serialize_identifier(&value, &mut id)?; + + let s = value.to_css_string(Default::default())?; + + if id.len() > 0 && id.len() < s.len() { + dest.write_str(&id)?; } else { - value.to_css(dest)?; - } - - match case_sensitivity { - parcel_selectors::attr::ParsedCaseSensitivity::CaseSensitive - | parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {} - parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, - parcel_selectors::attr::ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + dest.write_str(&s)?; } - dest.write_char(']') + } else { + value.to_css(dest)?; } - Component::Is(ref list) - | Component::Where(ref list) - | Component::Negation(ref list) - | Component::Any(_, ref list) => { - match *self { - Component::Where(..) => dest.write_str(":where(")?, - Component::Is(ref selectors) => { - // If there's only one simple selector, serialize it directly. - if selectors.len() == 1 { - let first = selectors.first().unwrap(); - if !has_type_selector(first) && is_simple(first) { - serialize_selector(first, dest, context, false)?; - return Ok(()); - } - } - let vp = dest.vendor_prefix; - if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { - dest.write_char(':')?; - vp.to_css(dest)?; - dest.write_str("any(")?; - } else { - dest.write_str(":is(")?; + match case_sensitivity { + parcel_selectors::attr::ParsedCaseSensitivity::CaseSensitive + | parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {} + parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, + parcel_selectors::attr::ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + } + dest.write_char(']') + } + Component::Is(ref list) + | Component::Where(ref list) + | Component::Negation(ref list) + | Component::Any(_, ref list) => { + match *component { + Component::Where(..) => dest.write_str(":where(")?, + Component::Is(ref selectors) => { + // If there's only one simple selector, serialize it directly. + if selectors.len() == 1 { + let first = selectors.first().unwrap(); + if !has_type_selector(first) && is_simple(first) { + serialize_selector(first, dest, context, false)?; + return Ok(()); } } - Component::Negation(..) => return serialize_negation(list.iter(), dest, context), - Component::Any(ref prefix, ..) => { + + let vp = dest.vendor_prefix; + if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { dest.write_char(':')?; - prefix.to_css(dest)?; + vp.to_css(dest)?; dest.write_str("any(")?; + } else { + dest.write_str(":is(")?; } - _ => unreachable!(), } - serialize_selector_list(list.iter(), dest, context, false)?; - dest.write_str(")") - } - Component::Has(ref list) => { - dest.write_str(":has(")?; - serialize_selector_list(list.iter(), dest, context, true)?; - dest.write_str(")") - } - Component::NonTSPseudoClass(pseudo) => pseudo.to_css_with_context(dest, context), - Component::PseudoElement(pseudo) => pseudo.to_css(dest), - Component::Nesting => serialize_nesting(dest, context, false), - Component::Class(ref class) => { - dest.write_char('.')?; - dest.write_ident(&class.0) - } - Component::ID(ref id) => { - dest.write_char('#')?; - dest.write_ident(&id.0) - } - _ => { - cssparser::ToCss::to_css(self, dest)?; - Ok(()) + Component::Negation(..) => return serialize_negation(list.iter(), dest, context), + Component::Any(ref prefix, ..) => { + dest.write_char(':')?; + prefix.to_css(dest)?; + dest.write_str("any(")?; + } + _ => unreachable!(), } + serialize_selector_list(list.iter(), dest, context, false)?; + dest.write_str(")") + } + Component::Has(ref list) => { + dest.write_str(":has(")?; + serialize_selector_list(list.iter(), dest, context, true)?; + dest.write_str(")") + } + Component::NonTSPseudoClass(pseudo) => serialize_pseudo_class(pseudo, dest, context), + Component::PseudoElement(pseudo) => serialize_pseudo_element(pseudo, dest, context), + Component::Nesting => serialize_nesting(dest, context, false), + Component::Class(ref class) => { + dest.write_char('.')?; + dest.write_ident(&class.0) + } + Component::ID(ref id) => { + dest.write_char('#')?; + dest.write_ident(&id.0) + } + _ => { + cssparser::ToCss::to_css(component, dest)?; + Ok(()) } } } -fn serialize_nesting( +fn serialize_nesting( dest: &mut Printer, - context: Option<&StyleContext>, + context: Option<&StyleContext>, first: bool, ) -> Result<(), PrinterError> where @@ -1367,13 +1343,13 @@ where // Otherwise, use an :is() pseudo class. // Type selectors are only allowed at the start of a compound selector, // so use :is() if that is not the case. - if ctx.rule.selectors.0.len() == 1 - && (first || (!has_type_selector(&ctx.rule.selectors.0[0]) && is_simple(&ctx.rule.selectors.0[0]))) + if ctx.selectors.0.len() == 1 + && (first || (!has_type_selector(&ctx.selectors.0[0]) && is_simple(&ctx.selectors.0[0]))) { - ctx.rule.selectors.0.first().unwrap().to_css_with_context(dest, ctx.parent) + serialize_selector(ctx.selectors.0.first().unwrap(), dest, ctx.parent, false) } else { dest.write_str(":is(")?; - serialize_selector_list(ctx.rule.selectors.0.iter(), dest, ctx.parent, false)?; + serialize_selector_list(ctx.selectors.0.iter(), dest, ctx.parent, false)?; dest.write_char(')') } } else { @@ -1416,10 +1392,10 @@ fn is_namespace(component: Option<&Component>) -> bool { ) } -fn serialize_selector_list<'a, 'i: 'a, I, W, T>( +fn serialize_selector_list<'a, 'i: 'a, I, W>( iter: I, dest: &mut Printer, - context: Option<&StyleContext<'_, 'i, T>>, + context: Option<&StyleContext>, is_relative: bool, ) -> Result<(), PrinterError> where @@ -1437,10 +1413,10 @@ where Ok(()) } -fn serialize_negation<'a, 'i: 'a, I, W, T>( +fn serialize_negation<'a, 'i: 'a, I, W>( iter: I, dest: &mut Printer, - context: Option<&StyleContext<'_, 'i, T>>, + context: Option<&StyleContext>, ) -> Result<(), PrinterError> where I: Iterator>, @@ -1460,7 +1436,7 @@ where } else { for selector in iter { dest.write_str(":not(")?; - selector.to_css_with_context(dest, context)?; + serialize_selector(selector, dest, context, false)?; dest.write_char(')')?; } } @@ -1852,3 +1828,34 @@ impl<'i, T: Visit<'i, T, V>, V: Visitor<'i, T>> Visit<'i, T, V> for Selector<'i> fn visit_children(&mut self, _visitor: &mut V) {} } + +impl<'i, T> ParseWithOptions<'i, T> for Selector<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, T>, + ) -> Result>> { + Selector::parse( + &SelectorParser { + is_nesting_allowed: options.nesting, + options: &options, + }, + input, + ) + } +} + +impl<'i, T> ParseWithOptions<'i, T> for SelectorList<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, T>, + ) -> Result>> { + SelectorList::parse( + &SelectorParser { + is_nesting_allowed: options.nesting, + options: &options, + }, + input, + parcel_selectors::parser::NestingRequirement::None, + ) + } +} From 0efe206f65fcfddfbebf8a4456c7512061a89202 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 11 Jan 2023 21:29:58 -0500 Subject: [PATCH 02/12] Expose options to custom at rule parsers --- examples/custom_at_rule.rs | 17 ++- src/bundler.rs | 3 +- src/error.rs | 3 + src/parser.rs | 281 ++++++++++++++++-------------------- src/stylesheet.rs | 4 +- src/traits.rs | 95 +++++++++++- tests/test_custom_parser.rs | 15 +- 7 files changed, 248 insertions(+), 170 deletions(-) diff --git a/examples/custom_at_rule.rs b/examples/custom_at_rule.rs index 9f3bcf24..f04a56f7 100644 --- a/examples/custom_at_rule.rs +++ b/examples/custom_at_rule.rs @@ -1,4 +1,7 @@ -use std::collections::HashMap; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; use cssparser::*; use lightningcss::{ @@ -10,7 +13,7 @@ use lightningcss::{ selector::{Component, Selector}, stylesheet::{ParserOptions, PrinterOptions, StyleSheet}, targets::Browsers, - traits::ToCss, + traits::{AtRuleParser, ToCss}, values::{color::CssColor, length::LengthValue}, vendor_prefix::VendorPrefix, visit_types, @@ -21,7 +24,7 @@ fn main() { let args: Vec = std::env::args().collect(); let source = std::fs::read_to_string(&args[1]).unwrap(); let opts = ParserOptions { - at_rule_parser: Some(TailwindAtRuleParser), + at_rule_parser: Some(Arc::new(RwLock::new(TailwindAtRuleParser))), filename: args[1].clone(), nesting: true, custom_media: false, @@ -101,6 +104,7 @@ impl<'i> AtRuleParser<'i> for TailwindAtRuleParser { &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i, Self>, ) -> Result> { match_ignore_ascii_case! {&*name, "tailwind" => { @@ -133,7 +137,12 @@ impl<'i> AtRuleParser<'i> for TailwindAtRuleParser { } } - fn rule_without_block(&mut self, prelude: Self::Prelude, start: &ParserState) -> Result { + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + _options: &ParserOptions<'_, 'i, Self>, + ) -> Result { let loc = start.source_location(); match prelude { Prelude::Tailwind(directive) => Ok(AtRule::Tailwind(TailwindRule { directive, loc })), diff --git a/src/bundler.rs b/src/bundler.rs index 80f68af6..cbc5ae8c 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -36,7 +36,7 @@ use crate::{ layer::{LayerBlockRule, LayerName}, Location, }, - traits::ToCss, + traits::{AtRuleParser, ToCss}, values::ident::DashedIdentReference, }; use crate::{ @@ -50,7 +50,6 @@ use crate::{ }, stylesheet::{ParserOptions, StyleSheet}, }; -use cssparser::AtRuleParser; use dashmap::DashMap; use parcel_sourcemap::SourceMap; use rayon::prelude::*; diff --git a/src/error.rs b/src/error.rs index a203dc56..f694ca16 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,6 +71,8 @@ impl fmt::Display for ErrorLocation { pub enum ParserError<'i> { /// An at rule body was invalid. AtRuleBodyInvalid, + /// An at rule prelude was invalid + AtRulePreludeInvalid, /// An unknown or unsupported at rule was encountered. AtRuleInvalid(CowArcStr<'i>), /// Unexpectedly encountered the end of input data. @@ -106,6 +108,7 @@ impl<'i> fmt::Display for ParserError<'i> { use ParserError::*; match self { AtRuleBodyInvalid => write!(f, "Invalid @ rule body"), + AtRulePreludeInvalid => write!(f, "Invalid @ rule prelude"), AtRuleInvalid(name) => write!(f, "Unknown at rule: @{}", name), EndOfInput => write!(f, "Unexpected end of input"), InvalidDeclaration => write!(f, "Invalid declaration"), diff --git a/src/parser.rs b/src/parser.rs index 0906beff..ccd4931d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -34,7 +34,6 @@ use crate::vendor_prefix::VendorPrefix; use crate::visitor::{Visit, VisitTypes, Visitor}; use cssparser::*; use parcel_selectors::parser::NestingRequirement; -use std::collections::HashMap; use std::sync::{Arc, RwLock}; /// CSS parsing options. @@ -56,7 +55,7 @@ pub struct ParserOptions<'o, 'i, T = DefaultAtRuleParser> { /// A list that will be appended to when a warning occurs. pub warnings: Option>>>>>, /// A custom at rule parser. - pub at_rule_parser: Option, + pub at_rule_parser: Option>>, } impl<'o, 'i, T> ParserOptions<'o, 'i, T> { @@ -94,7 +93,7 @@ impl<'o, 'i> ParserOptions<'o, 'i, DefaultAtRuleParser> { #[derive(Clone, Default)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub struct DefaultAtRuleParser; -impl<'i> AtRuleParser<'i> for DefaultAtRuleParser { +impl<'i> crate::traits::AtRuleParser<'i> for DefaultAtRuleParser { type AtRule = DefaultAtRule; type Error = (); type Prelude = (); @@ -130,28 +129,20 @@ enum State { /// The parser for the top-level rules in a stylesheet. pub struct TopLevelRuleParser<'a, 'o, 'i, T> { - default_namespace: Option>, - namespace_prefixes: HashMap, CowArcStr<'i>>, - pub options: &'a mut ParserOptions<'o, 'i, T>, + pub options: &'a ParserOptions<'o, 'i, T>, state: State, } impl<'a, 'o, 'b, 'i, T> TopLevelRuleParser<'a, 'o, 'i, T> { - pub fn new(options: &'a mut ParserOptions<'o, 'i, T>) -> Self { + pub fn new(options: &'a ParserOptions<'o, 'i, T>) -> Self { TopLevelRuleParser { - default_namespace: None, - namespace_prefixes: HashMap::new(), options, state: State::Start, } } fn nested<'x: 'b>(&'x mut self) -> NestedRuleParser<'_, 'o, 'i, T> { - NestedRuleParser { - default_namespace: &mut self.default_namespace, - namespace_prefixes: &mut self.namespace_prefixes, - options: &mut self.options, - } + NestedRuleParser { options: &self.options } } } @@ -208,7 +199,7 @@ pub enum AtRulePrelude<'i, T> { Custom(T), } -impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i, T> { type Prelude = AtRulePrelude<'i, T::Prelude>; type AtRule = (SourcePosition, CssRule<'i, T::AtRule>); type Error = ParserError<'i>; @@ -316,12 +307,6 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a AtRulePrelude::Namespace(prefix, url) => { self.state = State::Namespaces; - if let Some(prefix) = &prefix { - self.namespace_prefixes.insert(prefix.into(), url.clone().into()); - } else { - self.default_namespace = Some(url.clone().into()); - } - CssRule::Namespace(NamespaceRule { prefix: prefix.map(|x| x.into()), url: url.into(), @@ -359,7 +344,9 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a } } -impl<'a, 'o, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> + for TopLevelRuleParser<'a, 'o, 'i, T> +{ type Prelude = SelectorList<'i>; type QualifiedRule = (SourcePosition, CssRule<'i, T::AtRule>); type Error = ParserError<'i>; @@ -385,22 +372,16 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for TopLevelRulePa } } -struct NestedRuleParser<'a, 'o, 'i, T> { - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, +pub struct NestedRuleParser<'a, 'o, 'i, T> { + pub options: &'a ParserOptions<'o, 'i, T>, } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> { - fn parse_nested_rules<'t>( +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> { + pub fn parse_nested_rules<'t>( &mut self, input: &mut Parser<'i, 't>, ) -> Result, ParseError<'i, ParserError<'i>>> { - let nested_parser = NestedRuleParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, - options: self.options, - }; + let nested_parser = NestedRuleParser { options: self.options }; let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser); let mut rules = Vec::new(); @@ -431,7 +412,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> { } } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser<'a, 'o, 'i, T> { type Prelude = AtRulePrelude<'i, T::Prelude>; type AtRule = CssRule<'i, T::AtRule>; type Error = ParserError<'i>; @@ -530,18 +511,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser< let condition = MediaCondition::parse(input, true)?; Ok(AtRulePrelude::Container(name, condition)) }, - _ => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - if let Ok(prelude) = at_rule_parser.parse_prelude(name.clone(), input) { - return Ok(AtRulePrelude::Custom(prelude)) - } - } - - self.options.warn(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))); - input.skip_whitespace(); - let tokens = TokenList::parse(input, &self.options, 0)?; - Ok(AtRulePrelude::Unknown(name.into(), tokens)) - } + _ => parse_custom_at_rule_prelude(&name, input, self.options) } } @@ -661,16 +631,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser< block: Some(TokenList::parse(input, &self.options, 0)?), loc, })), - AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - at_rule_parser - .parse_block(prelude, start, input) - .map(|prelude| CssRule::Custom(prelude)) - .map_err(|_| input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) - } else { - Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) - } - } + AtRulePrelude::Custom(prelude) => parse_custom_at_rule_body(prelude, input, start, self.options), } } @@ -695,21 +656,15 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser< block: None, loc, })), - AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - at_rule_parser - .rule_without_block(prelude, start) - .map(|prelude| CssRule::Custom(prelude)) - } else { - Err(()) - } - } + AtRulePrelude::Custom(prelude) => parse_custom_at_rule_without_block(prelude, start, self.options), _ => Err(()), } } } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> + for NestedRuleParser<'a, 'o, 'i, T> +{ type Prelude = SelectorList<'i>; type QualifiedRule = CssRule<'i, T::AtRule>; type Error = ParserError<'i>; @@ -719,8 +674,6 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRule input: &mut Parser<'i, 't>, ) -> Result> { let selector_parser = SelectorParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: false, options: &self.options, }; @@ -735,7 +688,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRule ) -> Result, ParseError<'i, Self::Error>> { let loc = self.loc(start); let (declarations, rules) = if self.options.nesting { - parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)? + parse_declarations_and_nested_rules(input, self.options)? } else { (DeclarationBlock::parse(input, self.options)?, CssRuleList(vec![])) }; @@ -749,18 +702,84 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRule } } -fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( +fn parse_custom_at_rule_prelude<'i, 't, T: crate::traits::AtRuleParser<'i>>( + name: &CowRcStr<'i>, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, T>, +) -> Result, ParseError<'i, ParserError<'i>>> { + if let Some(at_rule_parser) = &options.at_rule_parser { + match at_rule_parser.write().unwrap().parse_prelude(name.clone(), input, options) { + Ok(prelude) => return Ok(AtRulePrelude::Custom(prelude)), + Err(ParseError { + kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(..)), + .. + }) => {} + Err(err) => { + return Err(match &err.kind { + ParseErrorKind::Basic(kind) => ParseError { + kind: ParseErrorKind::Basic(kind.clone()), + location: err.location, + }, + _ => input.new_custom_error(ParserError::AtRulePreludeInvalid), + }) + } + } + } + + options.warn(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))); + input.skip_whitespace(); + let tokens = TokenList::parse(input, &options, 0)?; + Ok(AtRulePrelude::Unknown(name.into(), tokens)) +} + +fn parse_custom_at_rule_body<'i, 't, T: crate::traits::AtRuleParser<'i>>( + prelude: T::Prelude, + input: &mut Parser<'i, 't>, + start: &ParserState, + options: &ParserOptions<'_, 'i, T>, +) -> Result, ParseError<'i, ParserError<'i>>> { + if let Some(at_rule_parser) = &options.at_rule_parser { + at_rule_parser + .write() + .unwrap() + .parse_block(prelude, start, input, options) + .map(|prelude| CssRule::Custom(prelude)) + .map_err(|err| match &err.kind { + ParseErrorKind::Basic(kind) => ParseError { + kind: ParseErrorKind::Basic(kind.clone()), + location: err.location, + }, + _ => input.new_error(BasicParseErrorKind::AtRuleBodyInvalid), + }) + } else { + Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) + } +} + +fn parse_custom_at_rule_without_block<'i, 't, T: crate::traits::AtRuleParser<'i>>( + prelude: T::Prelude, + start: &ParserState, + options: &ParserOptions<'_, 'i, T>, +) -> Result, ()> { + if let Some(at_rule_parser) = &options.at_rule_parser { + at_rule_parser + .write() + .unwrap() + .rule_without_block(prelude, start, options) + .map(|prelude| CssRule::Custom(prelude)) + } else { + Err(()) + } +} + +fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>( input: &mut Parser<'i, 't>, - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i, T>, ) -> Result<(DeclarationBlock<'i>, CssRuleList<'i, T::AtRule>), ParseError<'i, ParserError<'i>>> { let mut important_declarations = DeclarationList::new(); let mut declarations = DeclarationList::new(); let mut rules = CssRuleList(vec![]); let mut parser = StyleRuleParser { - default_namespace, - namespace_prefixes, options, declarations: &mut declarations, important_declarations: &mut important_declarations, @@ -809,17 +828,17 @@ fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( )) } -pub struct StyleRuleParser<'a, 'o, 'i, T: AtRuleParser<'i>> { - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, +pub struct StyleRuleParser<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> { + options: &'a ParserOptions<'o, 'i, T>, declarations: &'a mut DeclarationList<'i>, important_declarations: &'a mut DeclarationList<'i>, rules: &'a mut CssRuleList<'i, T::AtRule>, } /// Parse a declaration within {} block: `color: blue` -impl<'a, 'o, 'i, T: AtRuleParser<'i>> cssparser::DeclarationParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> cssparser::DeclarationParser<'i> + for StyleRuleParser<'a, 'o, 'i, T> +{ type Declaration = (); type Error = ParserError<'i>; @@ -838,7 +857,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> cssparser::DeclarationParser<'i> for Style } } -impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { type Prelude = AtRulePrelude<'i, T::Prelude>; type AtRule = (); type Error = ParserError<'i>; @@ -870,26 +889,13 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' "nest" => { self.options.warn(input.new_custom_error(ParserError::DeprecatedNestRule)); let selector_parser = SelectorParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: true, options: &self.options, }; let selectors = SelectorList::parse(&selector_parser, input, NestingRequirement::Contained)?; Ok(AtRulePrelude::Nest(selectors)) }, - _ => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - if let Ok(prelude) = at_rule_parser.parse_prelude(name.clone(), input) { - return Ok(AtRulePrelude::Custom(prelude)) - } - } - - self.options.warn(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))); - input.skip_whitespace(); - let tokens = TokenList::parse(input, &self.options, 0)?; - Ok(AtRulePrelude::Unknown(name.into(), tokens)) - } + _ => parse_custom_at_rule_prelude(&name, input, self.options) } } @@ -909,13 +915,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' AtRulePrelude::Media(query) => { self.rules.0.push(CssRule::Media(MediaRule { query, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options)?, loc, })); Ok(()) @@ -923,13 +923,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' AtRulePrelude::Supports(condition) => { self.rules.0.push(CssRule::Supports(SupportsRule { condition, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options)?, loc, })); Ok(()) @@ -938,13 +932,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' self.rules.0.push(CssRule::Container(ContainerRule { name, condition, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options)?, loc, })); Ok(()) @@ -952,24 +940,13 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' AtRulePrelude::LayerBlock(name) => { self.rules.0.push(CssRule::LayerBlock(LayerBlockRule { name, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options)?, loc, })); Ok(()) } AtRulePrelude::Nest(selectors) => { - let (declarations, rules) = parse_declarations_and_nested_rules( - input, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.options)?; self.rules.0.push(CssRule::Nesting(NestingRule { style: StyleRule { selectors, @@ -992,15 +969,11 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' Ok(()) } AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - let rule = at_rule_parser - .parse_block(prelude, start, input) - .map_err(|_| input.new_error(BasicParseErrorKind::AtRuleBodyInvalid))?; - self.rules.0.push(CssRule::Custom(rule)); - Ok(()) - } else { - Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) - } + self + .rules + .0 + .push(parse_custom_at_rule_body(prelude, input, start, self.options)?); + Ok(()) } _ => Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)), } @@ -1023,38 +996,31 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' Ok(()) } AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - let rule = at_rule_parser.rule_without_block(prelude, start)?; - self.rules.0.push(CssRule::Custom(rule)); - Ok(()) - } else { - Err(()) - } + self + .rules + .0 + .push(parse_custom_at_rule_without_block(prelude, start, self.options)?); + Ok(()) } _ => Err(()), } } } -#[inline] -fn parse_nested_at_rule<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( +pub fn parse_nested_at_rule<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>( input: &mut Parser<'i, 't>, - source_index: u32, - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i, T>, ) -> Result, ParseError<'i, ParserError<'i>>> { let loc = input.current_source_location(); let loc = Location { - source_index, + source_index: options.source_index, line: loc.line, column: loc.column, }; // 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, options)?; + let (declarations, mut rules) = parse_declarations_and_nested_rules(input, options)?; if declarations.len() > 0 { rules.0.insert( @@ -1072,7 +1038,9 @@ fn parse_nested_at_rule<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( Ok(rules) } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> + for StyleRuleParser<'a, 'o, 'i, T> +{ type Prelude = SelectorList<'i>; type QualifiedRule = (); type Error = ParserError<'i>; @@ -1095,8 +1063,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for StyleRuleP input: &mut Parser<'i, 't>, ) -> Result<(), ParseError<'i, Self::Error>> { let loc = start.source_location(); - let (declarations, rules) = - parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.options)?; self.rules.0.push(CssRule::Style(StyleRule { selectors, vendor_prefix: VendorPrefix::empty(), diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 8404624a..7a2f103a 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -13,10 +13,10 @@ use crate::parser::{DefaultAtRuleParser, TopLevelRuleParser}; use crate::printer::Printer; use crate::rules::{CssRule, CssRuleList, MinifyContext}; use crate::targets::Browsers; -use crate::traits::ToCss; +use crate::traits::{AtRuleParser, ToCss}; #[cfg(feature = "visitor")] use crate::visitor::{Visit, VisitTypes, Visitor}; -use cssparser::{AtRuleParser, Parser, ParserInput, RuleListParser}; +use cssparser::{Parser, ParserInput, RuleListParser}; #[cfg(feature = "sourcemap")] use parcel_sourcemap::SourceMap; use std::collections::{HashMap, HashSet}; diff --git a/src/traits.rs b/src/traits.rs index f96d83be..8ac879d7 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -32,13 +32,13 @@ pub trait ParseWithOptions<'i, T>: Sized { /// Parse a value of this type with the given options. fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result>>; /// Parse a value from a string with the given options. fn parse_string_with_options( input: &'i str, - options: ParserOptions, + options: ParserOptions<'_, 'i, T>, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); @@ -234,3 +234,94 @@ pub trait Zero { /// Returns whether the value is zero. fn is_zero(&self) -> bool; } + +/// A trait to provide parsing of custom at-rules. +/// +/// For example, there could be different implementations for top-level at-rules +/// (`@media`, `@font-face`, …) +/// and for page-margin rules inside `@page`. +/// +/// Default implementations that reject all at-rules are provided, +/// so that `impl AtRuleParser<(), ()> for ... {}` can be used +/// for using `DeclarationListParser` to parse a declarations list with only qualified rules. +/// +/// Note: this trait is copied from cssparser and modified to provide parser options. +pub trait AtRuleParser<'i>: Sized { + /// The intermediate representation of prelude of an at-rule. + type Prelude; + + /// The finished representation of an at-rule. + type AtRule; + + /// The error type that is included in the ParseError value that can be returned. + type Error: 'i; + + /// Parse the prelude of an at-rule with the given `name`. + /// + /// Return the representation of the prelude and the type of at-rule, + /// or `Err(())` to ignore the entire at-rule as invalid. + /// + /// The prelude is the part after the at-keyword + /// and before the `;` semicolon or `{ /* ... */ }` block. + /// + /// At-rule name matching should be case-insensitive in the ASCII range. + /// This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + /// or with the `match_ignore_ascii_case!` macro. + /// + /// The given `input` is a "delimited" parser + /// that ends wherever the prelude should end. + /// (Before the next semicolon, the next `{`, or the end of the current block.) + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, Self>, + ) -> Result> { + let _ = name; + let _ = input; + let _ = options; + Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) + } + + /// End an at-rule which doesn't have block. Return the finished + /// representation of the at-rule. + /// + /// The location passed in is source location of the start of the prelude. + /// + /// This is only called when either the `;` semicolon indeed follows the prelude, + /// or parser is at the end of the input. + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + options: &ParserOptions<'_, 'i, Self>, + ) -> Result { + let _ = prelude; + let _ = start; + let _ = options; + Err(()) + } + + /// Parse the content of a `{ /* ... */ }` block for the body of the at-rule. + /// + /// The location passed in is source location of the start of the prelude. + /// + /// Return the finished representation of the at-rule + /// as returned by `RuleListParser::next` or `DeclarationListParser::next`, + /// or `Err(())` to ignore the entire at-rule as invalid. + /// + /// This is only called when a block was found following the prelude. + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, Self>, + ) -> Result> { + let _ = prelude; + let _ = start; + let _ = input; + let _ = options; + Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) + } +} diff --git a/tests/test_custom_parser.rs b/tests/test_custom_parser.rs index 9e5e8868..644c4150 100644 --- a/tests/test_custom_parser.rs +++ b/tests/test_custom_parser.rs @@ -1,10 +1,12 @@ +use std::sync::{Arc, RwLock}; + use cssparser::*; use lightningcss::{ declaration::DeclarationBlock, error::{ParserError, PrinterError}, printer::Printer, stylesheet::{ParserOptions, PrinterOptions, StyleSheet}, - traits::{Parse, ToCss}, + traits::{AtRuleParser, Parse, ToCss}, values::ident::Ident, }; @@ -12,7 +14,7 @@ fn minify_test(source: &str, expected: &str) { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - at_rule_parser: Some(TestAtRuleParser), + at_rule_parser: Some(Arc::new(RwLock::new(TestAtRuleParser))), ..Default::default() }, ) @@ -85,6 +87,7 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i, Self>, ) -> Result> { let location = input.current_source_location(); match_ignore_ascii_case! {&*name, @@ -102,7 +105,12 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { } } - fn rule_without_block(&mut self, prelude: Self::Prelude, _start: &ParserState) -> Result { + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + _start: &ParserState, + _options: &ParserOptions<'_, 'i, Self>, + ) -> Result { match prelude { Prelude::Inline(name) => Ok(AtRule::Inline(InlineRule { name })), _ => unreachable!(), @@ -114,6 +122,7 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { prelude: Self::Prelude, _start: &ParserState, input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i, Self>, ) -> Result> { match prelude { Prelude::Block(name) => Ok(AtRule::Block(BlockRule { From 95cb8355aae8a32323f327c34100156cdfca0be9 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 11 Jan 2023 21:58:13 -0500 Subject: [PATCH 03/12] Implement support for parsing custom at rules in JS bindings --- node/index.d.ts | 56 ++++++++++- node/src/at_rule_parser.rs | 195 +++++++++++++++++++++++++++++++++++++ node/src/lib.rs | 37 +++++-- node/src/transformer.rs | 18 +++- src/values/syntax.rs | 5 + test.js | 125 +++++++----------------- 6 files changed, 326 insertions(+), 110 deletions(-) create mode 100644 node/src/at_rule_parser.rs diff --git a/node/index.d.ts b/node/index.d.ts index 583966e9..fc3c4ef5 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -1,4 +1,4 @@ -import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock } from './ast'; +import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock, ParsedComponent } from './ast'; import type { Targets } from './targets'; export * from './ast'; @@ -55,7 +55,14 @@ export interface TransformOptions { * For optimal performance, visitors should be as specific as possible about what types of values * they care about so that JavaScript has to be called as little as possible. */ - visitor?: Visitor + visitor?: Visitor, + /** + * Defines how to parse custom CSS at-rules. Each at-rule can have a prelude, defined using a CSS + * [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), and + * a block body. The body can be a declaration list, rule list, or style block as defined in the + * [css spec](https://drafts.csswg.org/css-syntax/#declaration-rule-list). + */ + customAtRules?: CustomAtRules } // This is a hack to make TS still provide autocomplete for `property` vs. just making it `string`. @@ -79,12 +86,27 @@ type MappedRuleVisitors = { [Name in Exclude]?: RuleVisitor>>; } -type UnknownVisitors = { - [name: string]: RuleVisitor +type UnknownVisitors = { + [name: string]: RuleVisitor } type RuleVisitors = MappedRuleVisitors & { - unknown?: UnknownVisitors | RuleVisitor + unknown?: UnknownVisitors | RuleVisitor, + custom?: UnknownVisitors | RuleVisitor +}; + +interface CustomAtRule { + name: string, + prelude: ParsedComponent | null, + body: CustomAtRuleBody +} + +type CustomAtRuleBody = { + type: 'declaration-list', + value: DeclarationBlock +} | { + type: 'rule-list', + value: Rule[] }; type FindProperty = Union extends { property: Name } ? Union : never; @@ -143,6 +165,30 @@ export interface Visitor { EnvironmentVariableExit?: EnvironmentVariableVisitor | EnvironmentVariableVisitors; } +export interface CustomAtRules { + [name: string]: CustomAtRule +} + +export interface CustomAtRule { + /** + * Defines the syntax for a custom at-rule prelude. The value should be a + * CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings) + * representing the types of values that are accepted. This property may be omitted or + * set to null to indicate that no prelude is accepted. + */ + prelude?: string | null, + /** + * Defines the type of body contained within the at-rule block. + * - declaration-list: A CSS declaration list, as in a style rule. + * - rule-list: A list of CSS rules, as supported within a non-nested + * at-rule such as `@media` or `@supports`. + * - style-block: Both a declaration list and rule list, as accepted within + * a nested at-rule within a style rule (e.g. `@media` inside a style rule + * with directly nested declarations). + */ + body?: 'declaration-list' | 'rule-list' | 'style-block' | null +} + export interface DependencyOptions { /** Whether to preserve `@import` rules rather than removing them. */ preserveImports?: boolean diff --git a/node/src/at_rule_parser.rs b/node/src/at_rule_parser.rs new file mode 100644 index 00000000..82597fab --- /dev/null +++ b/node/src/at_rule_parser.rs @@ -0,0 +1,195 @@ +use std::collections::HashMap; + +use cssparser::*; +use lightningcss::{ + declaration::DeclarationBlock, + error::ParserError, + rules::CssRuleList, + stylesheet::ParserOptions, + traits::{AtRuleParser, ToCss}, + values::{ + string::CowArcStr, + syntax::{ParsedComponent, SyntaxString}, + }, + visitor::{Visit, VisitTypes, Visitor}, +}; +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Deserialize, Debug, Clone)] +pub struct CustomAtRuleConfig { + #[serde(default, deserialize_with = "deserialize_prelude")] + prelude: Option, + body: Option, +} + +fn deserialize_prelude<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = Option::>::deserialize(deserializer)?; + if let Some(s) = s { + Ok(Some( + SyntaxString::parse_string(&s).map_err(|_| serde::de::Error::custom("invalid syntax string"))?, + )) + } else { + Ok(None) + } +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +enum CustomAtRuleBodyType { + DeclarationList, + RuleList, + StyleBlock, +} + +pub struct Prelude<'i> { + name: CowArcStr<'i>, + prelude: Option>, +} + +#[derive(Serialize, Deserialize)] +pub struct AtRule<'i> { + #[serde(borrow)] + pub name: CowArcStr<'i>, + pub prelude: Option>, + pub body: Option>, +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type", content = "value", rename_all = "kebab-case")] +pub enum AtRuleBody<'i> { + #[serde(borrow)] + DeclarationList(DeclarationBlock<'i>), + RuleList(CssRuleList<'i, AtRule<'i>>), +} + +#[derive(Clone)] +pub struct CustomAtRuleParser { + pub configs: HashMap, +} + +impl<'i> AtRuleParser<'i> for CustomAtRuleParser { + type Prelude = Prelude<'i>; + type Error = ParserError<'i>; + type AtRule = AtRule<'i>; + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i, Self>, + ) -> Result> { + if let Some(config) = self.configs.get(name.as_ref()) { + let prelude = if let Some(prelude) = &config.prelude { + Some(prelude.parse_value(input)?) + } else { + None + }; + Ok(Prelude { + name: name.into(), + prelude, + }) + } else { + Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) + } + } + + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + _start: &ParserState, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, Self>, + ) -> Result> { + let config = self.configs.get(prelude.name.as_ref()).unwrap(); + let body = if let Some(body) = &config.body { + match body { + CustomAtRuleBodyType::DeclarationList => { + Some(AtRuleBody::DeclarationList(DeclarationBlock::parse(input, options)?)) + } + CustomAtRuleBodyType::RuleList => Some(AtRuleBody::RuleList(CssRuleList::parse(input, options)?)), + CustomAtRuleBodyType::StyleBlock => { + Some(AtRuleBody::RuleList(CssRuleList::parse_style_block(input, options)?)) + } + } + } else { + None + }; + + Ok(AtRule { + name: prelude.name, + prelude: prelude.prelude, + body, + }) + } + + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + _start: &ParserState, + _options: &ParserOptions<'_, 'i, Self>, + ) -> Result { + let config = self.configs.get(prelude.name.as_ref()).unwrap(); + if config.body.is_some() { + return Err(()); + } + + Ok(AtRule { + name: prelude.name, + prelude: prelude.prelude, + body: None, + }) + } +} + +impl<'i> ToCss for AtRule<'i> { + fn to_css( + &self, + dest: &mut lightningcss::printer::Printer, + ) -> Result<(), lightningcss::error::PrinterError> + where + W: std::fmt::Write, + { + dest.write_char('@')?; + serialize_identifier(&self.name, dest)?; + if let Some(prelude) = &self.prelude { + dest.write_char(' ')?; + prelude.to_css(dest)?; + } + + if let Some(body) = &self.body { + match body { + AtRuleBody::DeclarationList(decls) => { + decls.to_css_block(dest)?; + } + AtRuleBody::RuleList(rules) => { + dest.whitespace()?; + dest.write_char('{')?; + dest.indent(); + dest.newline()?; + rules.to_css(dest)?; + dest.dedent(); + dest.newline()?; + dest.write_char('}')?; + } + } + } + + Ok(()) + } +} + +impl<'i, V: Visitor<'i, AtRule<'i>>> Visit<'i, AtRule<'i>, V> for AtRule<'i> { + const CHILD_TYPES: VisitTypes = VisitTypes::empty(); + + fn visit_children(&mut self, visitor: &mut V) { + self.prelude.visit(visitor); + match &mut self.body { + Some(AtRuleBody::DeclarationList(decls)) => decls.visit(visitor), + Some(AtRuleBody::RuleList(rules)) => rules.visit(visitor), + None => {} + } + } +} diff --git a/node/src/lib.rs b/node/src/lib.rs index f9c76e82..3c8d2809 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -2,6 +2,7 @@ #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; +use at_rule_parser::{CustomAtRuleConfig, CustomAtRuleParser}; use lightningcss::bundler::{BundleErrorKind, Bundler, FileProvider, SourceProvider}; use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError}; use lightningcss::dependencies::{Dependency, DependencyOptions}; @@ -13,13 +14,14 @@ use lightningcss::targets::Browsers; use lightningcss::visitor::Visit; use parcel_sourcemap::SourceMap; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::{Arc, Mutex, RwLock}; use transformer::JsVisitor; +mod at_rule_parser; #[cfg(not(target_arch = "wasm32"))] mod threadsafe_function; mod transformer; @@ -142,7 +144,7 @@ mod bundle { &fs, &config, visitor.as_mut().map(|visitor| { - |stylesheet: &mut StyleSheet| { + |stylesheet: &mut StyleSheet| { stylesheet.visit(visitor); if let Some(err) = visitor.errors.first() { return Err(err.clone()); @@ -260,7 +262,7 @@ mod bundle { } struct VisitMessage { - stylesheet: &'static mut StyleSheet<'static, 'static>, + stylesheet: &'static mut StyleSheet<'static, 'static, CustomAtRuleParser>, tx: Sender>, } @@ -415,15 +417,16 @@ mod bundle { unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) }, &config, tsfn.map(move |tsfn| { - move |stylesheet: &mut StyleSheet| { + move |stylesheet: &mut StyleSheet| { CHANNEL.with(|channel| { let message = VisitMessage { // SAFETY: we immediately lock the thread until we get a response, // so stylesheet cannot be dropped in that time. stylesheet: unsafe { - std::mem::transmute::<&'_ mut StyleSheet<'_, '_>, &'static mut StyleSheet<'static, 'static>>( - stylesheet, - ) + std::mem::transmute::< + &'_ mut StyleSheet<'_, '_, CustomAtRuleParser>, + &'static mut StyleSheet<'static, 'static, CustomAtRuleParser>, + >(stylesheet) }, tx: channel.0.clone(), }; @@ -509,6 +512,7 @@ struct Config { pub pseudo_classes: Option, pub unused_symbols: Option>, pub error_recovery: Option, + pub custom_at_rules: Option>, } #[derive(Debug, Deserialize)] @@ -633,7 +637,13 @@ fn compile<'i>( source_index: 0, error_recovery: config.error_recovery.unwrap_or_default(), warnings: warnings.clone(), - at_rule_parser: ParserOptions::default_at_rule_parser(), + at_rule_parser: if let Some(custom_at_rules) = &config.custom_at_rules { + Some(Arc::new(RwLock::new(CustomAtRuleParser { + configs: custom_at_rules.clone(), + }))) + } else { + None + }, }, )?; @@ -696,7 +706,12 @@ fn compile<'i>( }) } -fn compile_bundle<'i, 'o, P: SourceProvider, F: FnOnce(&mut StyleSheet<'i, 'o>) -> napi::Result<()>>( +fn compile_bundle< + 'i, + 'o, + P: SourceProvider, + F: FnOnce(&mut StyleSheet<'i, 'o, CustomAtRuleParser>) -> napi::Result<()>, +>( fs: &'i P, config: &'o BundleConfig, visit: Option, @@ -734,7 +749,9 @@ fn compile_bundle<'i, 'o, P: SourceProvider, F: FnOnce(&mut StyleSheet<'i, 'o>) }, error_recovery: config.error_recovery.unwrap_or_default(), warnings: warnings.clone(), - ..ParserOptions::default() + at_rule_parser: None, + filename: String::new(), + source_index: 0, }; let mut bundler = Bundler::new(fs, source_map.as_mut(), parser_options); diff --git a/node/src/transformer.rs b/node/src/transformer.rs index 27e82b82..73236db5 100644 --- a/node/src/transformer.rs +++ b/node/src/transformer.rs @@ -17,6 +17,8 @@ use napi::{Env, JsFunction, JsObject, JsUnknown, Ref, ValueType}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; +use crate::at_rule_parser::AtRule; + pub struct JsVisitor { env: Env, visit_rule: VisitorsRef, @@ -251,14 +253,14 @@ macro_rules! unwrap { }; } -impl<'i> Visitor<'i> for JsVisitor { +impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { const TYPES: lightningcss::visitor::VisitTypes = VisitTypes::all(); fn visit_types(&self) -> VisitTypes { self.types } - fn visit_rule_list(&mut self, rules: &mut lightningcss::rules::CssRuleList<'i>) { + fn visit_rule_list(&mut self, rules: &mut lightningcss::rules::CssRuleList<'i, AtRule<'i>>) { if self.types.contains(VisitTypes::RULES) { let env = self.env; let rule_map = self.rule_map.get::(&env); @@ -298,7 +300,17 @@ impl<'i> Visitor<'i> for JsVisitor { "unknown" } } - CssRule::Ignored | CssRule::Custom(..) => return Ok(None), + CssRule::Custom(c) => { + let name = c.name.as_ref(); + if let Some(visit) = rule_map.custom(stage, "custom", name) { + let js_value = env.to_js_value(c)?; + let res = visit.call(None, &[js_value])?; + return env.from_js_value(res).map(serde_detach::detach); + } else { + "custom" + } + } + CssRule::Ignored => return Ok(None), }; if let Some(visit) = rule_map.named(stage, name).as_ref().or(visit_rule.for_stage(stage)) { diff --git a/src/values/syntax.rs b/src/values/syntax.rs index a4df102f..5dede895 100644 --- a/src/values/syntax.rs +++ b/src/values/syntax.rs @@ -6,6 +6,8 @@ use crate::error::{ParserError, PrinterError}; use crate::printer::Printer; use crate::traits::{Parse, ToCss}; use crate::values; +#[cfg(feature = "visitor")] +use crate::visitor::Visit; use cssparser::*; /// A CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings) @@ -83,6 +85,7 @@ pub enum SyntaxComponentKind { /// A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a /// [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated. #[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), @@ -100,6 +103,7 @@ pub enum Multiplier { /// A parsed value for a [SyntaxComponent](SyntaxComponent). #[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr( feature = "serde", @@ -142,6 +146,7 @@ pub enum ParsedComponent<'i> { /// A repeated component value. Repeated { /// The components to repeat. + #[cfg_attr(feature = "visitor", skip_type)] components: Vec>, /// A multiplier describing how the components repeat. multiplier: Multiplier, diff --git a/test.js b/test.js index bcfbada3..cdb2d1c0 100644 --- a/test.js +++ b/test.js @@ -30,108 +30,49 @@ if (process.argv[process.argv.length - 1] !== __filename) { let res = css.transform({ filename: __filename, - // minify: true, - // targets: { - // safari: 4 << 16, - // firefox: 3 << 16 | 5 << 8, - // opera: 10 << 16 | 5 << 8 - // }, code: Buffer.from(` - @namespace "http://foo.com"; - @namespace svg "http://bar.com"; - - .selector > .nested[data-foo=bar]:not(.foo):hover::part(tab active) { - width: 32px; - --foo: var(--bar, 30px); - background: linear-gradient(red, green); - } - - svg|foo { - test: foo; + @breakpoints { + .foo { color: yellow; } } - @media (hover) and (width > 50px) { - .foo { - color: red; - background: inline('.gitignore'); + .foo { + color: red; + @bar { + width: 25px; } } `), - visitor: { - visitLength(length) { - if (length.unit === 'px') { - return { - unit: 'rem', - value: length.value / 16 - } - } - return length; - }, - visitColor(color) { - console.log(color); - return color; - }, - visitImage(image) { - // console.log(image.value.value); - image.value.value[1].push('moz'); - return image; - }, - visitProperty(property) { - // console.log(require('util').inspect(property, {depth: 50})) - if (property.property === 'background') { - property.value[0].repeat.x = 'no-repeat'; - property.value[0].repeat.y = 'no-repeat'; - } - - return property; - }, - // visitRule(rule) { - // console.log(require('util').inspect(rule, {depth: 10})); - // if (rule.type === 'style') { - // for (let selector of rule.value.selectors) { - // for (let component of selector) { - // if (component.type === 'class') { - // component.value = 'tw-' + component.value; - // } - // } - // } - // } - // return rule; - // }, - visitMediaQuery(query) { - // console.log(query); - query.media_type = 'print'; - return query; + drafts: { + nesting: true + }, + targets: { + safari: 16 << 16 + }, + customAtRules: { + breakpoints: { + // Syntax string defining the at rule prelude. + // https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings + prelude: null, + // Type of the at rule block. + // Can be declaration-list, rule-list, or style-block. + // https://www.w3.org/TR/css-syntax-3/#declaration-rule-list + body: 'rule-list' }, - visitFunction(fn) { - // console.log(require('util').inspect(fn, {depth: 50})); - if (fn.name === 'inline') { - return { - name: 'url', - arguments: [{ - type: 'token', - value: { - type: 'string', - value: fs.readFileSync(fn.arguments[0].value.value).toString('base64'), - } - }] - } + bar: { + body: 'style-block' + } + }, + visitor: { + Rule: { + custom(rule) { + console.log(rule.body); } - return fn; }, - visitSelector(selector) { - console.log(require('util').inspect(selector, { depth: 10 })); - for (let component of selector) { - if (component.type === 'class') { - component.name = 'tw-' + component.name; - } else if (component.type === 'attribute') { - component.name = 'tw-' + component.name; - component.operation.operator = 'includes'; - } - } - return selector; + Length(length) { + length.value *= 2; + return length; } - }, + } }); console.log(res.code.toString()); From cd0cd88cca59eb95788bc67cc7cd2c53f96ee729 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 11 Jan 2023 21:29:18 -0500 Subject: [PATCH 04/12] Support standalone selector and rule parsing and printing --- node/ast.d.ts | 12 +- selectors/parser.rs | 130 +++--- selectors/serialization.rs | 17 +- src/declaration.rs | 2 +- src/lib.rs | 2 +- src/parser.rs | 2 - src/printer.rs | 28 +- src/properties/custom.rs | 18 +- src/properties/mod.rs | 4 +- src/rules/container.rs | 11 +- src/rules/layer.rs | 12 +- src/rules/media.rs | 13 +- src/rules/mod.rs | 97 ++--- src/rules/nesting.rs | 13 +- src/rules/style.rs | 30 +- src/rules/supports.rs | 11 +- src/selector.rs | 785 +++++++++++++++++++------------------ 17 files changed, 577 insertions(+), 610 deletions(-) diff --git a/node/ast.d.ts b/node/ast.d.ts index b6b4be69..871addbd 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -6039,22 +6039,16 @@ export type SelectorComponent = | ( | { type: "namespace"; - value: "none"; + kind: "none"; } | { type: "namespace"; - value: "any"; - } - | { - type: "namespace"; - url: string; - value: "default"; + kind: "any"; } | { type: "namespace"; + kind: "named"; prefix: string; - url: string; - value: "some"; } ) | { diff --git a/selectors/parser.rs b/selectors/parser.rs index 5796a070..9b66523b 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -2009,21 +2009,19 @@ where }), QNamePrefix::ExplicitNoNamespace => sink.push(Component::ExplicitNoNamespace), QNamePrefix::ExplicitAnyNamespace => { - match parser.default_namespace() { - // Element type selectors that have no namespace - // component (no namespace separator) represent elements - // without regard to the element's namespace (equivalent - // to "*|") unless a default namespace has been declared - // for namespaced selectors (e.g. in CSS, in the style - // sheet). If a default namespace has been declared, - // such selectors will represent only elements in the - // default namespace. - // -- Selectors § 6.1.1 - // So we'll have this act the same as the - // QNamePrefix::ImplicitAnyNamespace case. - None => {} - Some(_) => sink.push(Component::ExplicitAnyNamespace), - } + // Element type selectors that have no namespace + // component (no namespace separator) represent elements + // without regard to the element's namespace (equivalent + // to "*|") unless a default namespace has been declared + // for namespaced selectors (e.g. in CSS, in the style + // sheet). If a default namespace has been declared, + // such selectors will represent only elements in the + // default namespace. + // -- Selectors § 6.1.1 + // So we'll have this act the same as the + // QNamePrefix::ImplicitAnyNamespace case. + // For lightning css this logic was removed, should be handled when matching. + sink.push(Component::ExplicitAnyNamespace) } QNamePrefix::ImplicitNoNamespace => { unreachable!() // Not returned with in_attr_selector = false @@ -3117,38 +3115,38 @@ pub mod tests { ); // When the default namespace is not set, *| should be elided. // https://github.com/servo/servo/pull/17537 - assert_eq!( - parse_expected("*|e", Some("e")), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![Component::LocalName(LocalName { - name: DummyAtom::from("e"), - lower_name: DummyAtom::from("e"), - })], - specificity(0, 0, 1), - Default::default(), - )])) - ); + // assert_eq!( + // parse_expected("*|e", Some("e")), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![Component::LocalName(LocalName { + // name: DummyAtom::from("e"), + // lower_name: DummyAtom::from("e"), + // })], + // specificity(0, 0, 1), + // Default::default(), + // )])) + // ); // When the default namespace is set, *| should _not_ be elided (as foo // is no longer equivalent to *|foo--the former is only for foo in the // default namespace). // https://github.com/servo/servo/issues/16020 - assert_eq!( - parse_ns( - "*|e", - &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) - ), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![ - Component::ExplicitAnyNamespace, - Component::LocalName(LocalName { - name: DummyAtom::from("e"), - lower_name: DummyAtom::from("e"), - }), - ], - specificity(0, 0, 1), - Default::default(), - )])) - ); + // assert_eq!( + // parse_ns( + // "*|e", + // &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) + // ), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![ + // Component::ExplicitAnyNamespace, + // Component::LocalName(LocalName { + // name: DummyAtom::from("e"), + // lower_name: DummyAtom::from("e"), + // }), + // ], + // specificity(0, 0, 1), + // Default::default(), + // )])) + // ); assert_eq!( parse("*"), Ok(SelectorList::from_vec(vec![Selector::from_vec( @@ -3165,14 +3163,14 @@ pub mod tests { Default::default(), )])) ); - assert_eq!( - parse_expected("*|*", Some("*")), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![Component::ExplicitUniversalType], - specificity(0, 0, 0), - Default::default(), - )])) - ); + // assert_eq!( + // parse_expected("*|*", Some("*")), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![Component::ExplicitUniversalType], + // specificity(0, 0, 0), + // Default::default(), + // )])) + // ); assert_eq!( parse_ns( "*|*", @@ -3517,21 +3515,21 @@ pub mod tests { ); // *| should be elided if there is no default namespace. // https://github.com/servo/servo/pull/17537 - assert_eq!( - parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![Component::Negation( - vec![Selector::from_vec( - vec![Component::ExplicitUniversalType], - specificity(0, 0, 0), - Default::default() - )] - .into_boxed_slice() - )], - specificity(0, 0, 0), - Default::default(), - )])) - ); + // assert_eq!( + // parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![Component::Negation( + // vec![Selector::from_vec( + // vec![Component::ExplicitUniversalType], + // specificity(0, 0, 0), + // Default::default() + // )] + // .into_boxed_slice() + // )], + // specificity(0, 0, 0), + // Default::default(), + // )])) + // ); assert!(parse("::slotted()").is_err()); assert!(parse("::slotted(div)").is_ok()); diff --git a/selectors/serialization.rs b/selectors/serialization.rs index 0696d68b..dff9ceff 100644 --- a/selectors/serialization.rs +++ b/selectors/serialization.rs @@ -9,6 +9,7 @@ use crate::{ }; use std::borrow::Cow; +use cssparser::CowRcStr; #[cfg(feature = "jsonschema")] use schemars::JsonSchema; @@ -61,12 +62,11 @@ enum SerializedComponent<'i, 's, Impl: SelectorImpl<'s>, PseudoClass, PseudoElem #[derive(serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] -#[serde(tag = "value", rename_all = "kebab-case")] +#[serde(tag = "kind", rename_all = "kebab-case")] enum Namespace<'i> { None, Any, - Default { url: Cow<'i, str> }, - Some { prefix: Cow<'i, str>, url: Cow<'i, str> }, + Named { prefix: Cow<'i, str> }, } #[derive(serde::Serialize, serde::Deserialize)] @@ -288,12 +288,10 @@ where Component::ExplicitUniversalType => SerializedComponent::Universal, Component::ExplicitAnyNamespace => SerializedComponent::Namespace(Namespace::Any), Component::ExplicitNoNamespace => SerializedComponent::Namespace(Namespace::None), - Component::DefaultNamespace(url) => SerializedComponent::Namespace(Namespace::Default { - url: url.as_ref().into(), - }), - Component::Namespace(prefix, url) => SerializedComponent::Namespace(Namespace::Some { + // can't actually happen anymore. + Component::DefaultNamespace(_url) => SerializedComponent::Namespace(Namespace::Any), + Component::Namespace(prefix, _url) => SerializedComponent::Namespace(Namespace::Named { prefix: prefix.as_ref().into(), - url: url.as_ref().into(), }), Component::LocalName(name) => SerializedComponent::Type { name: name.name.as_ref().into(), @@ -440,8 +438,7 @@ where SerializedComponent::Namespace(n) => match n { Namespace::Any => Component::ExplicitAnyNamespace, Namespace::None => Component::ExplicitNoNamespace, - Namespace::Default { url } => Component::DefaultNamespace(url.into()), - Namespace::Some { prefix, url } => Component::Namespace(prefix.into(), url.into()), + Namespace::Named { prefix } => Component::Namespace(prefix.into(), CowRcStr::from("").into()), }, SerializedComponent::Type { name } => { let name: Impl::LocalName = name.into(); diff --git a/src/declaration.rs b/src/declaration.rs index c596c443..a9afde50 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -441,7 +441,7 @@ pub(crate) fn parse_declaration<'i, 't, T>( input: &mut cssparser::Parser<'i, 't>, declarations: &mut DeclarationList<'i>, important_declarations: &mut DeclarationList<'i>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result<(), cssparser::ParseError<'i, ParserError<'i>>> { let property = input.parse_until_before(Delimiter::Bang, |input| { Property::parse(PropertyId::from(CowArcStr::from(name)), input, options) diff --git a/src/lib.rs b/src/lib.rs index 64f2a99e..62134822 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5644,7 +5644,7 @@ mod tests { minify_test("a:is(.foo) { color: yellow }", "a.foo{color:#ff0}"); minify_test("a:is([data-test]) { color: yellow }", "a[data-test]{color:#ff0}"); minify_test(".foo:is(a) { color: yellow }", ".foo:is(a){color:#ff0}"); - minify_test(".foo:is(*|a) { color: yellow }", ".foo:is(a){color:#ff0}"); + minify_test(".foo:is(*|a) { color: yellow }", ".foo:is(*|a){color:#ff0}"); minify_test(".foo:is(*) { color: yellow }", ".foo:is(*){color:#ff0}"); minify_test( "@namespace svg url(http://www.w3.org/2000/svg); .foo:is(svg|a) { color: yellow }", diff --git a/src/parser.rs b/src/parser.rs index 88f9d4ca..81979b9c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1085,8 +1085,6 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for StyleRuleP input: &mut Parser<'i, 't>, ) -> Result> { let selector_parser = SelectorParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: true, options: &self.options, }; diff --git a/src/printer.rs b/src/printer.rs index 424b585e..4c19b4de 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -3,7 +3,8 @@ use crate::css_modules::CssModule; use crate::dependencies::{Dependency, DependencyOptions}; use crate::error::{Error, ErrorLocation, PrinterError, PrinterErrorKind}; -use crate::rules::Location; +use crate::rules::{Location, StyleContext}; +use crate::selector::SelectorList; use crate::targets::Browsers; use crate::vendor_prefix::VendorPrefix; use cssparser::{serialize_identifier, serialize_name}; @@ -85,6 +86,7 @@ pub struct Printer<'a, 'b, 'c, W> { pub(crate) dependencies: Option>, pub(crate) remove_imports: bool, pub(crate) pseudo_classes: Option>, + context: Option<&'a StyleContext<'a, 'b>>, } impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { @@ -117,6 +119,7 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { }, remove_imports: matches!(&options.analyze_dependencies, Some(d) if d.remove_imports), pseudo_classes: options.pseudo_classes, + context: None, } } @@ -330,6 +333,29 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { }), } } + + pub(crate) fn with_context) -> Result>( + &mut self, + selectors: &SelectorList, + f: F, + ) -> Result { + let parent = std::mem::take(&mut self.context); + let ctx = StyleContext { + selectors: unsafe { std::mem::transmute(selectors) }, + parent, + }; + + // I can't figure out what lifetime to use here to convince the compiler that + // the reference doesn't live beyond the function call. + self.context = Some(unsafe { std::mem::transmute(&ctx) }); + let res = f(self); + self.context = parent; + res + } + + pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'b>> { + self.context.clone() + } } impl<'a, 'b, 'c, W: std::fmt::Write + Sized> std::fmt::Write for Printer<'a, 'b, 'c, W> { diff --git a/src/properties/custom.rs b/src/properties/custom.rs index 3ce1a136..84b090f5 100644 --- a/src/properties/custom.rs +++ b/src/properties/custom.rs @@ -49,7 +49,7 @@ impl<'i> CustomProperty<'i> { pub fn parse<'t, T>( name: CustomPropertyName<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result>> { let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { TokenList::parse(input, options, 0) @@ -149,7 +149,7 @@ impl<'i> UnparsedProperty<'i> { pub fn parse<'t, T>( property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result>> { let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { TokenList::parse(input, options, 0) @@ -262,7 +262,7 @@ impl<'i> TokenOrValue<'i> { impl<'i, T> ParseWithOptions<'i, T> for TokenList<'i> { fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result>> { TokenList::parse(input, options, 0) } @@ -271,7 +271,7 @@ impl<'i, T> ParseWithOptions<'i, T> for TokenList<'i> { impl<'i> TokenList<'i> { pub(crate) fn parse<'t, T>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, depth: usize, ) -> Result>> { let mut tokens = vec![]; @@ -296,7 +296,7 @@ impl<'i> TokenList<'i> { fn parse_into<'t, T>( input: &mut Parser<'i, 't>, tokens: &mut Vec>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, depth: usize, ) -> Result<(), ParseError<'i, ParserError<'i>>> { if depth > 500 { @@ -1060,7 +1060,7 @@ pub struct Variable<'i> { impl<'i> Variable<'i> { fn parse<'t, T>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, depth: usize, ) -> Result>> { let name = DashedIdentReference::parse_with_options(input, options)?; @@ -1211,7 +1211,7 @@ impl<'i> ToCss for EnvironmentVariableName<'i> { impl<'i> EnvironmentVariable<'i> { pub(crate) fn parse<'t, T>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, depth: usize, ) -> Result>> { input.expect_function_matching("env")?; @@ -1220,7 +1220,7 @@ impl<'i> EnvironmentVariable<'i> { pub(crate) fn parse_nested<'t, T>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, depth: usize, ) -> Result>> { let name = EnvironmentVariableName::parse(input)?; @@ -1348,7 +1348,7 @@ impl<'i> UnresolvedColor<'i> { fn parse<'t, T>( f: &CowArcStr<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result>> { let parser = ComponentParser::new(false); match_ignore_ascii_case! { &*f, diff --git a/src/properties/mod.rs b/src/properties/mod.rs index adc3182d..e65b80ce 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -676,7 +676,7 @@ macro_rules! define_properties { impl<'i> Property<'i> { /// Parses a CSS property by name. - pub fn parse<'t, T>(property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions) -> Result, ParseError<'i, ParserError<'i>>> { + pub fn parse<'t, T>(property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i, T>) -> Result, ParseError<'i, ParserError<'i>>> { let state = input.state(); match property_id { @@ -717,7 +717,7 @@ macro_rules! define_properties { } /// Parses a CSS property from a string. - pub fn parse_string(property_id: PropertyId<'i>, input: &'i str, options: ParserOptions) -> Result>> { + pub fn parse_string(property_id: PropertyId<'i>, input: &'i str, options: ParserOptions<'_, 'i, T>) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); Self::parse(property_id, &mut parser, &options) diff --git a/src/rules/container.rs b/src/rules/container.rs index 2aaa7bef..1851dc47 100644 --- a/src/rules/container.rs +++ b/src/rules/container.rs @@ -8,7 +8,6 @@ use crate::error::{MinifyError, ParserError, PrinterError}; use crate::media_query::MediaCondition; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::traits::{Parse, ToCss}; use crate::values::ident::CustomIdent; #[cfg(feature = "visitor")] @@ -70,12 +69,8 @@ impl<'i, T> ContainerRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for ContainerRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for ContainerRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -97,7 +92,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for ContainerRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/rules/layer.rs b/src/rules/layer.rs index 261d4a65..7bebd154 100644 --- a/src/rules/layer.rs +++ b/src/rules/layer.rs @@ -1,6 +1,6 @@ //! The `@layer` rule. -use super::{CssRuleList, Location, MinifyContext, StyleContext, ToCssWithContext}; +use super::{CssRuleList, Location, MinifyContext}; use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; @@ -140,12 +140,8 @@ impl<'i, T> LayerBlockRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for LayerBlockRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for LayerBlockRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -161,7 +157,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for LayerBlockRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/rules/media.rs b/src/rules/media.rs index 8857bade..f3898c9a 100644 --- a/src/rules/media.rs +++ b/src/rules/media.rs @@ -6,7 +6,6 @@ use crate::error::{MinifyError, PrinterError}; use crate::media_query::MediaList; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::traits::ToCss; #[cfg(feature = "visitor")] use crate::visitor::Visit; @@ -43,18 +42,14 @@ impl<'i, T> MediaRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for MediaRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for MediaRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { // If the media query always matches, we can just output the nested rules. if dest.minify && self.query.always_matches() { - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; return Ok(()); } @@ -66,7 +61,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for MediaRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 9388949b..be240a40 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -62,21 +62,21 @@ use crate::context::PropertyHandlerContext; use crate::declaration::DeclarationHandler; use crate::dependencies::{Dependency, ImportDependency}; use crate::error::{MinifyError, ParserError, PrinterError, PrinterErrorKind}; -use crate::parser::{DefaultAtRule, TopLevelRuleParser}; +use crate::parser::{parse_nested_at_rule, DefaultAtRule, NestedRuleParser, TopLevelRuleParser}; use crate::prefixes::Feature; use crate::printer::Printer; use crate::rules::keyframes::KeyframesName; -use crate::selector::{downlevel_selectors, get_prefix, is_equivalent}; +use crate::selector::{downlevel_selectors, get_prefix, is_equivalent, SelectorList}; use crate::stylesheet::ParserOptions; use crate::targets::Browsers; -use crate::traits::ToCss; +use crate::traits::{AtRuleParser, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] use crate::visitor::{Visit, VisitTypes, Visitor}; use container::ContainerRule; use counter_style::CounterStyleRule; -use cssparser::{parse_one_rule, AtRuleParser, ParseError, Parser, ParserInput}; +use cssparser::{parse_one_rule, ParseError, Parser, ParserInput}; use custom_media::CustomMediaRule; use document::MozDocumentRule; use font_face::FontFaceRule; @@ -92,19 +92,10 @@ use supports::SupportsRule; use unknown::UnknownAtRule; use viewport::ViewportRule; -pub(crate) trait ToCssWithContext<'a, 'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: std::fmt::Write; -} - -pub(crate) struct StyleContext<'a, 'i, T> { - pub rule: &'a StyleRule<'i, T>, - pub parent: Option<&'a StyleContext<'a, 'i, T>>, +#[derive(Clone)] +pub(crate) struct StyleContext<'a, 'i> { + pub selectors: &'a SelectorList<'i>, + pub parent: Option<&'a StyleContext<'a, 'i>>, } /// A source location. @@ -320,34 +311,30 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for CssRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { - CssRule::Media(media) => media.to_css_with_context(dest, context), + CssRule::Media(media) => media.to_css(dest), CssRule::Import(import) => import.to_css(dest), - CssRule::Style(style) => style.to_css_with_context(dest, context), + CssRule::Style(style) => style.to_css(dest), CssRule::Keyframes(keyframes) => keyframes.to_css(dest), CssRule::FontFace(font_face) => font_face.to_css(dest), CssRule::FontPaletteValues(f) => f.to_css(dest), CssRule::Page(font_face) => font_face.to_css(dest), - CssRule::Supports(supports) => supports.to_css_with_context(dest, context), + CssRule::Supports(supports) => supports.to_css(dest), CssRule::CounterStyle(counter_style) => counter_style.to_css(dest), CssRule::Namespace(namespace) => namespace.to_css(dest), CssRule::MozDocument(document) => document.to_css(dest), - CssRule::Nesting(nesting) => nesting.to_css_with_context(dest, context), + CssRule::Nesting(nesting) => nesting.to_css(dest), CssRule::Viewport(viewport) => viewport.to_css(dest), CssRule::CustomMedia(custom_media) => custom_media.to_css(dest), CssRule::LayerStatement(layer) => layer.to_css(dest), - CssRule::LayerBlock(layer) => layer.to_css_with_context(dest, context), + CssRule::LayerBlock(layer) => layer.to_css(dest), CssRule::Property(property) => property.to_css(dest), - CssRule::Container(container) => container.to_css_with_context(dest, context), + CssRule::Container(container) => container.to_css(dest), CssRule::Unknown(unknown) => unknown.to_css(dest), CssRule::Custom(rule) => rule.to_css(dest).map_err(|_| PrinterError { kind: PrinterErrorKind::FmtError, @@ -362,7 +349,7 @@ impl<'i, T> CssRule<'i, T> { /// Parse a single rule. pub fn parse<'t, P: AtRuleParser<'i, AtRule = T>>( input: &mut Parser<'i, 't>, - options: &mut ParserOptions<'_, 'i, P>, + options: &ParserOptions<'_, 'i, P>, ) -> Result>> { let (_, rule) = parse_one_rule(input, &mut TopLevelRuleParser::new(options))?; Ok(rule) @@ -371,20 +358,11 @@ impl<'i, T> CssRule<'i, T> { /// Parse a single rule from a string. pub fn parse_string>( input: &'i str, - mut options: ParserOptions<'_, 'i, P>, + options: ParserOptions<'_, 'i, P>, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); - Self::parse(&mut parser, &mut options) - } -} - -impl<'i, T: ToCss> ToCss for CssRule<'i, T> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - self.to_css_with_context(dest, None) + Self::parse(&mut parser, &options) } } @@ -396,6 +374,26 @@ pub struct CssRuleList<'i, R = DefaultAtRule>( #[cfg_attr(feature = "serde", serde(borrow))] pub Vec>, ); +impl<'i, T> CssRuleList<'i, T> { + /// Parse a rule list. + pub fn parse<'t, P: AtRuleParser<'i, AtRule = T>>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, P>, + ) -> Result>> { + let mut nested_parser = NestedRuleParser { options }; + nested_parser.parse_nested_rules(input) + } + + /// Parse a style block, with both declarations and rules. + /// Resulting declarations are returned in a nested style rule. + pub fn parse_style_block<'t, P: AtRuleParser<'i, AtRule = T>>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, P>, + ) -> Result>> { + parse_nested_at_rule(input, options) + } +} + // Manually implemented to avoid circular child types. #[cfg(feature = "visitor")] #[cfg_attr(docsrs, doc(cfg(feature = "visitor")))] @@ -687,21 +685,8 @@ fn merge_style_rules<'i, T>( false } -impl<'i, T: ToCss> ToCss for CssRuleList<'i, T> { +impl<'a, 'i, T: ToCss> ToCss for CssRuleList<'i, T> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - self.to_css_with_context(dest, None) - } -} - -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for CssRuleList<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -743,7 +728,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for CssRuleList<'i, T> { } dest.newline()?; } - rule.to_css_with_context(dest, context)?; + rule.to_css(dest)?; last_without_block = matches!( rule, CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..) diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs index b834a27f..d2696202 100644 --- a/src/rules/nesting.rs +++ b/src/rules/nesting.rs @@ -6,7 +6,6 @@ use super::MinifyContext; use crate::error::{MinifyError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::traits::ToCss; #[cfg(feature = "visitor")] use crate::visitor::Visit; @@ -34,20 +33,16 @@ impl<'i, T> NestingRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for NestingRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for NestingRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { #[cfg(feature = "sourcemap")] dest.add_mapping(self.loc); - if context.is_none() { + if dest.context().is_none() { dest.write_str("@nest ")?; } - self.style.to_css_with_context(dest, context) + self.style.to_css(dest) } } diff --git a/src/rules/style.rs b/src/rules/style.rs index d9c1a114..f68d9891 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -11,7 +11,7 @@ use crate::error::ParserError; use crate::error::{MinifyError, PrinterError, PrinterErrorKind}; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{CssRuleList, StyleContext, ToCssWithContext}; +use crate::rules::CssRuleList; use crate::selector::{is_compatible, is_unused, SelectorList}; use crate::targets::Browsers; use crate::traits::ToCss; @@ -156,17 +156,13 @@ where } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for StyleRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for StyleRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { if self.vendor_prefix.is_empty() { - self.to_css_base(dest, context) + self.to_css_base(dest) } else { let mut first_rule = true; macro_rules! prefix { @@ -182,7 +178,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for StyleRule<'i, T> { dest.newline()?; } dest.vendor_prefix = VendorPrefix::$prefix; - self.to_css_base(dest, context)?; + self.to_css_base(dest)?; } }; } @@ -200,11 +196,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for StyleRule<'i, T> { } impl<'a, 'i, T: ToCss> StyleRule<'i, T> { - fn to_css_base( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> + fn to_css_base(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -218,7 +210,7 @@ impl<'a, 'i, T: ToCss> StyleRule<'i, T> { if has_declarations { #[cfg(feature = "sourcemap")] dest.add_mapping(self.loc); - self.selectors.to_css_with_context(dest, context)?; + self.selectors.to_css(dest)?; dest.whitespace()?; dest.write_char('{')?; dest.indent(); @@ -286,13 +278,7 @@ impl<'a, 'i, T: ToCss> StyleRule<'i, T> { } else { end!(); newline!(); - self.rules.to_css_with_context( - dest, - Some(&StyleContext { - rule: self, - parent: context, - }), - )?; + dest.with_context(&self.selectors, |dest| self.rules.to_css(dest))?; } Ok(()) diff --git a/src/rules/supports.rs b/src/rules/supports.rs index 2481f584..365ba11b 100644 --- a/src/rules/supports.rs +++ b/src/rules/supports.rs @@ -6,7 +6,6 @@ use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; use crate::properties::PropertyId; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::targets::Browsers; use crate::traits::{Parse, ToCss}; use crate::values::string::CowArcStr; @@ -48,12 +47,8 @@ impl<'i, T> SupportsRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for SupportsRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for SupportsRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -65,7 +60,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for SupportsRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/selector.rs b/src/selector.rs index 703b5fba..798c2f66 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -2,13 +2,12 @@ use crate::compat::Feature; use crate::error::{ParserError, PrinterError}; -use crate::parser::DefaultAtRule; use crate::printer::Printer; use crate::properties::custom::TokenList; -use crate::rules::{StyleContext, ToCssWithContext}; +use crate::rules::StyleContext; use crate::stylesheet::{ParserOptions, PrinterOptions}; use crate::targets::Browsers; -use crate::traits::{Parse, ToCss}; +use crate::traits::{Parse, ParseWithOptions, ToCss}; use crate::values::ident::Ident; use crate::values::string::CSSString; use crate::vendor_prefix::VendorPrefix; @@ -21,7 +20,6 @@ use parcel_selectors::{ attr::{AttrSelectorOperator, ParsedAttrSelectorOperation, ParsedCaseSensitivity}, parser::SelectorImpl, }; -use std::collections::HashMap; use std::collections::HashSet; use std::fmt; @@ -62,14 +60,11 @@ impl<'i> SelectorImpl<'i> for Selectors { fn to_css(selectors: &SelectorList<'i>, dest: &mut W) -> std::fmt::Result { let mut printer = Printer::new(dest, PrinterOptions::default()); - serialize_selector_list::<_, _, DefaultAtRule>(selectors.0.iter(), &mut printer, None, false) - .map_err(|_| std::fmt::Error) + serialize_selector_list(selectors.0.iter(), &mut printer, None, false).map_err(|_| std::fmt::Error) } } pub(crate) struct SelectorParser<'a, 'o, 'i, T> { - pub default_namespace: &'a Option>, - pub namespace_prefixes: &'a HashMap, CowArcStr<'i>>, pub is_nesting_allowed: bool, pub options: &'a ParserOptions<'o, 'i, T>, } @@ -303,11 +298,11 @@ impl<'a, 'o, 'i, T> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, } fn default_namespace(&self) -> Option> { - self.default_namespace.clone() + None } fn namespace_for_prefix(&self, prefix: &Ident<'i>) -> Option> { - self.namespace_prefixes.get(&prefix.0).cloned() + Some(prefix.0.clone()) } #[inline] @@ -560,178 +555,176 @@ impl<'i> cssparser::ToCss for PseudoClass<'i> { } } -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for PseudoClass<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: fmt::Write, - { - use PseudoClass::*; - match &self { - Lang { languages: lang } => { - dest.write_str(":lang(")?; - let mut first = true; - for lang in lang { - if first { - first = false; - } else { - dest.delim(',', false)?; - } - serialize_identifier(lang, dest)?; +fn serialize_pseudo_class<'a, 'i, W>( + pseudo_class: &PseudoClass<'i>, + dest: &mut Printer, + context: Option<&StyleContext>, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + use PseudoClass::*; + match pseudo_class { + Lang { languages: lang } => { + dest.write_str(":lang(")?; + let mut first = true; + for lang in lang { + if first { + first = false; + } else { + dest.delim(',', false)?; } - return dest.write_str(")"); + serialize_identifier(lang, dest)?; } - Dir { direction: dir } => { - dest.write_str(":dir(")?; - dir.to_css(dest)?; - return dest.write_str(")"); - } - _ => {} + return dest.write_str(")"); } - - macro_rules! write_prefixed { - ($prefix: ident, $val: expr) => {{ - dest.write_char(':')?; - // If the printer has a vendor prefix override, use that. - let vp = if !dest.vendor_prefix.is_empty() { - dest.vendor_prefix - } else { - *$prefix - }; - vp.to_css(dest)?; - dest.write_str($val) - }}; + Dir { direction: dir } => { + dest.write_str(":dir(")?; + dir.to_css(dest)?; + return dest.write_str(")"); } + _ => {} + } - macro_rules! pseudo { - ($key: ident, $s: literal) => {{ - let class = if let Some(pseudo_classes) = &dest.pseudo_classes { - pseudo_classes.$key - } else { - None - }; - - if let Some(class) = class { - dest.write_char('.')?; - dest.write_ident(class) - } else { - dest.write_str($s) - } - }}; - } - - match &self { - // https://drafts.csswg.org/selectors-4/#useraction-pseudos - Hover => pseudo!(hover, ":hover"), - Active => pseudo!(active, ":active"), - Focus => pseudo!(focus, ":focus"), - FocusVisible => pseudo!(focus_visible, ":focus-visible"), - FocusWithin => pseudo!(focus_within, ":focus-within"), - - // https://drafts.csswg.org/selectors-4/#time-pseudos - Current => dest.write_str(":current"), - Past => dest.write_str(":past"), - Future => dest.write_str(":future"), + macro_rules! write_prefixed { + ($prefix: ident, $val: expr) => {{ + dest.write_char(':')?; + // If the printer has a vendor prefix override, use that. + let vp = if !dest.vendor_prefix.is_empty() { + dest.vendor_prefix + } else { + *$prefix + }; + vp.to_css(dest)?; + dest.write_str($val) + }}; + } - // https://drafts.csswg.org/selectors-4/#resource-pseudos - Playing => dest.write_str(":playing"), - Paused => dest.write_str(":paused"), - Seeking => dest.write_str(":seeking"), - Buffering => dest.write_str(":buffering"), - Stalled => dest.write_str(":stalled"), - Muted => dest.write_str(":muted"), - VolumeLocked => dest.write_str(":volume-locked"), + macro_rules! pseudo { + ($key: ident, $s: literal) => {{ + let class = if let Some(pseudo_classes) = &dest.pseudo_classes { + pseudo_classes.$key + } else { + None + }; - // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class - Fullscreen(prefix) => { - dest.write_char(':')?; - let vp = if !dest.vendor_prefix.is_empty() { - dest.vendor_prefix - } else { - *prefix - }; - vp.to_css(dest)?; - if vp == VendorPrefix::WebKit || vp == VendorPrefix::Moz { - dest.write_str("full-screen") - } else { - dest.write_str("fullscreen") - } + if let Some(class) = class { + dest.write_char('.')?; + dest.write_ident(class) + } else { + dest.write_str($s) } + }}; + } - // https://drafts.csswg.org/selectors-4/#the-defined-pseudo - Defined => dest.write_str(":defined"), - - // https://drafts.csswg.org/selectors-4/#location - AnyLink(prefix) => write_prefixed!(prefix, "any-link"), - Link => dest.write_str(":link"), - LocalLink => dest.write_str(":local-link"), - Target => dest.write_str(":target"), - TargetWithin => dest.write_str(":target-within"), - Visited => dest.write_str(":visited"), - - // https://drafts.csswg.org/selectors-4/#input-pseudos - Enabled => dest.write_str(":enabled"), - Disabled => dest.write_str(":disabled"), - ReadOnly(prefix) => write_prefixed!(prefix, "read-only"), - ReadWrite(prefix) => write_prefixed!(prefix, "read-write"), - PlaceholderShown(prefix) => write_prefixed!(prefix, "placeholder-shown"), - Default => dest.write_str(":default"), - Checked => dest.write_str(":checked"), - Indeterminate => dest.write_str(":indeterminate"), - Blank => dest.write_str(":blank"), - Valid => dest.write_str(":valid"), - Invalid => dest.write_str(":invalid"), - InRange => dest.write_str(":in-range"), - OutOfRange => dest.write_str(":out-of-range"), - Required => dest.write_str(":required"), - Optional => dest.write_str(":optional"), - UserValid => dest.write_str(":user-valid"), - UserInvalid => dest.write_str(":user-invalid"), - - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill - Autofill(prefix) => write_prefixed!(prefix, "autofill"), - - Local { selector } => selector.to_css_with_context(dest, context), - Global { selector } => { - let css_module = std::mem::take(&mut dest.css_module); - selector.to_css_with_context(dest, context)?; - dest.css_module = css_module; - Ok(()) + match pseudo_class { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + Hover => pseudo!(hover, ":hover"), + Active => pseudo!(active, ":active"), + Focus => pseudo!(focus, ":focus"), + FocusVisible => pseudo!(focus_visible, ":focus-visible"), + FocusWithin => pseudo!(focus_within, ":focus-within"), + + // https://drafts.csswg.org/selectors-4/#time-pseudos + Current => dest.write_str(":current"), + Past => dest.write_str(":past"), + Future => dest.write_str(":future"), + + // https://drafts.csswg.org/selectors-4/#resource-pseudos + Playing => dest.write_str(":playing"), + Paused => dest.write_str(":paused"), + Seeking => dest.write_str(":seeking"), + Buffering => dest.write_str(":buffering"), + Stalled => dest.write_str(":stalled"), + Muted => dest.write_str(":muted"), + VolumeLocked => dest.write_str(":volume-locked"), + + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + Fullscreen(prefix) => { + dest.write_char(':')?; + let vp = if !dest.vendor_prefix.is_empty() { + dest.vendor_prefix + } else { + *prefix + }; + vp.to_css(dest)?; + if vp == VendorPrefix::WebKit || vp == VendorPrefix::Moz { + dest.write_str("full-screen") + } else { + dest.write_str("fullscreen") } + } - // https://webkit.org/blog/363/styling-scrollbars/ - WebKitScrollbar(s) => { - use WebKitScrollbarPseudoClass::*; - dest.write_str(match s { - Horizontal => ":horizontal", - Vertical => ":vertical", - Decrement => ":decrement", - Increment => ":increment", - Start => ":start", - End => ":end", - DoubleButton => ":double-button", - SingleButton => ":single-button", - NoButton => ":no-button", - CornerPresent => ":corner-present", - WindowInactive => ":window-inactive", - }) - } + // https://drafts.csswg.org/selectors-4/#the-defined-pseudo + Defined => dest.write_str(":defined"), + + // https://drafts.csswg.org/selectors-4/#location + AnyLink(prefix) => write_prefixed!(prefix, "any-link"), + Link => dest.write_str(":link"), + LocalLink => dest.write_str(":local-link"), + Target => dest.write_str(":target"), + TargetWithin => dest.write_str(":target-within"), + Visited => dest.write_str(":visited"), + + // https://drafts.csswg.org/selectors-4/#input-pseudos + Enabled => dest.write_str(":enabled"), + Disabled => dest.write_str(":disabled"), + ReadOnly(prefix) => write_prefixed!(prefix, "read-only"), + ReadWrite(prefix) => write_prefixed!(prefix, "read-write"), + PlaceholderShown(prefix) => write_prefixed!(prefix, "placeholder-shown"), + Default => dest.write_str(":default"), + Checked => dest.write_str(":checked"), + Indeterminate => dest.write_str(":indeterminate"), + Blank => dest.write_str(":blank"), + Valid => dest.write_str(":valid"), + Invalid => dest.write_str(":invalid"), + InRange => dest.write_str(":in-range"), + OutOfRange => dest.write_str(":out-of-range"), + Required => dest.write_str(":required"), + Optional => dest.write_str(":optional"), + UserValid => dest.write_str(":user-valid"), + UserInvalid => dest.write_str(":user-invalid"), + + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + Autofill(prefix) => write_prefixed!(prefix, "autofill"), + + Local { selector } => serialize_selector(selector, dest, context, false), + Global { selector } => { + let css_module = std::mem::take(&mut dest.css_module); + serialize_selector(selector, dest, context, false)?; + dest.css_module = css_module; + Ok(()) + } - Lang { languages: _ } | Dir { direction: _ } => unreachable!(), - Custom { name } => { - dest.write_char(':')?; - return dest.write_str(&name); - } - CustomFunction { name, arguments: args } => { - dest.write_char(':')?; - dest.write_str(name)?; - dest.write_char('(')?; - args.to_css(dest, false)?; - dest.write_char(')') - } + // https://webkit.org/blog/363/styling-scrollbars/ + WebKitScrollbar(s) => { + use WebKitScrollbarPseudoClass::*; + dest.write_str(match s { + Horizontal => ":horizontal", + Vertical => ":vertical", + Decrement => ":decrement", + Increment => ":increment", + Start => ":start", + End => ":end", + DoubleButton => ":double-button", + SingleButton => ":single-button", + NoButton => ":no-button", + CornerPresent => ":corner-present", + WindowInactive => ":window-inactive", + }) + } + + Lang { languages: _ } | Dir { direction: _ } => unreachable!(), + Custom { name } => { + dest.write_char(':')?; + return dest.write_str(&name); + } + CustomFunction { name, arguments: args } => { + dest.write_char(':')?; + dest.write_str(name)?; + dest.write_char('(')?; + args.to_css(dest, false)?; + dest.write_char(')') } } } @@ -872,99 +865,101 @@ impl<'i> cssparser::ToCss for PseudoElement<'i> { } } -impl<'i> ToCss for PseudoElement<'i> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: fmt::Write, - { - use PseudoElement::*; +fn serialize_pseudo_element<'a, 'i, W>( + pseudo_element: &PseudoElement, + dest: &mut Printer, + context: Option<&StyleContext>, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + use PseudoElement::*; + + macro_rules! write_prefix { + ($prefix: ident) => {{ + dest.write_str("::")?; + // If the printer has a vendor prefix override, use that. + let vp = if !dest.vendor_prefix.is_empty() { + dest.vendor_prefix + } else { + *$prefix + }; + vp.to_css(dest)?; + vp + }}; + } - macro_rules! write_prefix { - ($prefix: ident) => {{ - dest.write_str("::")?; - // If the printer has a vendor prefix override, use that. - let vp = if !dest.vendor_prefix.is_empty() { - dest.vendor_prefix - } else { - *$prefix - }; - vp.to_css(dest)?; - vp - }}; - } + macro_rules! write_prefixed { + ($prefix: ident, $val: expr) => {{ + write_prefix!($prefix); + dest.write_str($val) + }}; + } - macro_rules! write_prefixed { - ($prefix: ident, $val: expr) => {{ - write_prefix!($prefix); - dest.write_str($val) - }}; + match pseudo_element { + // CSS2 pseudo elements support a single colon syntax in addition + // to the more correct double colon for other pseudo elements. + // We use that here because it's supported everywhere and is shorter. + After => dest.write_str(":after"), + Before => dest.write_str(":before"), + FirstLine => dest.write_str(":first-line"), + FirstLetter => dest.write_str(":first-letter"), + Marker => dest.write_str("::marker"), + Selection(prefix) => write_prefixed!(prefix, "selection"), + Cue => dest.write_str("::cue"), + CueRegion => dest.write_str("::cue-region"), + CueFunction { selector } => { + dest.write_str("::cue(")?; + serialize_selector(selector, dest, context, false)?; + dest.write_char(')') } - - match &self { - // CSS2 pseudo elements support a single colon syntax in addition - // to the more correct double colon for other pseudo elements. - // We use that here because it's supported everywhere and is shorter. - After => dest.write_str(":after"), - Before => dest.write_str(":before"), - FirstLine => dest.write_str(":first-line"), - FirstLetter => dest.write_str(":first-letter"), - Marker => dest.write_str("::marker"), - Selection(prefix) => write_prefixed!(prefix, "selection"), - Cue => dest.write_str("::cue"), - CueRegion => dest.write_str("::cue-region"), - CueFunction { selector } => { - dest.write_str("::cue(")?; - serialize_selector::<_, DefaultAtRule>(selector, dest, None, false)?; - dest.write_char(')') - } - CueRegionFunction(selector) => { - dest.write_str("::cue-region(")?; - serialize_selector::<_, DefaultAtRule>(selector, dest, None, false)?; - dest.write_char(')') - } - Placeholder(prefix) => { - let vp = write_prefix!(prefix); - if vp == VendorPrefix::WebKit || vp == VendorPrefix::Ms { - dest.write_str("input-placeholder") - } else { - dest.write_str("placeholder") - } - } - Backdrop(prefix) => write_prefixed!(prefix, "backdrop"), - FileSelectorButton(prefix) => { - let vp = write_prefix!(prefix); - if vp == VendorPrefix::WebKit { - dest.write_str("file-upload-button") - } else if vp == VendorPrefix::Ms { - dest.write_str("browse") - } else { - dest.write_str("file-selector-button") - } - } - WebKitScrollbar(s) => { - use WebKitScrollbarPseudoElement::*; - dest.write_str(match s { - Scrollbar => "::-webkit-scrollbar", - Button => "::-webkit-scrollbar-button", - Track => "::-webkit-scrollbar-track", - TrackPiece => "::-webkit-scrollbar-track-piece", - Thumb => "::-webkit-scrollbar-thumb", - Corner => "::-webkit-scrollbar-corner", - Resizer => "::-webkit-resizer", - }) - } - Custom { name: val } => { - dest.write_str("::")?; - return dest.write_str(val); + CueRegionFunction(selector) => { + dest.write_str("::cue-region(")?; + serialize_selector(selector, dest, context, false)?; + dest.write_char(')') + } + Placeholder(prefix) => { + let vp = write_prefix!(prefix); + if vp == VendorPrefix::WebKit || vp == VendorPrefix::Ms { + dest.write_str("input-placeholder") + } else { + dest.write_str("placeholder") } - CustomFunction { name, arguments: args } => { - dest.write_str("::")?; - dest.write_str(name)?; - dest.write_char('(')?; - args.to_css(dest, false)?; - dest.write_char(')') + } + Backdrop(prefix) => write_prefixed!(prefix, "backdrop"), + FileSelectorButton(prefix) => { + let vp = write_prefix!(prefix); + if vp == VendorPrefix::WebKit { + dest.write_str("file-upload-button") + } else if vp == VendorPrefix::Ms { + dest.write_str("browse") + } else { + dest.write_str("file-selector-button") } } + WebKitScrollbar(s) => { + use WebKitScrollbarPseudoElement::*; + dest.write_str(match s { + Scrollbar => "::-webkit-scrollbar", + Button => "::-webkit-scrollbar-button", + Track => "::-webkit-scrollbar-track", + TrackPiece => "::-webkit-scrollbar-track-piece", + Thumb => "::-webkit-scrollbar-thumb", + Corner => "::-webkit-scrollbar-corner", + Resizer => "::-webkit-resizer", + }) + } + Custom { name: val } => { + dest.write_str("::")?; + return dest.write_str(val); + } + CustomFunction { name, arguments: args } => { + dest.write_str("::")?; + dest.write_str(name)?; + dest.write_char('(')?; + args.to_css(dest, false)?; + dest.write_char(')') + } } } @@ -1030,25 +1025,12 @@ impl<'i> PseudoElement<'i> { } } -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for SelectorList<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: fmt::Write, - { - serialize_selector_list(self.0.iter(), dest, context, false) - } -} - -impl<'i> ToCss for SelectorList<'i> { +impl<'a, 'i> ToCss for SelectorList<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where - W: std::fmt::Write, + W: fmt::Write, { - serialize_selector_list::<_, _, DefaultAtRule>(self.0.iter(), dest, None, false) + serialize_selector_list(self.0.iter(), dest, dest.context(), false) } } @@ -1068,23 +1050,19 @@ impl ToCss for Combinator { } // Copied from the selectors crate and modified to override to_css implementation. -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for Selector<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i> ToCss for Selector<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: fmt::Write, { - serialize_selector(self, dest, context, false) + serialize_selector(self, dest, dest.context(), false) } } -fn serialize_selector<'a, 'i, W, T>( +fn serialize_selector<'a, 'i, W>( selector: &Selector<'i>, dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, + context: Option<&StyleContext>, mut is_relative: bool, ) -> Result<(), PrinterError> where @@ -1169,7 +1147,7 @@ where } for simple in iter { - simple.to_css_with_context(dest, context)?; + serialize_component(simple, dest, context)?; } if swap_nesting { @@ -1199,15 +1177,15 @@ where // This ensures that the compiled selector is valid. e.g. (div.foo is valid, .foodiv is not). let nesting = iter.next().unwrap(); let local = iter.next().unwrap(); - local.to_css_with_context(dest, context)?; + serialize_component(local, dest, context)?; // Also check the next item in case of namespaces. if first_non_namespace > first_index { let local = iter.next().unwrap(); - local.to_css_with_context(dest, context)?; + serialize_component(local, dest, context)?; } - nesting.to_css_with_context(dest, context)?; + serialize_component(nesting, dest, context)?; } else if has_leading_nesting && context.is_some() { // Nesting selector may serialize differently if it is leading, due to type selectors. iter.next(); @@ -1224,7 +1202,7 @@ where continue; } } - simple.to_css_with_context(dest, context)?; + serialize_component(simple, dest, context)?; } } @@ -1248,115 +1226,113 @@ where Ok(()) } -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for Component<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: fmt::Write, - { - match &self { - Component::Combinator(ref c) => c.to_css(dest), - Component::AttributeInNoNamespace { - ref local_name, - operator, - ref value, - case_sensitivity, - .. - } => { - dest.write_char('[')?; - cssparser::ToCss::to_css(local_name, dest)?; - cssparser::ToCss::to_css(operator, dest)?; - - if dest.minify { - // Serialize as both an identifier and a string and choose the shorter one. - let mut id = String::new(); - serialize_identifier(&value, &mut id)?; - - let s = value.to_css_string(Default::default())?; - - if id.len() > 0 && id.len() < s.len() { - dest.write_str(&id)?; - } else { - dest.write_str(&s)?; - } +fn serialize_component<'a, 'i, W>( + component: &Component, + dest: &mut Printer, + context: Option<&StyleContext>, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + match component { + Component::Combinator(ref c) => c.to_css(dest), + Component::AttributeInNoNamespace { + ref local_name, + operator, + ref value, + case_sensitivity, + .. + } => { + dest.write_char('[')?; + cssparser::ToCss::to_css(local_name, dest)?; + cssparser::ToCss::to_css(operator, dest)?; + + if dest.minify { + // Serialize as both an identifier and a string and choose the shorter one. + let mut id = String::new(); + serialize_identifier(&value, &mut id)?; + + let s = value.to_css_string(Default::default())?; + + if id.len() > 0 && id.len() < s.len() { + dest.write_str(&id)?; } else { - value.to_css(dest)?; - } - - match case_sensitivity { - parcel_selectors::attr::ParsedCaseSensitivity::CaseSensitive - | parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {} - parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, - parcel_selectors::attr::ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + dest.write_str(&s)?; } - dest.write_char(']') + } else { + value.to_css(dest)?; } - Component::Is(ref list) - | Component::Where(ref list) - | Component::Negation(ref list) - | Component::Any(_, ref list) => { - match *self { - Component::Where(..) => dest.write_str(":where(")?, - Component::Is(ref selectors) => { - // If there's only one simple selector, serialize it directly. - if selectors.len() == 1 { - let first = selectors.first().unwrap(); - if !has_type_selector(first) && is_simple(first) { - serialize_selector(first, dest, context, false)?; - return Ok(()); - } - } - let vp = dest.vendor_prefix; - if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { - dest.write_char(':')?; - vp.to_css(dest)?; - dest.write_str("any(")?; - } else { - dest.write_str(":is(")?; + match case_sensitivity { + parcel_selectors::attr::ParsedCaseSensitivity::CaseSensitive + | parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {} + parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, + parcel_selectors::attr::ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + } + dest.write_char(']') + } + Component::Is(ref list) + | Component::Where(ref list) + | Component::Negation(ref list) + | Component::Any(_, ref list) => { + match *component { + Component::Where(..) => dest.write_str(":where(")?, + Component::Is(ref selectors) => { + // If there's only one simple selector, serialize it directly. + if selectors.len() == 1 { + let first = selectors.first().unwrap(); + if !has_type_selector(first) && is_simple(first) { + serialize_selector(first, dest, context, false)?; + return Ok(()); } } - Component::Negation(..) => return serialize_negation(list.iter(), dest, context), - Component::Any(ref prefix, ..) => { + + let vp = dest.vendor_prefix; + if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { dest.write_char(':')?; - prefix.to_css(dest)?; + vp.to_css(dest)?; dest.write_str("any(")?; + } else { + dest.write_str(":is(")?; } - _ => unreachable!(), } - serialize_selector_list(list.iter(), dest, context, false)?; - dest.write_str(")") - } - Component::Has(ref list) => { - dest.write_str(":has(")?; - serialize_selector_list(list.iter(), dest, context, true)?; - dest.write_str(")") - } - Component::NonTSPseudoClass(pseudo) => pseudo.to_css_with_context(dest, context), - Component::PseudoElement(pseudo) => pseudo.to_css(dest), - Component::Nesting => serialize_nesting(dest, context, false), - Component::Class(ref class) => { - dest.write_char('.')?; - dest.write_ident(&class.0) - } - Component::ID(ref id) => { - dest.write_char('#')?; - dest.write_ident(&id.0) - } - _ => { - cssparser::ToCss::to_css(self, dest)?; - Ok(()) + Component::Negation(..) => return serialize_negation(list.iter(), dest, context), + Component::Any(ref prefix, ..) => { + dest.write_char(':')?; + prefix.to_css(dest)?; + dest.write_str("any(")?; + } + _ => unreachable!(), } + serialize_selector_list(list.iter(), dest, context, false)?; + dest.write_str(")") + } + Component::Has(ref list) => { + dest.write_str(":has(")?; + serialize_selector_list(list.iter(), dest, context, true)?; + dest.write_str(")") + } + Component::NonTSPseudoClass(pseudo) => serialize_pseudo_class(pseudo, dest, context), + Component::PseudoElement(pseudo) => serialize_pseudo_element(pseudo, dest, context), + Component::Nesting => serialize_nesting(dest, context, false), + Component::Class(ref class) => { + dest.write_char('.')?; + dest.write_ident(&class.0) + } + Component::ID(ref id) => { + dest.write_char('#')?; + dest.write_ident(&id.0) + } + _ => { + cssparser::ToCss::to_css(component, dest)?; + Ok(()) } } } -fn serialize_nesting( +fn serialize_nesting( dest: &mut Printer, - context: Option<&StyleContext>, + context: Option<&StyleContext>, first: bool, ) -> Result<(), PrinterError> where @@ -1367,13 +1343,13 @@ where // Otherwise, use an :is() pseudo class. // Type selectors are only allowed at the start of a compound selector, // so use :is() if that is not the case. - if ctx.rule.selectors.0.len() == 1 - && (first || (!has_type_selector(&ctx.rule.selectors.0[0]) && is_simple(&ctx.rule.selectors.0[0]))) + if ctx.selectors.0.len() == 1 + && (first || (!has_type_selector(&ctx.selectors.0[0]) && is_simple(&ctx.selectors.0[0]))) { - ctx.rule.selectors.0.first().unwrap().to_css_with_context(dest, ctx.parent) + serialize_selector(ctx.selectors.0.first().unwrap(), dest, ctx.parent, false) } else { dest.write_str(":is(")?; - serialize_selector_list(ctx.rule.selectors.0.iter(), dest, ctx.parent, false)?; + serialize_selector_list(ctx.selectors.0.iter(), dest, ctx.parent, false)?; dest.write_char(')') } } else { @@ -1416,10 +1392,10 @@ fn is_namespace(component: Option<&Component>) -> bool { ) } -fn serialize_selector_list<'a, 'i: 'a, I, W, T>( +fn serialize_selector_list<'a, 'i: 'a, I, W>( iter: I, dest: &mut Printer, - context: Option<&StyleContext<'_, 'i, T>>, + context: Option<&StyleContext>, is_relative: bool, ) -> Result<(), PrinterError> where @@ -1437,10 +1413,10 @@ where Ok(()) } -fn serialize_negation<'a, 'i: 'a, I, W, T>( +fn serialize_negation<'a, 'i: 'a, I, W>( iter: I, dest: &mut Printer, - context: Option<&StyleContext<'_, 'i, T>>, + context: Option<&StyleContext>, ) -> Result<(), PrinterError> where I: Iterator>, @@ -1460,7 +1436,7 @@ where } else { for selector in iter { dest.write_str(":not(")?; - selector.to_css_with_context(dest, context)?; + serialize_selector(selector, dest, context, false)?; dest.write_char(')')?; } } @@ -1854,3 +1830,34 @@ impl<'i, T: Visit<'i, T, V>, V: Visitor<'i, T>> Visit<'i, T, V> for Selector<'i> Ok(()) } } + +impl<'i, T> ParseWithOptions<'i, T> for Selector<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, T>, + ) -> Result>> { + Selector::parse( + &SelectorParser { + is_nesting_allowed: options.nesting, + options: &options, + }, + input, + ) + } +} + +impl<'i, T> ParseWithOptions<'i, T> for SelectorList<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, T>, + ) -> Result>> { + SelectorList::parse( + &SelectorParser { + is_nesting_allowed: options.nesting, + options: &options, + }, + input, + parcel_selectors::parser::NestingRequirement::None, + ) + } +} From ba111080f464b0d39dc732e2e27cdf20222ba992 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 11 Jan 2023 21:29:58 -0500 Subject: [PATCH 05/12] Expose options to custom at rule parsers --- examples/custom_at_rule.rs | 18 ++- src/bundler.rs | 3 +- src/error.rs | 3 + src/parser.rs | 281 ++++++++++++++++-------------------- src/stylesheet.rs | 4 +- src/traits.rs | 95 +++++++++++- tests/test_custom_parser.rs | 15 +- 7 files changed, 249 insertions(+), 170 deletions(-) diff --git a/examples/custom_at_rule.rs b/examples/custom_at_rule.rs index 43fe79eb..cb3043ab 100644 --- a/examples/custom_at_rule.rs +++ b/examples/custom_at_rule.rs @@ -1,4 +1,8 @@ -use std::{collections::HashMap, convert::Infallible}; +use std::{ + collections::HashMap, + convert::Infallible, + sync::{Arc, RwLock}, +}; use cssparser::*; use lightningcss::{ @@ -10,7 +14,7 @@ use lightningcss::{ selector::{Component, Selector}, stylesheet::{ParserOptions, PrinterOptions, StyleSheet}, targets::Browsers, - traits::ToCss, + traits::{AtRuleParser, ToCss}, values::{color::CssColor, length::LengthValue}, vendor_prefix::VendorPrefix, visit_types, @@ -21,7 +25,7 @@ fn main() { let args: Vec = std::env::args().collect(); let source = std::fs::read_to_string(&args[1]).unwrap(); let opts = ParserOptions { - at_rule_parser: Some(TailwindAtRuleParser), + at_rule_parser: Some(Arc::new(RwLock::new(TailwindAtRuleParser))), filename: args[1].clone(), nesting: true, custom_media: false, @@ -103,6 +107,7 @@ impl<'i> AtRuleParser<'i> for TailwindAtRuleParser { &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i, Self>, ) -> Result> { match_ignore_ascii_case! {&*name, "tailwind" => { @@ -135,7 +140,12 @@ impl<'i> AtRuleParser<'i> for TailwindAtRuleParser { } } - fn rule_without_block(&mut self, prelude: Self::Prelude, start: &ParserState) -> Result { + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + _options: &ParserOptions<'_, 'i, Self>, + ) -> Result { let loc = start.source_location(); match prelude { Prelude::Tailwind(directive) => Ok(AtRule::Tailwind(TailwindRule { directive, loc })), diff --git a/src/bundler.rs b/src/bundler.rs index 80f68af6..cbc5ae8c 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -36,7 +36,7 @@ use crate::{ layer::{LayerBlockRule, LayerName}, Location, }, - traits::ToCss, + traits::{AtRuleParser, ToCss}, values::ident::DashedIdentReference, }; use crate::{ @@ -50,7 +50,6 @@ use crate::{ }, stylesheet::{ParserOptions, StyleSheet}, }; -use cssparser::AtRuleParser; use dashmap::DashMap; use parcel_sourcemap::SourceMap; use rayon::prelude::*; diff --git a/src/error.rs b/src/error.rs index a203dc56..f694ca16 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,6 +71,8 @@ impl fmt::Display for ErrorLocation { pub enum ParserError<'i> { /// An at rule body was invalid. AtRuleBodyInvalid, + /// An at rule prelude was invalid + AtRulePreludeInvalid, /// An unknown or unsupported at rule was encountered. AtRuleInvalid(CowArcStr<'i>), /// Unexpectedly encountered the end of input data. @@ -106,6 +108,7 @@ impl<'i> fmt::Display for ParserError<'i> { use ParserError::*; match self { AtRuleBodyInvalid => write!(f, "Invalid @ rule body"), + AtRulePreludeInvalid => write!(f, "Invalid @ rule prelude"), AtRuleInvalid(name) => write!(f, "Unknown at rule: @{}", name), EndOfInput => write!(f, "Unexpected end of input"), InvalidDeclaration => write!(f, "Invalid declaration"), diff --git a/src/parser.rs b/src/parser.rs index 81979b9c..5cbb3ed2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -34,7 +34,6 @@ use crate::vendor_prefix::VendorPrefix; use crate::visitor::{Visit, VisitTypes, Visitor}; use cssparser::*; use parcel_selectors::parser::NestingRequirement; -use std::collections::HashMap; use std::sync::{Arc, RwLock}; /// CSS parsing options. @@ -56,7 +55,7 @@ pub struct ParserOptions<'o, 'i, T = DefaultAtRuleParser> { /// A list that will be appended to when a warning occurs. pub warnings: Option>>>>>, /// A custom at rule parser. - pub at_rule_parser: Option, + pub at_rule_parser: Option>>, } impl<'o, 'i, T> ParserOptions<'o, 'i, T> { @@ -94,7 +93,7 @@ impl<'o, 'i> ParserOptions<'o, 'i, DefaultAtRuleParser> { #[derive(Clone, Default)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub struct DefaultAtRuleParser; -impl<'i> AtRuleParser<'i> for DefaultAtRuleParser { +impl<'i> crate::traits::AtRuleParser<'i> for DefaultAtRuleParser { type AtRule = DefaultAtRule; type Error = (); type Prelude = (); @@ -133,28 +132,20 @@ enum State { /// The parser for the top-level rules in a stylesheet. pub struct TopLevelRuleParser<'a, 'o, 'i, T> { - default_namespace: Option>, - namespace_prefixes: HashMap, CowArcStr<'i>>, - pub options: &'a mut ParserOptions<'o, 'i, T>, + pub options: &'a ParserOptions<'o, 'i, T>, state: State, } impl<'a, 'o, 'b, 'i, T> TopLevelRuleParser<'a, 'o, 'i, T> { - pub fn new(options: &'a mut ParserOptions<'o, 'i, T>) -> Self { + pub fn new(options: &'a ParserOptions<'o, 'i, T>) -> Self { TopLevelRuleParser { - default_namespace: None, - namespace_prefixes: HashMap::new(), options, state: State::Start, } } fn nested<'x: 'b>(&'x mut self) -> NestedRuleParser<'_, 'o, 'i, T> { - NestedRuleParser { - default_namespace: &mut self.default_namespace, - namespace_prefixes: &mut self.namespace_prefixes, - options: &mut self.options, - } + NestedRuleParser { options: &self.options } } } @@ -211,7 +202,7 @@ pub enum AtRulePrelude<'i, T> { Custom(T), } -impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i, T> { type Prelude = AtRulePrelude<'i, T::Prelude>; type AtRule = (SourcePosition, CssRule<'i, T::AtRule>); type Error = ParserError<'i>; @@ -319,12 +310,6 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a AtRulePrelude::Namespace(prefix, url) => { self.state = State::Namespaces; - if let Some(prefix) = &prefix { - self.namespace_prefixes.insert(prefix.into(), url.clone().into()); - } else { - self.default_namespace = Some(url.clone().into()); - } - CssRule::Namespace(NamespaceRule { prefix: prefix.map(|x| x.into()), url: url.into(), @@ -362,7 +347,9 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a } } -impl<'a, 'o, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> + for TopLevelRuleParser<'a, 'o, 'i, T> +{ type Prelude = SelectorList<'i>; type QualifiedRule = (SourcePosition, CssRule<'i, T::AtRule>); type Error = ParserError<'i>; @@ -388,22 +375,16 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for TopLevelRulePa } } -struct NestedRuleParser<'a, 'o, 'i, T> { - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, +pub struct NestedRuleParser<'a, 'o, 'i, T> { + pub options: &'a ParserOptions<'o, 'i, T>, } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> { - fn parse_nested_rules<'t>( +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> { + pub fn parse_nested_rules<'t>( &mut self, input: &mut Parser<'i, 't>, ) -> Result, ParseError<'i, ParserError<'i>>> { - let nested_parser = NestedRuleParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, - options: self.options, - }; + let nested_parser = NestedRuleParser { options: self.options }; let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser); let mut rules = Vec::new(); @@ -434,7 +415,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> { } } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser<'a, 'o, 'i, T> { type Prelude = AtRulePrelude<'i, T::Prelude>; type AtRule = CssRule<'i, T::AtRule>; type Error = ParserError<'i>; @@ -533,18 +514,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser< let condition = MediaCondition::parse(input, true)?; Ok(AtRulePrelude::Container(name, condition)) }, - _ => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - if let Ok(prelude) = at_rule_parser.parse_prelude(name.clone(), input) { - return Ok(AtRulePrelude::Custom(prelude)) - } - } - - self.options.warn(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))); - input.skip_whitespace(); - let tokens = TokenList::parse(input, &self.options, 0)?; - Ok(AtRulePrelude::Unknown(name.into(), tokens)) - } + _ => parse_custom_at_rule_prelude(&name, input, self.options) } } @@ -664,16 +634,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser< block: Some(TokenList::parse(input, &self.options, 0)?), loc, })), - AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - at_rule_parser - .parse_block(prelude, start, input) - .map(|prelude| CssRule::Custom(prelude)) - .map_err(|_| input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) - } else { - Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) - } - } + AtRulePrelude::Custom(prelude) => parse_custom_at_rule_body(prelude, input, start, self.options), } } @@ -698,21 +659,15 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser< block: None, loc, })), - AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - at_rule_parser - .rule_without_block(prelude, start) - .map(|prelude| CssRule::Custom(prelude)) - } else { - Err(()) - } - } + AtRulePrelude::Custom(prelude) => parse_custom_at_rule_without_block(prelude, start, self.options), _ => Err(()), } } } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> + for NestedRuleParser<'a, 'o, 'i, T> +{ type Prelude = SelectorList<'i>; type QualifiedRule = CssRule<'i, T::AtRule>; type Error = ParserError<'i>; @@ -722,8 +677,6 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRule input: &mut Parser<'i, 't>, ) -> Result> { let selector_parser = SelectorParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: false, options: &self.options, }; @@ -738,7 +691,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRule ) -> Result, ParseError<'i, Self::Error>> { let loc = self.loc(start); let (declarations, rules) = if self.options.nesting { - parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)? + parse_declarations_and_nested_rules(input, self.options)? } else { (DeclarationBlock::parse(input, self.options)?, CssRuleList(vec![])) }; @@ -752,18 +705,84 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRule } } -fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( +fn parse_custom_at_rule_prelude<'i, 't, T: crate::traits::AtRuleParser<'i>>( + name: &CowRcStr<'i>, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, T>, +) -> Result, ParseError<'i, ParserError<'i>>> { + if let Some(at_rule_parser) = &options.at_rule_parser { + match at_rule_parser.write().unwrap().parse_prelude(name.clone(), input, options) { + Ok(prelude) => return Ok(AtRulePrelude::Custom(prelude)), + Err(ParseError { + kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(..)), + .. + }) => {} + Err(err) => { + return Err(match &err.kind { + ParseErrorKind::Basic(kind) => ParseError { + kind: ParseErrorKind::Basic(kind.clone()), + location: err.location, + }, + _ => input.new_custom_error(ParserError::AtRulePreludeInvalid), + }) + } + } + } + + options.warn(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))); + input.skip_whitespace(); + let tokens = TokenList::parse(input, &options, 0)?; + Ok(AtRulePrelude::Unknown(name.into(), tokens)) +} + +fn parse_custom_at_rule_body<'i, 't, T: crate::traits::AtRuleParser<'i>>( + prelude: T::Prelude, + input: &mut Parser<'i, 't>, + start: &ParserState, + options: &ParserOptions<'_, 'i, T>, +) -> Result, ParseError<'i, ParserError<'i>>> { + if let Some(at_rule_parser) = &options.at_rule_parser { + at_rule_parser + .write() + .unwrap() + .parse_block(prelude, start, input, options) + .map(|prelude| CssRule::Custom(prelude)) + .map_err(|err| match &err.kind { + ParseErrorKind::Basic(kind) => ParseError { + kind: ParseErrorKind::Basic(kind.clone()), + location: err.location, + }, + _ => input.new_error(BasicParseErrorKind::AtRuleBodyInvalid), + }) + } else { + Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) + } +} + +fn parse_custom_at_rule_without_block<'i, 't, T: crate::traits::AtRuleParser<'i>>( + prelude: T::Prelude, + start: &ParserState, + options: &ParserOptions<'_, 'i, T>, +) -> Result, ()> { + if let Some(at_rule_parser) = &options.at_rule_parser { + at_rule_parser + .write() + .unwrap() + .rule_without_block(prelude, start, options) + .map(|prelude| CssRule::Custom(prelude)) + } else { + Err(()) + } +} + +fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>( input: &mut Parser<'i, 't>, - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i, T>, ) -> Result<(DeclarationBlock<'i>, CssRuleList<'i, T::AtRule>), ParseError<'i, ParserError<'i>>> { let mut important_declarations = DeclarationList::new(); let mut declarations = DeclarationList::new(); let mut rules = CssRuleList(vec![]); let mut parser = StyleRuleParser { - default_namespace, - namespace_prefixes, options, declarations: &mut declarations, important_declarations: &mut important_declarations, @@ -812,17 +831,17 @@ fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( )) } -pub struct StyleRuleParser<'a, 'o, 'i, T: AtRuleParser<'i>> { - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, +pub struct StyleRuleParser<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> { + options: &'a ParserOptions<'o, 'i, T>, declarations: &'a mut DeclarationList<'i>, important_declarations: &'a mut DeclarationList<'i>, rules: &'a mut CssRuleList<'i, T::AtRule>, } /// Parse a declaration within {} block: `color: blue` -impl<'a, 'o, 'i, T: AtRuleParser<'i>> cssparser::DeclarationParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> cssparser::DeclarationParser<'i> + for StyleRuleParser<'a, 'o, 'i, T> +{ type Declaration = (); type Error = ParserError<'i>; @@ -841,7 +860,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> cssparser::DeclarationParser<'i> for Style } } -impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { type Prelude = AtRulePrelude<'i, T::Prelude>; type AtRule = (); type Error = ParserError<'i>; @@ -873,26 +892,13 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' "nest" => { self.options.warn(input.new_custom_error(ParserError::DeprecatedNestRule)); let selector_parser = SelectorParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: true, options: &self.options, }; let selectors = SelectorList::parse(&selector_parser, input, NestingRequirement::Contained)?; Ok(AtRulePrelude::Nest(selectors)) }, - _ => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - if let Ok(prelude) = at_rule_parser.parse_prelude(name.clone(), input) { - return Ok(AtRulePrelude::Custom(prelude)) - } - } - - self.options.warn(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))); - input.skip_whitespace(); - let tokens = TokenList::parse(input, &self.options, 0)?; - Ok(AtRulePrelude::Unknown(name.into(), tokens)) - } + _ => parse_custom_at_rule_prelude(&name, input, self.options) } } @@ -912,13 +918,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' AtRulePrelude::Media(query) => { self.rules.0.push(CssRule::Media(MediaRule { query, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options)?, loc, })); Ok(()) @@ -926,13 +926,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' AtRulePrelude::Supports(condition) => { self.rules.0.push(CssRule::Supports(SupportsRule { condition, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options)?, loc, })); Ok(()) @@ -941,13 +935,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' self.rules.0.push(CssRule::Container(ContainerRule { name, condition, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options)?, loc, })); Ok(()) @@ -955,24 +943,13 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' AtRulePrelude::LayerBlock(name) => { self.rules.0.push(CssRule::LayerBlock(LayerBlockRule { name, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options)?, loc, })); Ok(()) } AtRulePrelude::Nest(selectors) => { - let (declarations, rules) = parse_declarations_and_nested_rules( - input, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.options)?; self.rules.0.push(CssRule::Nesting(NestingRule { style: StyleRule { selectors, @@ -995,15 +972,11 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' Ok(()) } AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - let rule = at_rule_parser - .parse_block(prelude, start, input) - .map_err(|_| input.new_error(BasicParseErrorKind::AtRuleBodyInvalid))?; - self.rules.0.push(CssRule::Custom(rule)); - Ok(()) - } else { - Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) - } + self + .rules + .0 + .push(parse_custom_at_rule_body(prelude, input, start, self.options)?); + Ok(()) } _ => Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)), } @@ -1026,38 +999,31 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' Ok(()) } AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - let rule = at_rule_parser.rule_without_block(prelude, start)?; - self.rules.0.push(CssRule::Custom(rule)); - Ok(()) - } else { - Err(()) - } + self + .rules + .0 + .push(parse_custom_at_rule_without_block(prelude, start, self.options)?); + Ok(()) } _ => Err(()), } } } -#[inline] -fn parse_nested_at_rule<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( +pub fn parse_nested_at_rule<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>( input: &mut Parser<'i, 't>, - source_index: u32, - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i, T>, ) -> Result, ParseError<'i, ParserError<'i>>> { let loc = input.current_source_location(); let loc = Location { - source_index, + source_index: options.source_index, line: loc.line, column: loc.column, }; // 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, options)?; + let (declarations, mut rules) = parse_declarations_and_nested_rules(input, options)?; if declarations.len() > 0 { rules.0.insert( @@ -1075,7 +1041,9 @@ fn parse_nested_at_rule<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( Ok(rules) } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> + for StyleRuleParser<'a, 'o, 'i, T> +{ type Prelude = SelectorList<'i>; type QualifiedRule = (); type Error = ParserError<'i>; @@ -1098,8 +1066,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for StyleRuleP input: &mut Parser<'i, 't>, ) -> Result<(), ParseError<'i, Self::Error>> { let loc = start.source_location(); - let (declarations, rules) = - parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.options)?; self.rules.0.push(CssRule::Style(StyleRule { selectors, vendor_prefix: VendorPrefix::empty(), diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 70a32655..0b8f41b6 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -13,10 +13,10 @@ use crate::parser::{DefaultAtRuleParser, TopLevelRuleParser}; use crate::printer::Printer; use crate::rules::{CssRule, CssRuleList, MinifyContext}; use crate::targets::Browsers; -use crate::traits::ToCss; +use crate::traits::{AtRuleParser, ToCss}; #[cfg(feature = "visitor")] use crate::visitor::{Visit, VisitTypes, Visitor}; -use cssparser::{AtRuleParser, Parser, ParserInput, RuleListParser}; +use cssparser::{Parser, ParserInput, RuleListParser}; #[cfg(feature = "sourcemap")] use parcel_sourcemap::SourceMap; use std::collections::{HashMap, HashSet}; diff --git a/src/traits.rs b/src/traits.rs index f96d83be..8ac879d7 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -32,13 +32,13 @@ pub trait ParseWithOptions<'i, T>: Sized { /// Parse a value of this type with the given options. fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i, T>, ) -> Result>>; /// Parse a value from a string with the given options. fn parse_string_with_options( input: &'i str, - options: ParserOptions, + options: ParserOptions<'_, 'i, T>, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); @@ -234,3 +234,94 @@ pub trait Zero { /// Returns whether the value is zero. fn is_zero(&self) -> bool; } + +/// A trait to provide parsing of custom at-rules. +/// +/// For example, there could be different implementations for top-level at-rules +/// (`@media`, `@font-face`, …) +/// and for page-margin rules inside `@page`. +/// +/// Default implementations that reject all at-rules are provided, +/// so that `impl AtRuleParser<(), ()> for ... {}` can be used +/// for using `DeclarationListParser` to parse a declarations list with only qualified rules. +/// +/// Note: this trait is copied from cssparser and modified to provide parser options. +pub trait AtRuleParser<'i>: Sized { + /// The intermediate representation of prelude of an at-rule. + type Prelude; + + /// The finished representation of an at-rule. + type AtRule; + + /// The error type that is included in the ParseError value that can be returned. + type Error: 'i; + + /// Parse the prelude of an at-rule with the given `name`. + /// + /// Return the representation of the prelude and the type of at-rule, + /// or `Err(())` to ignore the entire at-rule as invalid. + /// + /// The prelude is the part after the at-keyword + /// and before the `;` semicolon or `{ /* ... */ }` block. + /// + /// At-rule name matching should be case-insensitive in the ASCII range. + /// This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + /// or with the `match_ignore_ascii_case!` macro. + /// + /// The given `input` is a "delimited" parser + /// that ends wherever the prelude should end. + /// (Before the next semicolon, the next `{`, or the end of the current block.) + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, Self>, + ) -> Result> { + let _ = name; + let _ = input; + let _ = options; + Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) + } + + /// End an at-rule which doesn't have block. Return the finished + /// representation of the at-rule. + /// + /// The location passed in is source location of the start of the prelude. + /// + /// This is only called when either the `;` semicolon indeed follows the prelude, + /// or parser is at the end of the input. + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + options: &ParserOptions<'_, 'i, Self>, + ) -> Result { + let _ = prelude; + let _ = start; + let _ = options; + Err(()) + } + + /// Parse the content of a `{ /* ... */ }` block for the body of the at-rule. + /// + /// The location passed in is source location of the start of the prelude. + /// + /// Return the finished representation of the at-rule + /// as returned by `RuleListParser::next` or `DeclarationListParser::next`, + /// or `Err(())` to ignore the entire at-rule as invalid. + /// + /// This is only called when a block was found following the prelude. + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, Self>, + ) -> Result> { + let _ = prelude; + let _ = start; + let _ = input; + let _ = options; + Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) + } +} diff --git a/tests/test_custom_parser.rs b/tests/test_custom_parser.rs index 9e5e8868..644c4150 100644 --- a/tests/test_custom_parser.rs +++ b/tests/test_custom_parser.rs @@ -1,10 +1,12 @@ +use std::sync::{Arc, RwLock}; + use cssparser::*; use lightningcss::{ declaration::DeclarationBlock, error::{ParserError, PrinterError}, printer::Printer, stylesheet::{ParserOptions, PrinterOptions, StyleSheet}, - traits::{Parse, ToCss}, + traits::{AtRuleParser, Parse, ToCss}, values::ident::Ident, }; @@ -12,7 +14,7 @@ fn minify_test(source: &str, expected: &str) { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - at_rule_parser: Some(TestAtRuleParser), + at_rule_parser: Some(Arc::new(RwLock::new(TestAtRuleParser))), ..Default::default() }, ) @@ -85,6 +87,7 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i, Self>, ) -> Result> { let location = input.current_source_location(); match_ignore_ascii_case! {&*name, @@ -102,7 +105,12 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { } } - fn rule_without_block(&mut self, prelude: Self::Prelude, _start: &ParserState) -> Result { + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + _start: &ParserState, + _options: &ParserOptions<'_, 'i, Self>, + ) -> Result { match prelude { Prelude::Inline(name) => Ok(AtRule::Inline(InlineRule { name })), _ => unreachable!(), @@ -114,6 +122,7 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { prelude: Self::Prelude, _start: &ParserState, input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i, Self>, ) -> Result> { match prelude { Prelude::Block(name) => Ok(AtRule::Block(BlockRule { From 2ce573653de733766c3240e0be571dd408b57083 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 11 Jan 2023 21:58:13 -0500 Subject: [PATCH 06/12] Implement support for parsing custom at rules in JS bindings --- node/index.d.ts | 56 ++++++++++- node/src/at_rule_parser.rs | 195 +++++++++++++++++++++++++++++++++++++ node/src/lib.rs | 35 +++++-- node/src/transformer.rs | 25 ++++- src/values/syntax.rs | 5 + test.js | 125 +++++++----------------- 6 files changed, 333 insertions(+), 108 deletions(-) create mode 100644 node/src/at_rule_parser.rs diff --git a/node/index.d.ts b/node/index.d.ts index 583966e9..fc3c4ef5 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -1,4 +1,4 @@ -import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock } from './ast'; +import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock, ParsedComponent } from './ast'; import type { Targets } from './targets'; export * from './ast'; @@ -55,7 +55,14 @@ export interface TransformOptions { * For optimal performance, visitors should be as specific as possible about what types of values * they care about so that JavaScript has to be called as little as possible. */ - visitor?: Visitor + visitor?: Visitor, + /** + * Defines how to parse custom CSS at-rules. Each at-rule can have a prelude, defined using a CSS + * [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), and + * a block body. The body can be a declaration list, rule list, or style block as defined in the + * [css spec](https://drafts.csswg.org/css-syntax/#declaration-rule-list). + */ + customAtRules?: CustomAtRules } // This is a hack to make TS still provide autocomplete for `property` vs. just making it `string`. @@ -79,12 +86,27 @@ type MappedRuleVisitors = { [Name in Exclude]?: RuleVisitor>>; } -type UnknownVisitors = { - [name: string]: RuleVisitor +type UnknownVisitors = { + [name: string]: RuleVisitor } type RuleVisitors = MappedRuleVisitors & { - unknown?: UnknownVisitors | RuleVisitor + unknown?: UnknownVisitors | RuleVisitor, + custom?: UnknownVisitors | RuleVisitor +}; + +interface CustomAtRule { + name: string, + prelude: ParsedComponent | null, + body: CustomAtRuleBody +} + +type CustomAtRuleBody = { + type: 'declaration-list', + value: DeclarationBlock +} | { + type: 'rule-list', + value: Rule[] }; type FindProperty = Union extends { property: Name } ? Union : never; @@ -143,6 +165,30 @@ export interface Visitor { EnvironmentVariableExit?: EnvironmentVariableVisitor | EnvironmentVariableVisitors; } +export interface CustomAtRules { + [name: string]: CustomAtRule +} + +export interface CustomAtRule { + /** + * Defines the syntax for a custom at-rule prelude. The value should be a + * CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings) + * representing the types of values that are accepted. This property may be omitted or + * set to null to indicate that no prelude is accepted. + */ + prelude?: string | null, + /** + * Defines the type of body contained within the at-rule block. + * - declaration-list: A CSS declaration list, as in a style rule. + * - rule-list: A list of CSS rules, as supported within a non-nested + * at-rule such as `@media` or `@supports`. + * - style-block: Both a declaration list and rule list, as accepted within + * a nested at-rule within a style rule (e.g. `@media` inside a style rule + * with directly nested declarations). + */ + body?: 'declaration-list' | 'rule-list' | 'style-block' | null +} + export interface DependencyOptions { /** Whether to preserve `@import` rules rather than removing them. */ preserveImports?: boolean diff --git a/node/src/at_rule_parser.rs b/node/src/at_rule_parser.rs new file mode 100644 index 00000000..82597fab --- /dev/null +++ b/node/src/at_rule_parser.rs @@ -0,0 +1,195 @@ +use std::collections::HashMap; + +use cssparser::*; +use lightningcss::{ + declaration::DeclarationBlock, + error::ParserError, + rules::CssRuleList, + stylesheet::ParserOptions, + traits::{AtRuleParser, ToCss}, + values::{ + string::CowArcStr, + syntax::{ParsedComponent, SyntaxString}, + }, + visitor::{Visit, VisitTypes, Visitor}, +}; +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Deserialize, Debug, Clone)] +pub struct CustomAtRuleConfig { + #[serde(default, deserialize_with = "deserialize_prelude")] + prelude: Option, + body: Option, +} + +fn deserialize_prelude<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = Option::>::deserialize(deserializer)?; + if let Some(s) = s { + Ok(Some( + SyntaxString::parse_string(&s).map_err(|_| serde::de::Error::custom("invalid syntax string"))?, + )) + } else { + Ok(None) + } +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +enum CustomAtRuleBodyType { + DeclarationList, + RuleList, + StyleBlock, +} + +pub struct Prelude<'i> { + name: CowArcStr<'i>, + prelude: Option>, +} + +#[derive(Serialize, Deserialize)] +pub struct AtRule<'i> { + #[serde(borrow)] + pub name: CowArcStr<'i>, + pub prelude: Option>, + pub body: Option>, +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type", content = "value", rename_all = "kebab-case")] +pub enum AtRuleBody<'i> { + #[serde(borrow)] + DeclarationList(DeclarationBlock<'i>), + RuleList(CssRuleList<'i, AtRule<'i>>), +} + +#[derive(Clone)] +pub struct CustomAtRuleParser { + pub configs: HashMap, +} + +impl<'i> AtRuleParser<'i> for CustomAtRuleParser { + type Prelude = Prelude<'i>; + type Error = ParserError<'i>; + type AtRule = AtRule<'i>; + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i, Self>, + ) -> Result> { + if let Some(config) = self.configs.get(name.as_ref()) { + let prelude = if let Some(prelude) = &config.prelude { + Some(prelude.parse_value(input)?) + } else { + None + }; + Ok(Prelude { + name: name.into(), + prelude, + }) + } else { + Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) + } + } + + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + _start: &ParserState, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i, Self>, + ) -> Result> { + let config = self.configs.get(prelude.name.as_ref()).unwrap(); + let body = if let Some(body) = &config.body { + match body { + CustomAtRuleBodyType::DeclarationList => { + Some(AtRuleBody::DeclarationList(DeclarationBlock::parse(input, options)?)) + } + CustomAtRuleBodyType::RuleList => Some(AtRuleBody::RuleList(CssRuleList::parse(input, options)?)), + CustomAtRuleBodyType::StyleBlock => { + Some(AtRuleBody::RuleList(CssRuleList::parse_style_block(input, options)?)) + } + } + } else { + None + }; + + Ok(AtRule { + name: prelude.name, + prelude: prelude.prelude, + body, + }) + } + + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + _start: &ParserState, + _options: &ParserOptions<'_, 'i, Self>, + ) -> Result { + let config = self.configs.get(prelude.name.as_ref()).unwrap(); + if config.body.is_some() { + return Err(()); + } + + Ok(AtRule { + name: prelude.name, + prelude: prelude.prelude, + body: None, + }) + } +} + +impl<'i> ToCss for AtRule<'i> { + fn to_css( + &self, + dest: &mut lightningcss::printer::Printer, + ) -> Result<(), lightningcss::error::PrinterError> + where + W: std::fmt::Write, + { + dest.write_char('@')?; + serialize_identifier(&self.name, dest)?; + if let Some(prelude) = &self.prelude { + dest.write_char(' ')?; + prelude.to_css(dest)?; + } + + if let Some(body) = &self.body { + match body { + AtRuleBody::DeclarationList(decls) => { + decls.to_css_block(dest)?; + } + AtRuleBody::RuleList(rules) => { + dest.whitespace()?; + dest.write_char('{')?; + dest.indent(); + dest.newline()?; + rules.to_css(dest)?; + dest.dedent(); + dest.newline()?; + dest.write_char('}')?; + } + } + } + + Ok(()) + } +} + +impl<'i, V: Visitor<'i, AtRule<'i>>> Visit<'i, AtRule<'i>, V> for AtRule<'i> { + const CHILD_TYPES: VisitTypes = VisitTypes::empty(); + + fn visit_children(&mut self, visitor: &mut V) { + self.prelude.visit(visitor); + match &mut self.body { + Some(AtRuleBody::DeclarationList(decls)) => decls.visit(visitor), + Some(AtRuleBody::RuleList(rules)) => rules.visit(visitor), + None => {} + } + } +} diff --git a/node/src/lib.rs b/node/src/lib.rs index fc752c6e..fc39c299 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -2,6 +2,7 @@ #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; +use at_rule_parser::{CustomAtRuleConfig, CustomAtRuleParser}; use lightningcss::bundler::{BundleErrorKind, Bundler, FileProvider, SourceProvider}; use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError}; use lightningcss::dependencies::{Dependency, DependencyOptions}; @@ -13,13 +14,14 @@ use lightningcss::targets::Browsers; use lightningcss::visitor::Visit; use parcel_sourcemap::SourceMap; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::{Arc, Mutex, RwLock}; use transformer::JsVisitor; +mod at_rule_parser; #[cfg(not(target_arch = "wasm32"))] mod threadsafe_function; mod transformer; @@ -242,7 +244,7 @@ mod bundle { } struct VisitMessage { - stylesheet: &'static mut StyleSheet<'static, 'static>, + stylesheet: &'static mut StyleSheet<'static, 'static, CustomAtRuleParser>, tx: Sender>, } @@ -396,15 +398,16 @@ mod bundle { unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) }, &config, tsfn.map(move |tsfn| { - move |stylesheet: &mut StyleSheet| { + move |stylesheet: &mut StyleSheet| { CHANNEL.with(|channel| { let message = VisitMessage { // SAFETY: we immediately lock the thread until we get a response, // so stylesheet cannot be dropped in that time. stylesheet: unsafe { - std::mem::transmute::<&'_ mut StyleSheet<'_, '_>, &'static mut StyleSheet<'static, 'static>>( - stylesheet, - ) + std::mem::transmute::< + &'_ mut StyleSheet<'_, '_, CustomAtRuleParser>, + &'static mut StyleSheet<'static, 'static, CustomAtRuleParser>, + >(stylesheet) }, tx: channel.0.clone(), }; @@ -490,6 +493,7 @@ struct Config { pub pseudo_classes: Option, pub unused_symbols: Option>, pub error_recovery: Option, + pub custom_at_rules: Option>, } #[derive(Debug, Deserialize)] @@ -614,7 +618,13 @@ fn compile<'i>( source_index: 0, error_recovery: config.error_recovery.unwrap_or_default(), warnings: warnings.clone(), - at_rule_parser: ParserOptions::default_at_rule_parser(), + at_rule_parser: if let Some(custom_at_rules) = &config.custom_at_rules { + Some(Arc::new(RwLock::new(CustomAtRuleParser { + configs: custom_at_rules.clone(), + }))) + } else { + None + }, }, )?; @@ -677,7 +687,12 @@ fn compile<'i>( }) } -fn compile_bundle<'i, 'o, P: SourceProvider, F: FnOnce(&mut StyleSheet<'i, 'o>) -> napi::Result<()>>( +fn compile_bundle< + 'i, + 'o, + P: SourceProvider, + F: FnOnce(&mut StyleSheet<'i, 'o, CustomAtRuleParser>) -> napi::Result<()>, +>( fs: &'i P, config: &'o BundleConfig, visit: Option, @@ -715,7 +730,9 @@ fn compile_bundle<'i, 'o, P: SourceProvider, F: FnOnce(&mut StyleSheet<'i, 'o>) }, error_recovery: config.error_recovery.unwrap_or_default(), warnings: warnings.clone(), - ..ParserOptions::default() + at_rule_parser: None, + filename: String::new(), + source_index: 0, }; let mut bundler = Bundler::new(fs, source_map.as_mut(), parser_options); diff --git a/node/src/transformer.rs b/node/src/transformer.rs index 4aabf887..977c63d8 100644 --- a/node/src/transformer.rs +++ b/node/src/transformer.rs @@ -17,6 +17,8 @@ use napi::{Env, JsFunction, JsObject, JsUnknown, Ref, ValueType}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; +use crate::at_rule_parser::AtRule; + pub struct JsVisitor { env: Env, visit_rule: VisitorsRef, @@ -237,7 +239,7 @@ impl JsVisitor { } } -impl<'i> Visitor<'i> for JsVisitor { +impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { type Error = napi::Error; const TYPES: lightningcss::visitor::VisitTypes = VisitTypes::all(); @@ -246,7 +248,7 @@ impl<'i> Visitor<'i> for JsVisitor { self.types } - fn visit_rule_list(&mut self, rules: &mut lightningcss::rules::CssRuleList<'i>) -> Result<(), Self::Error> { + fn visit_rule_list(&mut self, rules: &mut lightningcss::rules::CssRuleList<'i, AtRule<'i>>) -> Result<(), Self::Error> { if self.types.contains(VisitTypes::RULES) { let env = self.env; let rule_map = self.rule_map.get::(&env); @@ -284,6 +286,25 @@ impl<'i> Visitor<'i> for JsVisitor { } else { "unknown" } + CssRule::Custom(c) => { + let name = c.name.as_ref(); + if let Some(visit) = rule_map.custom(stage, "custom", name) { + let js_value = env.to_js_value(c)?; + let res = visit.call(None, &[js_value])?; + return env.from_js_value(res).map(serde_detach::detach); + } else { + "custom" + } + } + CssRule::Ignored => return Ok(None), + }; + + if let Some(visit) = rule_map.named(stage, name).as_ref().or(visit_rule.for_stage(stage)) { + let js_value = env.to_js_value(value)?; + let res = visit.call(None, &[js_value])?; + env.from_js_value(res).map(serde_detach::detach) + } else { + Ok(None) } CssRule::Ignored | CssRule::Custom(..) => return Ok(None), }; diff --git a/src/values/syntax.rs b/src/values/syntax.rs index a4df102f..5dede895 100644 --- a/src/values/syntax.rs +++ b/src/values/syntax.rs @@ -6,6 +6,8 @@ use crate::error::{ParserError, PrinterError}; use crate::printer::Printer; use crate::traits::{Parse, ToCss}; use crate::values; +#[cfg(feature = "visitor")] +use crate::visitor::Visit; use cssparser::*; /// A CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings) @@ -83,6 +85,7 @@ pub enum SyntaxComponentKind { /// A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a /// [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated. #[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), @@ -100,6 +103,7 @@ pub enum Multiplier { /// A parsed value for a [SyntaxComponent](SyntaxComponent). #[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr( feature = "serde", @@ -142,6 +146,7 @@ pub enum ParsedComponent<'i> { /// A repeated component value. Repeated { /// The components to repeat. + #[cfg_attr(feature = "visitor", skip_type)] components: Vec>, /// A multiplier describing how the components repeat. multiplier: Multiplier, diff --git a/test.js b/test.js index bcfbada3..cdb2d1c0 100644 --- a/test.js +++ b/test.js @@ -30,108 +30,49 @@ if (process.argv[process.argv.length - 1] !== __filename) { let res = css.transform({ filename: __filename, - // minify: true, - // targets: { - // safari: 4 << 16, - // firefox: 3 << 16 | 5 << 8, - // opera: 10 << 16 | 5 << 8 - // }, code: Buffer.from(` - @namespace "http://foo.com"; - @namespace svg "http://bar.com"; - - .selector > .nested[data-foo=bar]:not(.foo):hover::part(tab active) { - width: 32px; - --foo: var(--bar, 30px); - background: linear-gradient(red, green); - } - - svg|foo { - test: foo; + @breakpoints { + .foo { color: yellow; } } - @media (hover) and (width > 50px) { - .foo { - color: red; - background: inline('.gitignore'); + .foo { + color: red; + @bar { + width: 25px; } } `), - visitor: { - visitLength(length) { - if (length.unit === 'px') { - return { - unit: 'rem', - value: length.value / 16 - } - } - return length; - }, - visitColor(color) { - console.log(color); - return color; - }, - visitImage(image) { - // console.log(image.value.value); - image.value.value[1].push('moz'); - return image; - }, - visitProperty(property) { - // console.log(require('util').inspect(property, {depth: 50})) - if (property.property === 'background') { - property.value[0].repeat.x = 'no-repeat'; - property.value[0].repeat.y = 'no-repeat'; - } - - return property; - }, - // visitRule(rule) { - // console.log(require('util').inspect(rule, {depth: 10})); - // if (rule.type === 'style') { - // for (let selector of rule.value.selectors) { - // for (let component of selector) { - // if (component.type === 'class') { - // component.value = 'tw-' + component.value; - // } - // } - // } - // } - // return rule; - // }, - visitMediaQuery(query) { - // console.log(query); - query.media_type = 'print'; - return query; + drafts: { + nesting: true + }, + targets: { + safari: 16 << 16 + }, + customAtRules: { + breakpoints: { + // Syntax string defining the at rule prelude. + // https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings + prelude: null, + // Type of the at rule block. + // Can be declaration-list, rule-list, or style-block. + // https://www.w3.org/TR/css-syntax-3/#declaration-rule-list + body: 'rule-list' }, - visitFunction(fn) { - // console.log(require('util').inspect(fn, {depth: 50})); - if (fn.name === 'inline') { - return { - name: 'url', - arguments: [{ - type: 'token', - value: { - type: 'string', - value: fs.readFileSync(fn.arguments[0].value.value).toString('base64'), - } - }] - } + bar: { + body: 'style-block' + } + }, + visitor: { + Rule: { + custom(rule) { + console.log(rule.body); } - return fn; }, - visitSelector(selector) { - console.log(require('util').inspect(selector, { depth: 10 })); - for (let component of selector) { - if (component.type === 'class') { - component.name = 'tw-' + component.name; - } else if (component.type === 'attribute') { - component.name = 'tw-' + component.name; - component.operation.operator = 'includes'; - } - } - return selector; + Length(length) { + length.value *= 2; + return length; } - }, + } }); console.log(res.code.toString()); From b57286684d3cbc39b96df9678c2445587dea322e Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 9 Feb 2023 23:35:58 -0500 Subject: [PATCH 07/12] Fixup --- node/src/at_rule_parser.rs | 27 ++++++++++++++++++++------- node/src/lib.rs | 2 +- node/src/transformer.rs | 34 ++++++++++++++-------------------- 3 files changed, 35 insertions(+), 28 deletions(-) diff --git a/node/src/at_rule_parser.rs b/node/src/at_rule_parser.rs index 82597fab..5eac8ec2 100644 --- a/node/src/at_rule_parser.rs +++ b/node/src/at_rule_parser.rs @@ -4,7 +4,7 @@ use cssparser::*; use lightningcss::{ declaration::DeclarationBlock, error::ParserError, - rules::CssRuleList, + rules::{CssRuleList, Location}, stylesheet::ParserOptions, traits::{AtRuleParser, ToCss}, values::{ @@ -55,6 +55,7 @@ pub struct AtRule<'i> { pub name: CowArcStr<'i>, pub prelude: Option>, pub body: Option>, + pub loc: Location, } #[derive(Serialize, Deserialize)] @@ -99,7 +100,7 @@ impl<'i> AtRuleParser<'i> for CustomAtRuleParser { fn parse_block<'t>( &mut self, prelude: Self::Prelude, - _start: &ParserState, + start: &ParserState, input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i, Self>, ) -> Result> { @@ -118,28 +119,40 @@ impl<'i> AtRuleParser<'i> for CustomAtRuleParser { None }; + let loc = start.source_location(); Ok(AtRule { name: prelude.name, prelude: prelude.prelude, body, + loc: Location { + source_index: options.source_index, + line: loc.line, + column: loc.column, + }, }) } fn rule_without_block( &mut self, prelude: Self::Prelude, - _start: &ParserState, - _options: &ParserOptions<'_, 'i, Self>, + start: &ParserState, + options: &ParserOptions<'_, 'i, Self>, ) -> Result { let config = self.configs.get(prelude.name.as_ref()).unwrap(); if config.body.is_some() { return Err(()); } + let loc = start.source_location(); Ok(AtRule { name: prelude.name, prelude: prelude.prelude, body: None, + loc: Location { + source_index: options.source_index, + line: loc.line, + column: loc.column, + }, }) } } @@ -184,12 +197,12 @@ impl<'i> ToCss for AtRule<'i> { impl<'i, V: Visitor<'i, AtRule<'i>>> Visit<'i, AtRule<'i>, V> for AtRule<'i> { const CHILD_TYPES: VisitTypes = VisitTypes::empty(); - fn visit_children(&mut self, visitor: &mut V) { - self.prelude.visit(visitor); + fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> { + self.prelude.visit(visitor)?; match &mut self.body { Some(AtRuleBody::DeclarationList(decls)) => decls.visit(visitor), Some(AtRuleBody::RuleList(rules)) => rules.visit(visitor), - None => {} + None => Ok(()), } } } diff --git a/node/src/lib.rs b/node/src/lib.rs index fc39c299..9625a9b1 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -133,7 +133,7 @@ mod bundle { &config, visitor .as_mut() - .map(|visitor| |stylesheet: &mut StyleSheet| stylesheet.visit(visitor)), + .map(|visitor| |stylesheet: &mut StyleSheet| stylesheet.visit(visitor)), ); match res { diff --git a/node/src/transformer.rs b/node/src/transformer.rs index 977c63d8..f65cd092 100644 --- a/node/src/transformer.rs +++ b/node/src/transformer.rs @@ -248,7 +248,10 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { self.types } - fn visit_rule_list(&mut self, rules: &mut lightningcss::rules::CssRuleList<'i, AtRule<'i>>) -> Result<(), Self::Error> { + fn visit_rule_list( + &mut self, + rules: &mut lightningcss::rules::CssRuleList<'i, AtRule<'i>>, + ) -> Result<(), Self::Error> { if self.types.contains(VisitTypes::RULES) { let env = self.env; let rule_map = self.rule_map.get::(&env); @@ -286,27 +289,18 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { } else { "unknown" } - CssRule::Custom(c) => { - let name = c.name.as_ref(); - if let Some(visit) = rule_map.custom(stage, "custom", name) { - let js_value = env.to_js_value(c)?; - let res = visit.call(None, &[js_value])?; - return env.from_js_value(res).map(serde_detach::detach); - } else { - "custom" - } + } + CssRule::Custom(c) => { + let name = c.name.as_ref(); + if let Some(visit) = rule_map.custom(stage, "custom", name) { + let js_value = env.to_js_value(c)?; + let res = visit.call(None, &[js_value])?; + return env.from_js_value(res).map(serde_detach::detach); + } else { + "custom" } - CssRule::Ignored => return Ok(None), - }; - - if let Some(visit) = rule_map.named(stage, name).as_ref().or(visit_rule.for_stage(stage)) { - let js_value = env.to_js_value(value)?; - let res = visit.call(None, &[js_value])?; - env.from_js_value(res).map(serde_detach::detach) - } else { - Ok(None) } - CssRule::Ignored | CssRule::Custom(..) => return Ok(None), + CssRule::Ignored => return Ok(None), }; if let Some(visit) = rule_map.named(stage, name).as_ref().or(visit_rule.for_stage(stage)) { From 283d831a477c122802a8e76656cd266c8f4718d0 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 10 Feb 2023 01:08:15 -0500 Subject: [PATCH 08/12] Test --- node/index.d.ts | 73 +++++++++++++------- node/test/customAtRules.mjs | 131 ++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 node/test/customAtRules.mjs diff --git a/node/index.d.ts b/node/index.d.ts index fc3c4ef5..3407a207 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -1,9 +1,9 @@ -import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock, ParsedComponent } from './ast'; +import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock, ParsedComponent, Multiplier } from './ast'; import type { Targets } from './targets'; export * from './ast'; -export interface TransformOptions { +export interface TransformOptions { /** The filename being transformed. Used for error messages and source maps. */ filename: string, /** The source code to transform. */ @@ -55,14 +55,14 @@ export interface TransformOptions { * For optimal performance, visitors should be as specific as possible about what types of values * they care about so that JavaScript has to be called as little as possible. */ - visitor?: Visitor, + visitor?: Visitor, /** * Defines how to parse custom CSS at-rules. Each at-rule can have a prelude, defined using a CSS * [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), and * a block body. The body can be a declaration list, rule list, or style block as defined in the * [css spec](https://drafts.csswg.org/css-syntax/#declaration-rule-list). */ - customAtRules?: CustomAtRules + customAtRules?: C } // This is a hack to make TS still provide autocomplete for `property` vs. just making it `string`. @@ -90,15 +90,42 @@ type UnknownVisitors = { [name: string]: RuleVisitor } -type RuleVisitors = MappedRuleVisitors & { +type CustomVisitors = { + [Name in keyof T]?: RuleVisitor> +}; + +type AnyCustomAtRule = { + [Key in keyof C]: CustomAtRule +}[keyof C]; + +type RuleVisitors = MappedRuleVisitors & { unknown?: UnknownVisitors | RuleVisitor, - custom?: UnknownVisitors | RuleVisitor + custom?: CustomVisitors | RuleVisitor> }; -interface CustomAtRule { - name: string, - prelude: ParsedComponent | null, - body: CustomAtRuleBody +type PreludeTypes = Exclude; +type SyntaxString = `<${PreludeTypes}>` | `<${PreludeTypes}>+` | `<${PreludeTypes}>#` | (string & {}); +type ComponentTypes = { + [Key in PreludeTypes as `<${Key}>`]: FindByType +}; + +type Repetitions = { + [Key in PreludeTypes as `<${Key}>+` | `<${Key}>#`]: { + type: "repeated", + value: { + components: FindByType[], + multiplier: Multiplier + } + } +}; + +type MappedPrelude = ComponentTypes & Repetitions; +type MappedBody

= P extends 'style-block' ? 'rule-list' : P; +interface CustomAtRule { + name: N, + prelude: R['prelude'] extends keyof MappedPrelude ? MappedPrelude[R['prelude']] : ParsedComponent, + body: FindByType>, + loc: Location } type CustomAtRuleBody = { @@ -135,9 +162,9 @@ type EnvironmentVariableVisitors = { [name: string]: EnvironmentVariableVisitor }; -export interface Visitor { - Rule?: RuleVisitor | RuleVisitors; - RuleExit?: RuleVisitor | RuleVisitors; +export interface Visitor { + Rule?: RuleVisitor | RuleVisitors; + RuleExit?: RuleVisitor | RuleVisitors; Declaration?: DeclarationVisitor | DeclarationVisitors; DeclarationExit?: DeclarationVisitor | DeclarationVisitors; Url?(url: Url): Url | void; @@ -166,17 +193,17 @@ export interface Visitor { } export interface CustomAtRules { - [name: string]: CustomAtRule + [name: string]: CustomAtRuleDefinition } -export interface CustomAtRule { +export interface CustomAtRuleDefinition { /** * Defines the syntax for a custom at-rule prelude. The value should be a * CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings) * representing the types of values that are accepted. This property may be omitted or * set to null to indicate that no prelude is accepted. */ - prelude?: string | null, + prelude?: SyntaxString | null, /** * Defines the type of body contained within the at-rule block. * - declaration-list: A CSS declaration list, as in a style rule. @@ -194,9 +221,9 @@ export interface DependencyOptions { preserveImports?: boolean } -export type BundleOptions = Omit; +export type BundleOptions = Omit, 'code'>; -export interface BundleAsyncOptions extends BundleOptions { +export interface BundleAsyncOptions extends BundleOptions { resolver?: Resolver; } @@ -345,7 +372,7 @@ export interface ErrorLocation extends Location { * Compiles a CSS file, including optionally minifying and lowering syntax to the given * targets. A source map may also be generated, but this is not enabled by default. */ -export declare function transform(options: TransformOptions): TransformResult; +export declare function transform(options: TransformOptions): TransformResult; export interface TransformAttributeOptions { /** The filename in which the style attribute appeared. Used for error messages and dependencies. */ @@ -375,7 +402,7 @@ export interface TransformAttributeOptions { * For optimal performance, visitors should be as specific as possible about what types of values * they care about so that JavaScript has to be called as little as possible. */ - visitor?: Visitor + visitor?: Visitor } export interface TransformAttributeResult { @@ -401,14 +428,14 @@ export declare function browserslistToTargets(browserslist: string[]): Targets; /** * Bundles a CSS file and its dependencies, inlining @import rules. */ -export declare function bundle(options: BundleOptions): TransformResult; +export declare function bundle(options: BundleOptions): TransformResult; /** * Bundles a CSS file and its dependencies asynchronously, inlining @import rules. */ -export declare function bundleAsync(options: BundleAsyncOptions): Promise; +export declare function bundleAsync(options: BundleAsyncOptions): Promise; /** * Composes multiple visitor objects into a single one. */ -export declare function composeVisitors(visitors: Visitor[]): Visitor; +export declare function composeVisitors(visitors: Visitor[]): Visitor; diff --git a/node/test/customAtRules.mjs b/node/test/customAtRules.mjs new file mode 100644 index 00000000..ab54f84e --- /dev/null +++ b/node/test/customAtRules.mjs @@ -0,0 +1,131 @@ +// @ts-check + +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { transform } from '../index.mjs'; + +test('declaration list', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @theme spacing { + foo: 16px; + bar: 32px; + } + `), + customAtRules: { + theme: { + prelude: '', + body: 'declaration-list' + } + }, + visitor: { + Rule: { + custom: { + theme(rule) { + return { + type: 'style', + value: { + selectors: [[{ type: 'pseudo-class', kind: 'root' }]], + declarations: rule.body.value, + loc: rule.loc + } + } + } + } + } + } + }); + + assert.equal(res.code.toString(), ':root{foo:16px;bar:32px}'); +}); + +test('rule list', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @breakpoint 1024px { + .foo { color: yellow; } + } + `), + customAtRules: { + breakpoint: { + prelude: '', + body: 'rule-list' + } + }, + visitor: { + Rule: { + custom: { + breakpoint(rule) { + return { + type: 'media', + value: { + query: { + mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }] + }, + rules: rule.body.value, + loc: rule.loc + } + } + } + } + } + } + }); + + assert.equal(res.code.toString(), '@media (width<=1024px){.foo{color:#ff0}}'); +}); + + +test('style block', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + .foo { + @breakpoint 1024px { + color: yellow; + + &.bar { + color: red; + } + } + } + `), + drafts: { + nesting: true + }, + targets: { + chrome: 105 << 16 + }, + customAtRules: { + breakpoint: { + prelude: '', + body: 'style-block' + } + }, + visitor: { + Rule: { + custom: { + breakpoint(rule) { + return { + type: 'media', + value: { + query: { + mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }] + }, + rules: rule.body.value, + loc: rule.loc + } + } + } + } + } + } + }); + + assert.equal(res.code.toString(), '@media (width<=1024px){.foo{color:#ff0}.foo.bar{color:red}}'); +}); From 123763bc69ce4ee9358112a7656e467d0afaaf34 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 11 Feb 2023 12:00:31 -0500 Subject: [PATCH 09/12] Fix deadlock with nested custom rules --- c/src/lib.rs | 1 - examples/custom_at_rule.rs | 14 +-- node/src/at_rule_parser.rs | 14 +-- node/src/lib.rs | 47 ++++++---- node/test/customAtRules.mjs | 54 +++++++++++ src/bundler.rs | 59 ++++++++++-- src/declaration.rs | 20 ++-- src/parser.rs | 182 +++++++++++++++++------------------- src/properties/custom.rs | 36 +++---- src/properties/mod.rs | 4 +- src/rules/mod.rs | 74 ++++++++++++--- src/rules/page.rs | 12 +-- src/selector.rs | 14 +-- src/stylesheet.rs | 58 ++++++------ src/traits.rs | 16 ++-- src/values/ident.rs | 4 +- tests/test_custom_parser.rs | 15 +-- 17 files changed, 374 insertions(+), 250 deletions(-) diff --git a/c/src/lib.rs b/c/src/lib.rs index d57db8c8..5b21bfdc 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -285,7 +285,6 @@ pub extern "C" fn lightningcss_stylesheet_parse( error_recovery: options.error_recovery, source_index: 0, warnings: Some(warnings.clone()), - at_rule_parser: None, }; let stylesheet = unwrap!(StyleSheet::parse(code, opts), error, std::ptr::null_mut()); diff --git a/examples/custom_at_rule.rs b/examples/custom_at_rule.rs index cb3043ab..a5f73806 100644 --- a/examples/custom_at_rule.rs +++ b/examples/custom_at_rule.rs @@ -25,17 +25,11 @@ fn main() { let args: Vec = std::env::args().collect(); let source = std::fs::read_to_string(&args[1]).unwrap(); let opts = ParserOptions { - at_rule_parser: Some(Arc::new(RwLock::new(TailwindAtRuleParser))), filename: args[1].clone(), - nesting: true, - custom_media: false, - css_modules: None, - error_recovery: false, - warnings: None, - source_index: 0, + ..Default::default() }; - let mut stylesheet = StyleSheet::parse(&source, opts).unwrap(); + let mut stylesheet = StyleSheet::parse_with(&source, opts, &mut TailwindAtRuleParser).unwrap(); println!("{:?}", stylesheet); @@ -107,7 +101,7 @@ impl<'i> AtRuleParser<'i> for TailwindAtRuleParser { &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, - _options: &ParserOptions<'_, 'i, Self>, + _options: &ParserOptions<'_, 'i>, ) -> Result> { match_ignore_ascii_case! {&*name, "tailwind" => { @@ -144,7 +138,7 @@ impl<'i> AtRuleParser<'i> for TailwindAtRuleParser { &mut self, prelude: Self::Prelude, start: &ParserState, - _options: &ParserOptions<'_, 'i, Self>, + _options: &ParserOptions<'_, 'i>, ) -> Result { let loc = start.source_location(); match prelude { diff --git a/node/src/at_rule_parser.rs b/node/src/at_rule_parser.rs index 5eac8ec2..62b053b4 100644 --- a/node/src/at_rule_parser.rs +++ b/node/src/at_rule_parser.rs @@ -80,7 +80,7 @@ impl<'i> AtRuleParser<'i> for CustomAtRuleParser { &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, - _options: &ParserOptions<'_, 'i, Self>, + _options: &ParserOptions<'_, 'i>, ) -> Result> { if let Some(config) = self.configs.get(name.as_ref()) { let prelude = if let Some(prelude) = &config.prelude { @@ -102,7 +102,7 @@ impl<'i> AtRuleParser<'i> for CustomAtRuleParser { prelude: Self::Prelude, start: &ParserState, input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, Self>, + options: &ParserOptions<'_, 'i>, ) -> Result> { let config = self.configs.get(prelude.name.as_ref()).unwrap(); let body = if let Some(body) = &config.body { @@ -110,10 +110,12 @@ impl<'i> AtRuleParser<'i> for CustomAtRuleParser { CustomAtRuleBodyType::DeclarationList => { Some(AtRuleBody::DeclarationList(DeclarationBlock::parse(input, options)?)) } - CustomAtRuleBodyType::RuleList => Some(AtRuleBody::RuleList(CssRuleList::parse(input, options)?)), - CustomAtRuleBodyType::StyleBlock => { - Some(AtRuleBody::RuleList(CssRuleList::parse_style_block(input, options)?)) + CustomAtRuleBodyType::RuleList => { + Some(AtRuleBody::RuleList(CssRuleList::parse_with(input, options, self)?)) } + CustomAtRuleBodyType::StyleBlock => Some(AtRuleBody::RuleList(CssRuleList::parse_style_block_with( + input, options, self, + )?)), } } else { None @@ -136,7 +138,7 @@ impl<'i> AtRuleParser<'i> for CustomAtRuleParser { &mut self, prelude: Self::Prelude, start: &ParserState, - options: &ParserOptions<'_, 'i, Self>, + options: &ParserOptions<'_, 'i>, ) -> Result { let config = self.configs.get(prelude.name.as_ref()).unwrap(); if config.body.is_some() { diff --git a/node/src/lib.rs b/node/src/lib.rs index 9625a9b1..ef290bca 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -2,7 +2,7 @@ #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; -use at_rule_parser::{CustomAtRuleConfig, CustomAtRuleParser}; +use at_rule_parser::{AtRule, CustomAtRuleConfig, CustomAtRuleParser}; use lightningcss::bundler::{BundleErrorKind, Bundler, FileProvider, SourceProvider}; use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError}; use lightningcss::dependencies::{Dependency, DependencyOptions}; @@ -128,12 +128,20 @@ mod bundle { let config: BundleConfig = ctx.env.from_js_value(opts)?; let fs = FileProvider::new(); + + // This is pretty silly, but works around a rust limitation that you cannot + // explicitly annotate lifetime bounds on closures. + fn annotate<'i, 'o, F>(f: F) -> F + where + F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, + { + f + } + let res = compile_bundle( &fs, &config, - visitor - .as_mut() - .map(|visitor| |stylesheet: &mut StyleSheet| stylesheet.visit(visitor)), + visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), ); match res { @@ -244,7 +252,7 @@ mod bundle { } struct VisitMessage { - stylesheet: &'static mut StyleSheet<'static, 'static, CustomAtRuleParser>, + stylesheet: &'static mut StyleSheet<'static, 'static, AtRule<'static>>, tx: Sender>, } @@ -398,15 +406,15 @@ mod bundle { unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) }, &config, tsfn.map(move |tsfn| { - move |stylesheet: &mut StyleSheet| { + move |stylesheet: &mut StyleSheet| { CHANNEL.with(|channel| { let message = VisitMessage { // SAFETY: we immediately lock the thread until we get a response, // so stylesheet cannot be dropped in that time. stylesheet: unsafe { std::mem::transmute::< - &'_ mut StyleSheet<'_, '_, CustomAtRuleParser>, - &'static mut StyleSheet<'static, 'static, CustomAtRuleParser>, + &'_ mut StyleSheet<'_, '_, AtRule>, + &'static mut StyleSheet<'static, 'static, AtRule>, >(stylesheet) }, tx: channel.0.clone(), @@ -537,6 +545,7 @@ struct BundleConfig { pub pseudo_classes: Option, pub unused_symbols: Option>, pub error_recovery: Option, + pub custom_at_rules: Option>, } #[derive(Debug, Deserialize)] @@ -590,7 +599,7 @@ fn compile<'i>( }; let res = { - let mut stylesheet = StyleSheet::parse( + let mut stylesheet = StyleSheet::parse_with( &code, ParserOptions { filename: filename.clone(), @@ -618,13 +627,9 @@ fn compile<'i>( source_index: 0, error_recovery: config.error_recovery.unwrap_or_default(), warnings: warnings.clone(), - at_rule_parser: if let Some(custom_at_rules) = &config.custom_at_rules { - Some(Arc::new(RwLock::new(CustomAtRuleParser { - configs: custom_at_rules.clone(), - }))) - } else { - None - }, + }, + &mut CustomAtRuleParser { + configs: config.custom_at_rules.clone().unwrap_or_default(), }, )?; @@ -691,7 +696,7 @@ fn compile_bundle< 'i, 'o, P: SourceProvider, - F: FnOnce(&mut StyleSheet<'i, 'o, CustomAtRuleParser>) -> napi::Result<()>, + F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, >( fs: &'i P, config: &'o BundleConfig, @@ -730,12 +735,16 @@ fn compile_bundle< }, error_recovery: config.error_recovery.unwrap_or_default(), warnings: warnings.clone(), - at_rule_parser: None, filename: String::new(), source_index: 0, }; - let mut bundler = Bundler::new(fs, source_map.as_mut(), parser_options); + let mut at_rule_parser = CustomAtRuleParser { + configs: config.custom_at_rules.clone().unwrap_or_default(), + }; + + let mut bundler = + Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser); let mut stylesheet = bundler.bundle(Path::new(&config.filename))?; if let Some(visit) = visit { diff --git a/node/test/customAtRules.mjs b/node/test/customAtRules.mjs index ab54f84e..0f8855a6 100644 --- a/node/test/customAtRules.mjs +++ b/node/test/customAtRules.mjs @@ -129,3 +129,57 @@ test('style block', () => { assert.equal(res.code.toString(), '@media (width<=1024px){.foo{color:#ff0}.foo.bar{color:red}}'); }); + +test('multiple', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @breakpoint 1024px { + @theme spacing { + foo: 16px; + bar: 32px; + } + } + `), + customAtRules: { + breakpoint: { + prelude: '', + body: 'rule-list' + }, + theme: { + prelude: '', + body: 'declaration-list' + } + }, + visitor: { + Rule: { + custom(rule) { + if (rule.name === 'breakpoint') { + return { + type: 'media', + value: { + query: { + mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }] + }, + rules: rule.body.value, + loc: rule.loc + } + } + } else { + return { + type: 'style', + value: { + selectors: [[{ type: 'pseudo-class', kind: 'root' }]], + declarations: rule.body.value, + loc: rule.loc + } + } + } + } + } + } + }); + + assert.equal(res.code.toString(), '@media (width<=1024px){:root{foo:16px;bar:32px}}'); +}); diff --git a/src/bundler.rs b/src/bundler.rs index cbc5ae8c..bbe01314 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -25,6 +25,7 @@ use crate::{ error::ErrorLocation, + parser::DefaultAtRuleParser, properties::{ css_modules::Specifier, custom::{ @@ -66,11 +67,17 @@ pub struct Bundler<'a, 'o, 's, P, T: AtRuleParser<'a>> { source_map: Option>, fs: &'a P, source_indexes: DashMap, - stylesheets: Mutex>>, - options: ParserOptions<'o, 'a, T>, + stylesheets: Mutex>>, + options: ParserOptions<'o, 'a>, + at_rule_parser: Mutex>, } -struct BundleStyleSheet<'i, 'o, T: AtRuleParser<'i>> { +enum AtRuleParserValue<'a, T> { + Owned(T), + Borrowed(&'a mut T), +} + +struct BundleStyleSheet<'i, 'o, T> { stylesheet: Option>, dependencies: Vec, css_modules_deps: Vec, @@ -189,6 +196,26 @@ impl<'i, T: std::error::Error> BundleErrorKind<'i, T> { } } +impl<'a, 'o, 's, P: SourceProvider> Bundler<'a, 'o, 's, P, DefaultAtRuleParser> { + /// Creates a new Bundler using the given source provider. + /// If a source map is given, the content of each source file included in the bundle will + /// be added accordingly. + pub fn new( + fs: &'a P, + source_map: Option<&'s mut SourceMap>, + options: ParserOptions<'o, 'a>, + ) -> Bundler<'a, 'o, 's, P, DefaultAtRuleParser> { + Bundler { + source_map: source_map.map(Mutex::new), + fs, + source_indexes: DashMap::new(), + stylesheets: Mutex::new(Vec::new()), + options, + at_rule_parser: Mutex::new(AtRuleParserValue::Owned(DefaultAtRuleParser)), + } + } +} + impl<'a, 'o, 's, P: SourceProvider, T: AtRuleParser<'a> + Clone + Sync + Send> Bundler<'a, 'o, 's, P, T> where T::AtRule: Sync + Send + ToCss, @@ -196,13 +223,19 @@ where /// Creates a new Bundler using the given source provider. /// If a source map is given, the content of each source file included in the bundle will /// be added accordingly. - pub fn new(fs: &'a P, source_map: Option<&'s mut SourceMap>, options: ParserOptions<'o, 'a, T>) -> Self { + pub fn new_with_at_rule_parser( + fs: &'a P, + source_map: Option<&'s mut SourceMap>, + options: ParserOptions<'o, 'a>, + at_rule_parser: &'s mut T, + ) -> Self { Bundler { source_map: source_map.map(Mutex::new), fs, source_indexes: DashMap::new(), stylesheets: Mutex::new(Vec::new()), options, + at_rule_parser: Mutex::new(AtRuleParserValue::Borrowed(at_rule_parser)), } } @@ -210,7 +243,7 @@ where pub fn bundle<'e>( &mut self, entry: &'e Path, - ) -> Result, Error>> { + ) -> Result, Error>> { // Phase 1: load and parse all files. This is done in parallel. self.load_file( &entry, @@ -344,7 +377,15 @@ where opts.filename = filename.to_owned(); opts.source_index = source_index; - let mut stylesheet = StyleSheet::parse(code, opts)?; + let mut stylesheet = { + let mut at_rule_parser = self.at_rule_parser.lock().unwrap(); + let at_rule_parser = match &mut *at_rule_parser { + AtRuleParserValue::Owned(owned) => owned, + AtRuleParserValue::Borrowed(borrowed) => *borrowed, + }; + + StyleSheet::::parse_with(code, opts, at_rule_parser)? + }; if let Some(source_map) = &self.source_map { // Only add source if we don't have an input source map. @@ -549,7 +590,7 @@ where fn order(&mut self) { process(self.stylesheets.get_mut().unwrap(), 0, &mut HashSet::new()); - fn process<'i, T: AtRuleParser<'i>>( + fn process<'i, T>( stylesheets: &mut Vec>, source_index: u32, visited: &mut HashSet, @@ -592,10 +633,10 @@ where fn inline(&mut self, dest: &mut Vec>) { process(self.stylesheets.get_mut().unwrap(), 0, dest); - fn process<'a, T: AtRuleParser<'a>>( + fn process<'a, T>( stylesheets: &mut Vec>, source_index: u32, - dest: &mut Vec>, + dest: &mut Vec>, ) { let stylesheet = &mut stylesheets[source_index as usize]; let mut rules = std::mem::take(&mut stylesheet.stylesheet.as_mut().unwrap().rules.0); diff --git a/src/declaration.rs b/src/declaration.rs index a9afde50..d5ae9c49 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -63,9 +63,9 @@ pub struct DeclarationBlock<'i> { impl<'i> DeclarationBlock<'i> { /// Parses a declaration block from CSS syntax. - pub fn parse<'a, 'o, 't, T>( + pub fn parse<'a, 'o, 't>( input: &mut Parser<'i, 't>, - options: &'a ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i>, ) -> Result>> { let mut important_declarations = DeclarationList::new(); let mut declarations = DeclarationList::new(); @@ -94,9 +94,9 @@ impl<'i> DeclarationBlock<'i> { } /// Parses a declaration block from a string. - pub fn parse_string<'o, T>( + pub fn parse_string<'o>( input: &'i str, - options: ParserOptions<'o, 'i, T>, + options: ParserOptions<'o, 'i>, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); @@ -403,14 +403,14 @@ impl<'i> DeclarationBlock<'i> { } } -struct PropertyDeclarationParser<'a, 'o, 'i, T> { +struct PropertyDeclarationParser<'a, 'o, 'i> { important_declarations: &'a mut Vec>, declarations: &'a mut Vec>, - options: &'a ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i>, } /// Parse a declaration within {} block: `color: blue` -impl<'a, 'o, 'i, T> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> { type Declaration = (); type Error = ParserError<'i>; @@ -430,18 +430,18 @@ impl<'a, 'o, 'i, T> cssparser::DeclarationParser<'i> for PropertyDeclarationPars } /// Default methods reject all at rules. -impl<'a, 'o, 'i, T> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> { type Prelude = (); type AtRule = (); type Error = ParserError<'i>; } -pub(crate) fn parse_declaration<'i, 't, T>( +pub(crate) fn parse_declaration<'i, 't>( name: CowRcStr<'i>, input: &mut cssparser::Parser<'i, 't>, declarations: &mut DeclarationList<'i>, important_declarations: &mut DeclarationList<'i>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, ) -> Result<(), cssparser::ParseError<'i, ParserError<'i>>> { let property = input.parse_until_before(Delimiter::Bang, |input| { Property::parse(PropertyId::from(CowArcStr::from(name)), input, options) diff --git a/src/parser.rs b/src/parser.rs index 5cbb3ed2..159ad591 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -38,7 +38,7 @@ use std::sync::{Arc, RwLock}; /// CSS parsing options. #[derive(Clone, Debug, Default)] -pub struct ParserOptions<'o, 'i, T = DefaultAtRuleParser> { +pub struct ParserOptions<'o, 'i> { /// Filename to use in error messages. pub filename: String, /// Whether the enable the [CSS nesting](https://www.w3.org/TR/css-nesting-1/) draft syntax. @@ -54,11 +54,9 @@ pub struct ParserOptions<'o, 'i, T = DefaultAtRuleParser> { pub error_recovery: bool, /// A list that will be appended to when a warning occurs. pub warnings: Option>>>>>, - /// A custom at rule parser. - pub at_rule_parser: Option>>, } -impl<'o, 'i, T> ParserOptions<'o, 'i, T> { +impl<'o, 'i> ParserOptions<'o, 'i> { #[inline] pub(crate) fn warn(&self, warning: ParseError<'i, ParserError<'i>>) { if let Some(warnings) = &self.warnings { @@ -69,27 +67,6 @@ impl<'o, 'i, T> ParserOptions<'o, 'i, T> { } } -impl<'o, 'i> ParserOptions<'o, 'i, DefaultAtRuleParser> { - /// Returns the default parser options. - pub fn default() -> Self { - ParserOptions { - filename: String::default(), - nesting: false, - custom_media: false, - css_modules: None, - source_index: 0, - error_recovery: false, - warnings: None, - at_rule_parser: None, - } - } - - /// Returns the default at-rule parser. - pub fn default_at_rule_parser() -> Option { - None - } -} - #[derive(Clone, Default)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub struct DefaultAtRuleParser; @@ -132,20 +109,25 @@ enum State { /// The parser for the top-level rules in a stylesheet. pub struct TopLevelRuleParser<'a, 'o, 'i, T> { - pub options: &'a ParserOptions<'o, 'i, T>, + pub options: &'a ParserOptions<'o, 'i>, state: State, + at_rule_parser: &'a mut T, } impl<'a, 'o, 'b, 'i, T> TopLevelRuleParser<'a, 'o, 'i, T> { - pub fn new(options: &'a ParserOptions<'o, 'i, T>) -> Self { + pub fn new(options: &'a ParserOptions<'o, 'i>, at_rule_parser: &'a mut T) -> Self { TopLevelRuleParser { options, state: State::Start, + at_rule_parser, } } fn nested<'x: 'b>(&'x mut self) -> NestedRuleParser<'_, 'o, 'i, T> { - NestedRuleParser { options: &self.options } + NestedRuleParser { + options: &self.options, + at_rule_parser: self.at_rule_parser, + } } } @@ -376,7 +358,8 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> } pub struct NestedRuleParser<'a, 'o, 'i, T> { - pub options: &'a ParserOptions<'o, 'i, T>, + pub options: &'a ParserOptions<'o, 'i>, + pub at_rule_parser: &'a mut T, } impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> { @@ -384,7 +367,10 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o &mut self, input: &mut Parser<'i, 't>, ) -> Result, ParseError<'i, ParserError<'i>>> { - let nested_parser = NestedRuleParser { options: self.options }; + let nested_parser = NestedRuleParser { + options: self.options, + at_rule_parser: self.at_rule_parser, + }; let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser); let mut rules = Vec::new(); @@ -514,7 +500,7 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne let condition = MediaCondition::parse(input, true)?; Ok(AtRulePrelude::Container(name, condition)) }, - _ => parse_custom_at_rule_prelude(&name, input, self.options) + _ => parse_custom_at_rule_prelude(&name, input, self.options, self.at_rule_parser) } } @@ -634,7 +620,9 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne block: Some(TokenList::parse(input, &self.options, 0)?), loc, })), - AtRulePrelude::Custom(prelude) => parse_custom_at_rule_body(prelude, input, start, self.options), + AtRulePrelude::Custom(prelude) => { + parse_custom_at_rule_body(prelude, input, start, self.options, self.at_rule_parser) + } } } @@ -659,7 +647,9 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne block: None, loc, })), - AtRulePrelude::Custom(prelude) => parse_custom_at_rule_without_block(prelude, start, self.options), + AtRulePrelude::Custom(prelude) => { + parse_custom_at_rule_without_block(prelude, start, self.options, self.at_rule_parser) + } _ => Err(()), } } @@ -691,7 +681,7 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> ) -> Result, ParseError<'i, Self::Error>> { let loc = self.loc(start); let (declarations, rules) = if self.options.nesting { - parse_declarations_and_nested_rules(input, self.options)? + parse_declarations_and_nested_rules(input, self.options, self.at_rule_parser)? } else { (DeclarationBlock::parse(input, self.options)?, CssRuleList(vec![])) }; @@ -708,24 +698,23 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> fn parse_custom_at_rule_prelude<'i, 't, T: crate::traits::AtRuleParser<'i>>( name: &CowRcStr<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut T, ) -> Result, ParseError<'i, ParserError<'i>>> { - if let Some(at_rule_parser) = &options.at_rule_parser { - match at_rule_parser.write().unwrap().parse_prelude(name.clone(), input, options) { - Ok(prelude) => return Ok(AtRulePrelude::Custom(prelude)), - Err(ParseError { - kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(..)), - .. - }) => {} - Err(err) => { - return Err(match &err.kind { - ParseErrorKind::Basic(kind) => ParseError { - kind: ParseErrorKind::Basic(kind.clone()), - location: err.location, - }, - _ => input.new_custom_error(ParserError::AtRulePreludeInvalid), - }) - } + match at_rule_parser.parse_prelude(name.clone(), input, options) { + Ok(prelude) => return Ok(AtRulePrelude::Custom(prelude)), + Err(ParseError { + kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(..)), + .. + }) => {} + Err(err) => { + return Err(match &err.kind { + ParseErrorKind::Basic(kind) => ParseError { + kind: ParseErrorKind::Basic(kind.clone()), + location: err.location, + }, + _ => input.new_custom_error(ParserError::AtRulePreludeInvalid), + }) } } @@ -739,45 +728,36 @@ fn parse_custom_at_rule_body<'i, 't, T: crate::traits::AtRuleParser<'i>>( prelude: T::Prelude, input: &mut Parser<'i, 't>, start: &ParserState, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut T, ) -> Result, ParseError<'i, ParserError<'i>>> { - if let Some(at_rule_parser) = &options.at_rule_parser { - at_rule_parser - .write() - .unwrap() - .parse_block(prelude, start, input, options) - .map(|prelude| CssRule::Custom(prelude)) - .map_err(|err| match &err.kind { - ParseErrorKind::Basic(kind) => ParseError { - kind: ParseErrorKind::Basic(kind.clone()), - location: err.location, - }, - _ => input.new_error(BasicParseErrorKind::AtRuleBodyInvalid), - }) - } else { - Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) - } + at_rule_parser + .parse_block(prelude, start, input, options) + .map(|prelude| CssRule::Custom(prelude)) + .map_err(|err| match &err.kind { + ParseErrorKind::Basic(kind) => ParseError { + kind: ParseErrorKind::Basic(kind.clone()), + location: err.location, + }, + _ => input.new_error(BasicParseErrorKind::AtRuleBodyInvalid), + }) } fn parse_custom_at_rule_without_block<'i, 't, T: crate::traits::AtRuleParser<'i>>( prelude: T::Prelude, start: &ParserState, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut T, ) -> Result, ()> { - if let Some(at_rule_parser) = &options.at_rule_parser { - at_rule_parser - .write() - .unwrap() - .rule_without_block(prelude, start, options) - .map(|prelude| CssRule::Custom(prelude)) - } else { - Err(()) - } + at_rule_parser + .rule_without_block(prelude, start, options) + .map(|prelude| CssRule::Custom(prelude)) } fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>( input: &mut Parser<'i, 't>, - options: &'a ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i>, + at_rule_parser: &mut T, ) -> Result<(DeclarationBlock<'i>, CssRuleList<'i, T::AtRule>), ParseError<'i, ParserError<'i>>> { let mut important_declarations = DeclarationList::new(); let mut declarations = DeclarationList::new(); @@ -787,6 +767,7 @@ fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: crate::traits::AtRuleP declarations: &mut declarations, important_declarations: &mut important_declarations, rules: &mut rules, + at_rule_parser, }; // In the v2 nesting spec, declarations and nested rules may be mixed. @@ -832,10 +813,11 @@ fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: crate::traits::AtRuleP } pub struct StyleRuleParser<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> { - options: &'a ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i>, declarations: &'a mut DeclarationList<'i>, important_declarations: &'a mut DeclarationList<'i>, rules: &'a mut CssRuleList<'i, T::AtRule>, + at_rule_parser: &'a mut T, } /// Parse a declaration within {} block: `color: blue` @@ -898,7 +880,7 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleR let selectors = SelectorList::parse(&selector_parser, input, NestingRequirement::Contained)?; Ok(AtRulePrelude::Nest(selectors)) }, - _ => parse_custom_at_rule_prelude(&name, input, self.options) + _ => parse_custom_at_rule_prelude(&name, input, self.options, self.at_rule_parser) } } @@ -918,7 +900,7 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleR AtRulePrelude::Media(query) => { self.rules.0.push(CssRule::Media(MediaRule { query, - rules: parse_nested_at_rule(input, self.options)?, + rules: parse_nested_at_rule(input, self.options, self.at_rule_parser)?, loc, })); Ok(()) @@ -926,7 +908,7 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleR AtRulePrelude::Supports(condition) => { self.rules.0.push(CssRule::Supports(SupportsRule { condition, - rules: parse_nested_at_rule(input, self.options)?, + rules: parse_nested_at_rule(input, self.options, self.at_rule_parser)?, loc, })); Ok(()) @@ -935,7 +917,7 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleR self.rules.0.push(CssRule::Container(ContainerRule { name, condition, - rules: parse_nested_at_rule(input, self.options)?, + rules: parse_nested_at_rule(input, self.options, self.at_rule_parser)?, loc, })); Ok(()) @@ -943,13 +925,13 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleR AtRulePrelude::LayerBlock(name) => { self.rules.0.push(CssRule::LayerBlock(LayerBlockRule { name, - rules: parse_nested_at_rule(input, self.options)?, + rules: parse_nested_at_rule(input, self.options, self.at_rule_parser)?, loc, })); Ok(()) } AtRulePrelude::Nest(selectors) => { - let (declarations, rules) = parse_declarations_and_nested_rules(input, self.options)?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.options, self.at_rule_parser)?; self.rules.0.push(CssRule::Nesting(NestingRule { style: StyleRule { selectors, @@ -972,10 +954,13 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleR Ok(()) } AtRulePrelude::Custom(prelude) => { - self - .rules - .0 - .push(parse_custom_at_rule_body(prelude, input, start, self.options)?); + self.rules.0.push(parse_custom_at_rule_body( + prelude, + input, + start, + self.options, + self.at_rule_parser, + )?); Ok(()) } _ => Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)), @@ -999,10 +984,12 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleR Ok(()) } AtRulePrelude::Custom(prelude) => { - self - .rules - .0 - .push(parse_custom_at_rule_without_block(prelude, start, self.options)?); + self.rules.0.push(parse_custom_at_rule_without_block( + prelude, + start, + self.options, + self.at_rule_parser, + )?); Ok(()) } _ => Err(()), @@ -1012,7 +999,8 @@ impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleR pub fn parse_nested_at_rule<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>( input: &mut Parser<'i, 't>, - options: &'a ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i>, + at_rule_parser: &mut T, ) -> Result, ParseError<'i, ParserError<'i>>> { let loc = input.current_source_location(); let loc = Location { @@ -1023,7 +1011,7 @@ pub fn parse_nested_at_rule<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>( // 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, options)?; + let (declarations, mut rules) = parse_declarations_and_nested_rules(input, options, at_rule_parser)?; if declarations.len() > 0 { rules.0.insert( @@ -1066,7 +1054,7 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> input: &mut Parser<'i, 't>, ) -> Result<(), ParseError<'i, Self::Error>> { let loc = start.source_location(); - let (declarations, rules) = parse_declarations_and_nested_rules(input, self.options)?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.options, self.at_rule_parser)?; self.rules.0.push(CssRule::Style(StyleRule { selectors, vendor_prefix: VendorPrefix::empty(), diff --git a/src/properties/custom.rs b/src/properties/custom.rs index 84b090f5..e394fa5b 100644 --- a/src/properties/custom.rs +++ b/src/properties/custom.rs @@ -46,10 +46,10 @@ pub struct CustomProperty<'i> { impl<'i> CustomProperty<'i> { /// Parses a custom property with the given name. - pub fn parse<'t, T>( + pub fn parse<'t>( name: CustomPropertyName<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, ) -> Result>> { let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { TokenList::parse(input, options, 0) @@ -146,10 +146,10 @@ pub struct UnparsedProperty<'i> { impl<'i> UnparsedProperty<'i> { /// Parses a property with the given id as a token list. - pub fn parse<'t, T>( + pub fn parse<'t>( property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, ) -> Result>> { let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { TokenList::parse(input, options, 0) @@ -259,19 +259,19 @@ impl<'i> TokenOrValue<'i> { } } -impl<'i, T> ParseWithOptions<'i, T> for TokenList<'i> { +impl<'i> ParseWithOptions<'i> for TokenList<'i> { fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, ) -> Result>> { TokenList::parse(input, options, 0) } } impl<'i> TokenList<'i> { - pub(crate) fn parse<'t, T>( + pub(crate) fn parse<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, depth: usize, ) -> Result>> { let mut tokens = vec![]; @@ -293,10 +293,10 @@ impl<'i> TokenList<'i> { return Ok(TokenList(tokens)); } - fn parse_into<'t, T>( + fn parse_into<'t>( input: &mut Parser<'i, 't>, tokens: &mut Vec>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, depth: usize, ) -> Result<(), ParseError<'i, ParserError<'i>>> { if depth > 500 { @@ -1058,9 +1058,9 @@ pub struct Variable<'i> { } impl<'i> Variable<'i> { - fn parse<'t, T>( + fn parse<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, depth: usize, ) -> Result>> { let name = DashedIdentReference::parse_with_options(input, options)?; @@ -1209,18 +1209,18 @@ impl<'i> ToCss for EnvironmentVariableName<'i> { } impl<'i> EnvironmentVariable<'i> { - pub(crate) fn parse<'t, T>( + pub(crate) fn parse<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, depth: usize, ) -> Result>> { input.expect_function_matching("env")?; input.parse_nested_block(|input| Self::parse_nested(input, options, depth)) } - pub(crate) fn parse_nested<'t, T>( + pub(crate) fn parse_nested<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, depth: usize, ) -> Result>> { let name = EnvironmentVariableName::parse(input)?; @@ -1345,10 +1345,10 @@ pub enum UnresolvedColor<'i> { } impl<'i> UnresolvedColor<'i> { - fn parse<'t, T>( + fn parse<'t>( f: &CowArcStr<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, ) -> Result>> { let parser = ComponentParser::new(false); match_ignore_ascii_case! { &*f, diff --git a/src/properties/mod.rs b/src/properties/mod.rs index e65b80ce..a50949c2 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -676,7 +676,7 @@ macro_rules! define_properties { impl<'i> Property<'i> { /// Parses a CSS property by name. - pub fn parse<'t, T>(property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i, T>) -> Result, ParseError<'i, ParserError<'i>>> { + pub fn parse<'t>(property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>) -> Result, ParseError<'i, ParserError<'i>>> { let state = input.state(); match property_id { @@ -717,7 +717,7 @@ macro_rules! define_properties { } /// Parses a CSS property from a string. - pub fn parse_string(property_id: PropertyId<'i>, input: &'i str, options: ParserOptions<'_, 'i, T>) -> Result>> { + pub fn parse_string(property_id: PropertyId<'i>, input: &'i str, options: ParserOptions<'_, 'i>) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); Self::parse(property_id, &mut parser, &options) diff --git a/src/rules/mod.rs b/src/rules/mod.rs index be240a40..ecea16f0 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -62,7 +62,9 @@ use crate::context::PropertyHandlerContext; use crate::declaration::DeclarationHandler; use crate::dependencies::{Dependency, ImportDependency}; use crate::error::{MinifyError, ParserError, PrinterError, PrinterErrorKind}; -use crate::parser::{parse_nested_at_rule, DefaultAtRule, NestedRuleParser, TopLevelRuleParser}; +use crate::parser::{ + parse_nested_at_rule, DefaultAtRule, DefaultAtRuleParser, NestedRuleParser, TopLevelRuleParser, +}; use crate::prefixes::Feature; use crate::printer::Printer; use crate::rules::keyframes::KeyframesName; @@ -345,24 +347,44 @@ impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> { } } +impl<'i> CssRule<'i, DefaultAtRule> { + /// Parse a single rule. + pub fn parse<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + Self::parse_with(input, options, &mut DefaultAtRuleParser) + } + + /// Parse a single rule from a string. + pub fn parse_string( + input: &'i str, + options: ParserOptions<'_, 'i>, + ) -> Result>> { + Self::parse_string_with(input, options, &mut DefaultAtRuleParser) + } +} + impl<'i, T> CssRule<'i, T> { /// Parse a single rule. - pub fn parse<'t, P: AtRuleParser<'i, AtRule = T>>( + pub fn parse_with<'t, P: AtRuleParser<'i, AtRule = T>>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, P>, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut P, ) -> Result>> { - let (_, rule) = parse_one_rule(input, &mut TopLevelRuleParser::new(options))?; + let (_, rule) = parse_one_rule(input, &mut TopLevelRuleParser::new(options, at_rule_parser))?; Ok(rule) } /// Parse a single rule from a string. - pub fn parse_string>( + pub fn parse_string_with>( input: &'i str, - options: ParserOptions<'_, 'i, P>, + options: ParserOptions<'_, 'i>, + at_rule_parser: &mut P, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); - Self::parse(&mut parser, &options) + Self::parse_with(&mut parser, &options, at_rule_parser) } } @@ -374,23 +396,47 @@ pub struct CssRuleList<'i, R = DefaultAtRule>( #[cfg_attr(feature = "serde", serde(borrow))] pub Vec>, ); -impl<'i, T> CssRuleList<'i, T> { +impl<'i> CssRuleList<'i, DefaultAtRule> { /// Parse a rule list. - pub fn parse<'t, P: AtRuleParser<'i, AtRule = T>>( + pub fn parse<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + Self::parse_with(input, options, &mut DefaultAtRuleParser) + } + + /// Parse a style block, with both declarations and rules. + /// Resulting declarations are returned in a nested style rule. + pub fn parse_style_block<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + Self::parse_style_block_with(input, options, &mut DefaultAtRuleParser) + } +} + +impl<'i, T> CssRuleList<'i, T> { + /// Parse a rule list with a custom at rule parser. + pub fn parse_with<'t, P: AtRuleParser<'i, AtRule = T>>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, P>, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut P, ) -> Result>> { - let mut nested_parser = NestedRuleParser { options }; + let mut nested_parser = NestedRuleParser { + options, + at_rule_parser, + }; nested_parser.parse_nested_rules(input) } /// Parse a style block, with both declarations and rules. /// Resulting declarations are returned in a nested style rule. - pub fn parse_style_block<'t, P: AtRuleParser<'i, AtRule = T>>( + pub fn parse_style_block_with<'t, P: AtRuleParser<'i, AtRule = T>>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, P>, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut P, ) -> Result>> { - parse_nested_at_rule(input, options) + parse_nested_at_rule(input, options, at_rule_parser) } } diff --git a/src/rules/page.rs b/src/rules/page.rs index b418cf19..5f808649 100644 --- a/src/rules/page.rs +++ b/src/rules/page.rs @@ -174,11 +174,11 @@ pub struct PageRule<'i> { } impl<'i> PageRule<'i> { - pub(crate) fn parse<'t, 'o, T>( + pub(crate) fn parse<'t, 'o>( selectors: Vec>, input: &mut Parser<'i, 't>, loc: Location, - options: &ParserOptions<'o, 'i, T>, + options: &ParserOptions<'o, 'i>, ) -> Result>> { let mut declarations = DeclarationBlock::new(); let mut rules = Vec::new(); @@ -301,13 +301,13 @@ impl<'i> ToCss for PageSelector<'i> { } } -struct PageRuleParser<'a, 'o, 'i, T> { +struct PageRuleParser<'a, 'o, 'i> { declarations: &'a mut DeclarationBlock<'i>, rules: &'a mut Vec>, - options: &'a ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i>, } -impl<'a, 'o, 'i, T> cssparser::DeclarationParser<'i> for PageRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for PageRuleParser<'a, 'o, 'i> { type Declaration = (); type Error = ParserError<'i>; @@ -326,7 +326,7 @@ impl<'a, 'o, 'i, T> cssparser::DeclarationParser<'i> for PageRuleParser<'a, 'o, } } -impl<'a, 'o, 'i, T> AtRuleParser<'i> for PageRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i> AtRuleParser<'i> for PageRuleParser<'a, 'o, 'i> { type Prelude = PageMarginBox; type AtRule = (); type Error = ParserError<'i>; diff --git a/src/selector.rs b/src/selector.rs index 798c2f66..efe6cf12 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -64,12 +64,12 @@ impl<'i> SelectorImpl<'i> for Selectors { } } -pub(crate) struct SelectorParser<'a, 'o, 'i, T> { +pub(crate) struct SelectorParser<'a, 'o, 'i> { pub is_nesting_allowed: bool, - pub options: &'a ParserOptions<'o, 'i, T>, + pub options: &'a ParserOptions<'o, 'i>, } -impl<'a, 'o, 'i, T> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, 'i> { type Impl = Selectors; type Error = ParserError<'i>; @@ -1831,10 +1831,10 @@ impl<'i, T: Visit<'i, T, V>, V: Visitor<'i, T>> Visit<'i, T, V> for Selector<'i> } } -impl<'i, T> ParseWithOptions<'i, T> for Selector<'i> { +impl<'i> ParseWithOptions<'i> for Selector<'i> { fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, ) -> Result>> { Selector::parse( &SelectorParser { @@ -1846,10 +1846,10 @@ impl<'i, T> ParseWithOptions<'i, T> for Selector<'i> { } } -impl<'i, T> ParseWithOptions<'i, T> for SelectorList<'i> { +impl<'i> ParseWithOptions<'i> for SelectorList<'i> { fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, ) -> Result>> { SelectorList::parse( &SelectorParser { diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 0b8f41b6..384e6268 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -9,7 +9,7 @@ use crate::css_modules::{CssModule, CssModuleExports, CssModuleReferences}; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::dependencies::Dependency; use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterError, PrinterErrorKind}; -use crate::parser::{DefaultAtRuleParser, TopLevelRuleParser}; +use crate::parser::{DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser}; use crate::printer::Printer; use crate::rules::{CssRule, CssRuleList, MinifyContext}; use crate::targets::Browsers; @@ -68,24 +68,12 @@ pub use crate::printer::PseudoClasses; #[cfg_attr( feature = "jsonschema", derive(schemars::JsonSchema), - schemars( - rename = "StyleSheet", - bound = "T: schemars::JsonSchema, T::AtRule: schemars::JsonSchema" - ) + schemars(rename = "StyleSheet", bound = "T: schemars::JsonSchema") )] -pub struct StyleSheet<'i, 'o, T: AtRuleParser<'i> = DefaultAtRuleParser> { +pub struct StyleSheet<'i, 'o, T = DefaultAtRule> { /// A list of top-level rules within the style sheet. - #[cfg_attr( - feature = "serde", - serde( - borrow, - bound( - serialize = "T::AtRule: serde::Serialize", - deserialize = "T::AtRule: serde::Deserialize<'de>" - ) - ) - )] - pub rules: CssRuleList<'i, T::AtRule>, + #[cfg_attr(feature = "serde", serde(borrow))] + pub rules: CssRuleList<'i, T>, /// A list of file names for all source files included within the style sheet. /// Sources are referenced by index in the `loc` property of each rule. pub sources: Vec, @@ -93,7 +81,7 @@ pub struct StyleSheet<'i, 'o, T: AtRuleParser<'i> = DefaultAtRuleParser> { pub(crate) source_map_urls: Vec>, #[cfg_attr(feature = "serde", serde(skip))] /// The options the style sheet was originally parsed with. - options: ParserOptions<'o, 'i, T>, + options: ParserOptions<'o, 'i>, } /// Options for the `minify` function of a [StyleSheet](StyleSheet) @@ -124,15 +112,22 @@ pub struct ToCssResult { pub dependencies: Option>, } -impl<'i, 'o, T: AtRuleParser<'i>> StyleSheet<'i, 'o, T> +impl<'i, 'o> StyleSheet<'i, 'o, DefaultAtRule> { + /// Parse a style sheet from a string. + pub fn parse(code: &'i str, options: ParserOptions<'o, 'i>) -> Result>> { + Self::parse_with(code, options, &mut DefaultAtRuleParser) + } +} + +impl<'i, 'o, T> StyleSheet<'i, 'o, T> where - T::AtRule: ToCss, + T: ToCss, { /// Creates a new style sheet with the given source filenames and rules. pub fn new( sources: Vec, - rules: CssRuleList<'i, T::AtRule>, - options: ParserOptions<'o, 'i, T>, + rules: CssRuleList<'i, T>, + options: ParserOptions<'o, 'i>, ) -> StyleSheet<'i, 'o, T> { StyleSheet { sources, @@ -143,11 +138,15 @@ where } /// Parse a style sheet from a string. - pub fn parse(code: &'i str, mut options: ParserOptions<'o, 'i, T>) -> Result>> { + pub fn parse_with>( + code: &'i str, + mut options: ParserOptions<'o, 'i>, + at_rule_parser: &mut P, + ) -> Result>> { let mut input = ParserInput::new(&code); let mut parser = Parser::new(&mut input); let mut rule_list_parser = - RuleListParser::new_for_stylesheet(&mut parser, TopLevelRuleParser::new(&mut options)); + RuleListParser::new_for_stylesheet(&mut parser, TopLevelRuleParser::new(&mut options, at_rule_parser)); let mut rules = vec![]; while let Some(rule) = rule_list_parser.next() { @@ -280,11 +279,10 @@ where #[cfg(feature = "visitor")] #[cfg_attr(docsrs, doc(cfg(feature = "visitor")))] -impl<'i, 'o, T, V> Visit<'i, T::AtRule, V> for StyleSheet<'i, 'o, T> +impl<'i, 'o, T, V> Visit<'i, T, V> for StyleSheet<'i, 'o, T> where - T: AtRuleParser<'i>, - T::AtRule: Visit<'i, T::AtRule, V>, - V: Visitor<'i, T::AtRule>, + T: Visit<'i, T, V>, + V: Visitor<'i, T>, { const CHILD_TYPES: VisitTypes = VisitTypes::all(); @@ -329,9 +327,9 @@ pub struct StyleAttribute<'i> { impl<'i> StyleAttribute<'i> { /// Parses a style attribute from a string. - pub fn parse( + pub fn parse( code: &'i str, - options: ParserOptions<'_, 'i, T>, + options: ParserOptions<'_, 'i>, ) -> Result, Error>> { let mut input = ParserInput::new(&code); let mut parser = Parser::new(&mut input); diff --git a/src/traits.rs b/src/traits.rs index 8ac879d7..dad8397e 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -28,17 +28,17 @@ pub trait Parse<'i>: Sized { } /// Trait for things that can be parsed from CSS syntax and require ParserOptions. -pub trait ParseWithOptions<'i, T>: Sized { +pub trait ParseWithOptions<'i>: Sized { /// Parse a value of this type with the given options. fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, T>, + options: &ParserOptions<'_, 'i>, ) -> Result>>; /// Parse a value from a string with the given options. fn parse_string_with_options( input: &'i str, - options: ParserOptions<'_, 'i, T>, + options: ParserOptions<'_, 'i>, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); @@ -46,11 +46,11 @@ pub trait ParseWithOptions<'i, T>: Sized { } } -impl<'i, T: Parse<'i>, U> ParseWithOptions<'i, U> for T { +impl<'i, T: Parse<'i>> ParseWithOptions<'i> for T { #[inline] fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - _options: &ParserOptions, + _options: &ParserOptions, ) -> Result>> { T::parse(input) } @@ -275,7 +275,7 @@ pub trait AtRuleParser<'i>: Sized { &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, Self>, + options: &ParserOptions<'_, 'i>, ) -> Result> { let _ = name; let _ = input; @@ -294,7 +294,7 @@ pub trait AtRuleParser<'i>: Sized { &mut self, prelude: Self::Prelude, start: &ParserState, - options: &ParserOptions<'_, 'i, Self>, + options: &ParserOptions<'_, 'i>, ) -> Result { let _ = prelude; let _ = start; @@ -316,7 +316,7 @@ pub trait AtRuleParser<'i>: Sized { prelude: Self::Prelude, start: &ParserState, input: &mut Parser<'i, 't>, - options: &ParserOptions<'_, 'i, Self>, + options: &ParserOptions<'_, 'i>, ) -> Result> { let _ = prelude; let _ = start; diff --git a/src/values/ident.rs b/src/values/ident.rs index f7c93419..e8831722 100644 --- a/src/values/ident.rs +++ b/src/values/ident.rs @@ -110,10 +110,10 @@ pub struct DashedIdentReference<'i> { pub from: Option>, } -impl<'i, T> ParseWithOptions<'i, T> for DashedIdentReference<'i> { +impl<'i> ParseWithOptions<'i> for DashedIdentReference<'i> { fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &crate::stylesheet::ParserOptions, + options: &crate::stylesheet::ParserOptions, ) -> Result>> { let ident = DashedIdent::parse(input)?; diff --git a/tests/test_custom_parser.rs b/tests/test_custom_parser.rs index 644c4150..37e27107 100644 --- a/tests/test_custom_parser.rs +++ b/tests/test_custom_parser.rs @@ -11,14 +11,7 @@ use lightningcss::{ }; fn minify_test(source: &str, expected: &str) { - let mut stylesheet = StyleSheet::parse( - &source, - ParserOptions { - at_rule_parser: Some(Arc::new(RwLock::new(TestAtRuleParser))), - ..Default::default() - }, - ) - .unwrap(); + let mut stylesheet = StyleSheet::parse_with(&source, ParserOptions::default(), &mut TestAtRuleParser).unwrap(); stylesheet.minify(Default::default()).unwrap(); let res = stylesheet .to_css(PrinterOptions { @@ -87,7 +80,7 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, - _options: &ParserOptions<'_, 'i, Self>, + _options: &ParserOptions<'_, 'i>, ) -> Result> { let location = input.current_source_location(); match_ignore_ascii_case! {&*name, @@ -109,7 +102,7 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { &mut self, prelude: Self::Prelude, _start: &ParserState, - _options: &ParserOptions<'_, 'i, Self>, + _options: &ParserOptions<'_, 'i>, ) -> Result { match prelude { Prelude::Inline(name) => Ok(AtRule::Inline(InlineRule { name })), @@ -122,7 +115,7 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { prelude: Self::Prelude, _start: &ParserState, input: &mut Parser<'i, 't>, - _options: &ParserOptions<'_, 'i, Self>, + _options: &ParserOptions<'_, 'i>, ) -> Result> { match prelude { Prelude::Block(name) => Ok(AtRule::Block(BlockRule { From f5dac33d0db09a9ee28d9bc14b5305dbd5f05d7a Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 11 Feb 2023 12:28:24 -0500 Subject: [PATCH 10/12] "fix" flow build --- scripts/build-flow.js | 2 ++ tests/test_custom_parser.rs | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-flow.js b/scripts/build-flow.js index 46ee8da0..6e1a1ed9 100644 --- a/scripts/build-flow.js +++ b/scripts/build-flow.js @@ -3,6 +3,8 @@ const { compiler, beautify } = require('flowgen'); let dir = `${__dirname}/../`; let contents = fs.readFileSync(dir + '/node/index.d.ts', 'utf8').replace('`${PropertyStart}${string}`', 'string'); +contents = contents.replace(/`.*`/g, 'string'); +contents = contents.replaceAll('(string & {})', 'string'); let index = beautify(compiler.compileDefinitionString(contents, { inexact: false, interfaceRecords: true })); index = index.replace('{ code: any }', '{| code: any |}'); index = index.replace(/from "(.*?)";/g, 'from "$1.js.flow";'); diff --git a/tests/test_custom_parser.rs b/tests/test_custom_parser.rs index 37e27107..b75fa9be 100644 --- a/tests/test_custom_parser.rs +++ b/tests/test_custom_parser.rs @@ -1,5 +1,3 @@ -use std::sync::{Arc, RwLock}; - use cssparser::*; use lightningcss::{ declaration::DeclarationBlock, From ef64dae527f2e08b53d0b57dda84e6cd165f5e95 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 11 Feb 2023 14:01:37 -0500 Subject: [PATCH 11/12] More tests and docs --- examples/custom_at_rule.rs | 6 +- node/index.d.ts | 8 +-- node/src/at_rule_parser.rs | 2 +- node/test/customAtRules.mjs | 107 +++++++++++++++++++++++++++++++++--- src/declaration.rs | 2 +- src/rules/style.rs | 1 + tests/testdata/apply.css | 5 ++ tests/testdata/mixin.css | 7 +++ website/pages/transforms.md | 93 +++++++++++++++++++++++++++++++ 9 files changed, 212 insertions(+), 19 deletions(-) create mode 100644 tests/testdata/apply.css create mode 100644 tests/testdata/mixin.css diff --git a/examples/custom_at_rule.rs b/examples/custom_at_rule.rs index a5f73806..ada19de6 100644 --- a/examples/custom_at_rule.rs +++ b/examples/custom_at_rule.rs @@ -1,8 +1,4 @@ -use std::{ - collections::HashMap, - convert::Infallible, - sync::{Arc, RwLock}, -}; +use std::{collections::HashMap, convert::Infallible}; use cssparser::*; use lightningcss::{ diff --git a/node/index.d.ts b/node/index.d.ts index 3407a207..02319582 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -99,8 +99,8 @@ type AnyCustomAtRule = { }[keyof C]; type RuleVisitors = MappedRuleVisitors & { - unknown?: UnknownVisitors | RuleVisitor, - custom?: CustomVisitors | RuleVisitor> + unknown?: UnknownVisitors | Omit, keyof CallableFunction>, + custom?: CustomVisitors | Omit>, keyof CallableFunction> }; type PreludeTypes = Exclude; @@ -130,10 +130,10 @@ interface CustomAtRule { type CustomAtRuleBody = { type: 'declaration-list', - value: DeclarationBlock + value: Required } | { type: 'rule-list', - value: Rule[] + value: RequiredValue[] }; type FindProperty = Union extends { property: Name } ? Union : never; diff --git a/node/src/at_rule_parser.rs b/node/src/at_rule_parser.rs index 62b053b4..c147ee2d 100644 --- a/node/src/at_rule_parser.rs +++ b/node/src/at_rule_parser.rs @@ -118,7 +118,7 @@ impl<'i> AtRuleParser<'i> for CustomAtRuleParser { )?)), } } else { - None + return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)); }; let loc = start.source_location(); diff --git a/node/test/customAtRules.mjs b/node/test/customAtRules.mjs index 0f8855a6..0c0585e7 100644 --- a/node/test/customAtRules.mjs +++ b/node/test/customAtRules.mjs @@ -2,9 +2,10 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import { transform } from '../index.mjs'; +import { bundle, transform } from '../index.mjs'; test('declaration list', () => { + let definitions = new Map(); let res = transform({ filename: 'test.css', minify: true, @@ -13,6 +14,10 @@ test('declaration list', () => { foo: 16px; bar: 32px; } + + .foo { + width: theme('spacing.foo'); + } `), customAtRules: { theme: { @@ -24,21 +29,73 @@ test('declaration list', () => { Rule: { custom: { theme(rule) { - return { - type: 'style', - value: { - selectors: [[{ type: 'pseudo-class', kind: 'root' }]], - declarations: rule.body.value, - loc: rule.loc + for (let decl of rule.body.value.declarations) { + if (decl.property === 'custom') { + definitions.set(rule.prelude.value + '.' + decl.value.name, decl.value.value); } } + return []; + } + } + }, + Function: { + theme(f) { + if (f.arguments[0].type === 'token' && f.arguments[0].value.type === 'string') { + return definitions.get(f.arguments[0].value.value); } } } } }); - assert.equal(res.code.toString(), ':root{foo:16px;bar:32px}'); + assert.equal(res.code.toString(), '.foo{width:16px}'); +}); + +test('mixin', () => { + let mixins = new Map(); + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @mixin color { + color: red; + + &.bar { + color: yellow; + } + } + + .foo { + @apply color; + } + `), + drafts: { nesting: true }, + targets: { chrome: 100 << 16 }, + customAtRules: { + mixin: { + prelude: '', + body: 'style-block' + }, + apply: { + prelude: '' + } + }, + visitor: { + Rule: { + custom: { + mixin(rule) { + mixins.set(rule.prelude.value, rule.body.value); + return []; + }, + apply(rule) { + return mixins.get(rule.prelude.value); + } + } + } + } + }); + + assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}'); }); test('rule list', () => { @@ -183,3 +240,37 @@ test('multiple', () => { assert.equal(res.code.toString(), '@media (width<=1024px){:root{foo:16px;bar:32px}}'); }); + +test('bundler', () => { + let mixins = new Map(); + let res = bundle({ + filename: 'tests/testdata/apply.css', + minify: true, + drafts: { nesting: true }, + targets: { chrome: 100 << 16 }, + customAtRules: { + mixin: { + prelude: '', + body: 'style-block' + }, + apply: { + prelude: '' + } + }, + visitor: { + Rule: { + custom: { + mixin(rule) { + mixins.set(rule.prelude.value, rule.body.value); + return []; + }, + apply(rule) { + return mixins.get(rule.prelude.value); + } + } + } + } + }); + + assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}'); +}); diff --git a/src/declaration.rs b/src/declaration.rs index d5ae9c49..b6a3afd1 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -43,7 +43,7 @@ use cssparser::*; /// Properties are separated into a list of `!important` declararations, /// and a list of normal declarations. This reduces memory usage compared /// with storing a boolean along with each property. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Default)] #[cfg_attr(feature = "visitor", derive(Visit), visit(visit_declaration_block, PROPERTIES))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr( diff --git a/src/rules/style.rs b/src/rules/style.rs index f68d9891..1a75f0ec 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -34,6 +34,7 @@ pub struct StyleRule<'i, R = DefaultAtRule> { #[cfg_attr(feature = "visitor", skip_visit)] pub vendor_prefix: VendorPrefix, /// The declarations within the style rule. + #[cfg_attr(feature = "serde", serde(default))] pub declarations: DeclarationBlock<'i>, /// Nested rules within the style rule. #[cfg_attr(feature = "serde", serde(default = "default_rule_list::"))] diff --git a/tests/testdata/apply.css b/tests/testdata/apply.css new file mode 100644 index 00000000..e2508b31 --- /dev/null +++ b/tests/testdata/apply.css @@ -0,0 +1,5 @@ +@import "./mixin.css"; + +.foo { + @apply color; +} diff --git a/tests/testdata/mixin.css b/tests/testdata/mixin.css new file mode 100644 index 00000000..a836131c --- /dev/null +++ b/tests/testdata/mixin.css @@ -0,0 +1,7 @@ +@mixin color { + color: red; + + &.bar { + color: yellow; + } +} diff --git a/website/pages/transforms.md b/website/pages/transforms.md index eb155554..6e4e93d2 100644 --- a/website/pages/transforms.md +++ b/website/pages/transforms.md @@ -235,6 +235,99 @@ assert.equal(res.code.toString(), '.foo{padding:40px}'); Each visitor object has the opportunity to visit every value once. If a visitor returns a new value, that value is visited by the other visitor objects but not again by the original visitor that created it. If other visitors subsequently modify the value, the previous visitors will not revisit the value. This is to avoid infinite loops. +## Unknown at-rules + +By default, unknown at-rules are stored in the AST as raw tokens. This allows you to interpret them however you like by writing a custom visitor. The following example allows declaring static variables using named at-rules, and inlines them when an `at-keyword` token is seen: + +```js +let declared = new Map(); +let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @blue #056ef0; + + .menu_link { + background: @blue; + } + `), + visitor: { + Rule: { + unknown(rule) { + declared.set(rule.name, rule.prelude); + return []; + } + }, + Token: { + 'at-keyword'(token) { + return declared.get(token.value); + } + } + } +}); + +assert.equal(res.code.toString(), '.menu_link{background:#056ef0}'); +``` + +## Custom at-rules + +Raw tokens as stored in unknown at-rules are fine for simple cases, but in more complex cases, you may wish to interpret a custom at-rule body as a standard CSS declaration list or rule list. However, by default, Lightning CSS does not know how unknown rules should be parsed. You can define their syntax using the `customAtRules` option. + +The syntax of the at-rule prelude can be defined with a [CSS syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), which Lightning CSS will interpret and use to validate the input CSS. This uses the same syntax as the [@property](https://developer.mozilla.org/en-US/docs/Web/CSS/@property) rule. The body syntax is defined using one of the following options: + +* `"declaration-list"` – A list of CSS declarations (property value pairs), as in a style rule or other at-rules like `@font-face`. +* `"rule-list"` – A list of nested CSS rules, including style rules and at rules. Directly nested declarations with CSS nesting are not allowed. This matches how rules like `@keyframes` are parsed. +* `"style-block"` – A list of CSS declarations and/or nested rules. This matches the behavior of rules like `@media` and `@supports` which support directly nested declarations when inside a style rule. Note that the [nesting](transpilation.html#nesting) and [targets](transpilation.html#browser-targets) options must be defined for nesting to be compiled. + +This example defines two custom at-rules. `@mixin` defines a reusable style block, supporting both directly nested declarations and nested rules. A visitor function registers the mixin in a map and removes the custom rule. `@apply` looks up the requested mixin in the map and returns the nested rules, which are inlined into the parent. + +```js +let mixins = new Map(); +let res = transform({ + filename: 'test.css', + minify: true, + drafts: { nesting: true }, + targets: { chrome: 100 << 16 }, + code: Buffer.from(` + @mixin color { + color: red; + + &.bar { + color: yellow; + } + } + + .foo { + @apply color; + } + `), + customAtRules: { + mixin: { + prelude: '', + body: 'style-block' + }, + apply: { + prelude: '' + } + }, + visitor: { + Rule: { + custom: { + mixin(rule) { + mixins.set(rule.prelude.value, rule.body.value); + return []; + }, + apply(rule) { + return mixins.get(rule.prelude.value); + } + } + } + } +}); + +assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}'); +``` + ## Examples For examples of visitors that perform a variety of real world tasks, see the Lightning CSS [visitor tests](https://github.com/parcel-bundler/lightningcss/blob/master/node/test/visitor.test.mjs). From a866b26c6f40b9591b24471c8aea612573b97a0e Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 11 Feb 2023 14:17:33 -0500 Subject: [PATCH 12/12] Update ast types --- node/ast.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/ast.d.ts b/node/ast.d.ts index 871addbd..cab655e6 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -7036,7 +7036,7 @@ export interface StyleRule { /** * The declarations within the style rule. */ - declarations: DeclarationBlock; + declarations?: DeclarationBlock; /** * The location of the rule in the source file. */