From de3457d6ce02e25b41bb9ffd704089141512e8b3 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sun, 20 Nov 2022 22:18:47 -0600 Subject: [PATCH 1/3] Implement new nesting spec --- node/index.d.ts | 9 +- node/src/lib.rs | 50 +- selectors/builder.rs | 5 + selectors/parser.rs | 79 ++- src/lib.rs | 1101 +++++++++++++++++++++++++++++------------- src/main.rs | 8 +- src/parser.rs | 82 +++- src/stylesheet.rs | 2 +- 8 files changed, 963 insertions(+), 373 deletions(-) diff --git a/node/index.d.ts b/node/index.d.ts index 36a62fda..79b766ba 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -67,8 +67,13 @@ export interface Resolver { } export interface Drafts { - /** Whether to enable CSS nesting. */ - nesting?: boolean, + /** + * Whether to enable CSS nesting. + * If a number is provided, that version of the spec is used. + * v1 required an & in every selector, and supported the @nest rule. + * v2 has implicit nesting for descendant combinators. + */ + nesting?: boolean | 1 | 2, /** Whether to enable @custom-media rules. */ customMedia?: boolean } diff --git a/node/src/lib.rs b/node/src/lib.rs index 4b1072ab..a6370d30 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -7,7 +7,7 @@ use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternPa use lightningcss::dependencies::{Dependency, DependencyOptions}; use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind}; use lightningcss::stylesheet::{ - MinifyOptions, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet, + MinifyOptions, NestingSpec, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet, }; use lightningcss::targets::Browsers; use parcel_sourcemap::SourceMap; @@ -540,11 +540,43 @@ impl<'a> Into> for &'a OwnedPseudoClasses { #[serde(rename_all = "camelCase")] struct Drafts { #[serde(default)] - nesting: bool, + nesting: NestingOption, #[serde(default)] custom_media: bool, } +#[derive(Serialize, Debug, Deserialize)] +#[serde(untagged)] +enum NestingOption { + Bool(bool), + Version(u8), +} + +impl Default for NestingOption { + fn default() -> Self { + NestingOption::Bool(false) + } +} + +impl NestingOption { + fn into_nesting_spec<'i, E: std::error::Error>(&self) -> Result> { + Ok(match self { + NestingOption::Bool(n) => { + if *n { + NestingSpec::V1 + } else { + NestingSpec::None + } + } + NestingOption::Version(v) => match *v { + 1 => NestingSpec::V1, + 2 => NestingSpec::V2, + v => return Err(CompileError::InvalidNestingSpec(v)), + }, + }) + } +} + fn compile<'i>(code: &'i str, config: &Config) -> Result, CompileError<'i, std::io::Error>> { let drafts = config.drafts.as_ref(); let warnings = Some(Arc::new(RwLock::new(Vec::new()))); @@ -564,7 +596,11 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result, Co &code, ParserOptions { filename: filename.clone(), - nesting: matches!(drafts, Some(d) if d.nesting), + nesting: if let Some(drafts) = &config.drafts { + drafts.nesting.into_nesting_spec()? + } else { + NestingSpec::None + }, custom_media: matches!(drafts, Some(d) if d.custom_media), css_modules: if let Some(css_modules) = &config.css_modules { match css_modules { @@ -657,7 +693,11 @@ fn compile_bundle<'i, P: SourceProvider>( let res = { let drafts = config.drafts.as_ref(); let parser_options = ParserOptions { - nesting: matches!(drafts, Some(d) if d.nesting), + nesting: if let Some(drafts) = &config.drafts { + drafts.nesting.into_nesting_spec()? + } else { + NestingSpec::None + }, custom_media: matches!(drafts, Some(d) if d.custom_media), css_modules: if let Some(css_modules) = &config.css_modules { match css_modules { @@ -830,6 +870,7 @@ enum CompileError<'i, E: std::error::Error> { SourceMapError(parcel_sourcemap::SourceMapError), BundleError(Error>), PatternError(PatternParseError), + InvalidNestingSpec(u8), } impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> { @@ -841,6 +882,7 @@ impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> { CompileError::BundleError(err) => err.kind.fmt(f), CompileError::PatternError(err) => err.fmt(f), CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this + CompileError::InvalidNestingSpec(v) => write!(f, "Invalid nesting version {}", v), } } } diff --git a/selectors/builder.rs b/selectors/builder.rs index 6bcad0d9..7e2ace25 100644 --- a/selectors/builder.rs +++ b/selectors/builder.rs @@ -90,6 +90,11 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorBuilder<'i, Impl> { !self.combinators.is_empty() } + pub fn add_nesting_prefix(&mut self) { + self.combinators.insert(0, (Combinator::Descendant, 1)); + self.simple_selectors.insert(0, Component::Nesting); + } + /// Consumes the builder, producing a Selector. #[inline(always)] pub fn build( diff --git a/selectors/parser.rs b/selectors/parser.rs index 6f64a31f..b07e9698 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -354,6 +354,7 @@ pub enum NestingRequirement { None, Prefixed, Contained, + Implicit, } impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { @@ -422,12 +423,30 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { } } + pub fn parse_relative<'t, P>( + parser: &P, + input: &mut CssParser<'i, 't>, + nesting_requirement: NestingRequirement, + ) -> Result> + where + P: Parser<'i, Impl = Impl>, + { + Self::parse_relative_with_state( + parser, + input, + &mut SelectorParsingState::empty(), + ParseErrorRecovery::DiscardList, + nesting_requirement, + ) + } + #[inline] - fn parse_relative<'t, P>( + fn parse_relative_with_state<'t, P>( parser: &P, input: &mut CssParser<'i, 't>, state: &mut SelectorParsingState, recovery: ParseErrorRecovery, + nesting_requirement: NestingRequirement, ) -> Result> where P: Parser<'i, Impl = Impl>, @@ -437,7 +456,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { loop { let selector = input.parse_until_before(Delimiter::Comma, |input| { let mut selector_state = original_state; - let result = parse_relative_selector(parser, input, &mut selector_state); + let result = parse_relative_selector(parser, input, &mut selector_state, nesting_requirement); if selector_state.contains(SelectorParsingState::AFTER_NESTING) { state.insert(SelectorParsingState::AFTER_NESTING) } @@ -1107,7 +1126,7 @@ pub enum Component<'i, Impl: SelectorImpl<'i>> { Scope, NthChild(i32, i32), NthLastChild(i32, i32), - NthCol(i32, i32), // https://www.w3.org/TR/selectors-4/#the-nth-col-pseudo + NthCol(i32, i32), // https://www.w3.org/TR/selectors-4/#the-nth-col-pseudo NthLastCol(i32, i32), // https://www.w3.org/TR/selectors-4/#the-nth-last-col-pseudo NthOfType(i32, i32), NthLastOfType(i32, i32), @@ -1606,7 +1625,12 @@ impl<'i, Impl: SelectorImpl<'i>> ToCss for Component<'i, Impl> { FirstOfType => dest.write_str(":first-of-type"), LastOfType => dest.write_str(":last-of-type"), OnlyOfType => dest.write_str(":only-of-type"), - NthChild(a, b) | NthLastChild(a, b) | NthOfType(a, b) | NthLastOfType(a, b) | NthCol(a, b) | NthLastCol(a, b) => { + NthChild(a, b) + | NthLastChild(a, b) + | NthOfType(a, b) + | NthLastOfType(a, b) + | NthCol(a, b) + | NthLastCol(a, b) => { match *self { NthChild(_, _) => dest.write_str(":nth-child(")?, NthLastChild(_, _) => dest.write_str(":nth-last-child(")?, @@ -1710,6 +1734,18 @@ where input.reset(&state); } + // In the implicit nesting mode, selectors may not start with an ident or function token. + if nesting_requirement == NestingRequirement::Implicit { + let state = input.state(); + match input.next()? { + Token::Ident(..) | Token::Function(..) => { + return Err(input.new_custom_error(SelectorParseErrorKind::MissingNestingPrefix)); + } + _ => {} + } + input.reset(&state); + } + let mut builder = SelectorBuilder::default(); let mut has_pseudo_element = false; @@ -1773,8 +1809,16 @@ where builder.push_combinator(combinator); } - if nesting_requirement == NestingRequirement::Contained && !state.contains(SelectorParsingState::AFTER_NESTING) { - return Err(input.new_custom_error(SelectorParseErrorKind::MissingNestingSelector)); + if !state.contains(SelectorParsingState::AFTER_NESTING) { + match nesting_requirement { + NestingRequirement::Implicit => { + builder.add_nesting_prefix(); + } + NestingRequirement::Contained | NestingRequirement::Prefixed => { + return Err(input.new_custom_error(SelectorParseErrorKind::MissingNestingSelector)); + } + _ => {} + } } let (spec, components) = builder.build(has_pseudo_element, slotted, part); @@ -1801,6 +1845,7 @@ fn parse_relative_selector<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, state: &mut SelectorParsingState, + mut nesting_requirement: NestingRequirement, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, @@ -1818,11 +1863,21 @@ where } }; - let mut selector = parse_selector(parser, input, state, NestingRequirement::None)?; + let scope = if nesting_requirement == NestingRequirement::Implicit { + Component::Nesting + } else { + Component::Scope + }; + + if combinator.is_some() { + nesting_requirement = NestingRequirement::None; + } + + let mut selector = parse_selector(parser, input, state, nesting_requirement)?; if let Some(combinator) = combinator { // https://www.w3.org/TR/selectors/#absolutizing selector.1.push(Component::Combinator(combinator)); - selector.1.push(Component::Scope); + selector.1.push(scope); } Ok(selector) @@ -2366,7 +2421,13 @@ where Impl: SelectorImpl<'i>, { let mut child_state = *state; - let inner = SelectorList::parse_relative(parser, input, &mut child_state, parser.is_and_where_error_recovery())?; + let inner = SelectorList::parse_relative_with_state( + parser, + input, + &mut child_state, + parser.is_and_where_error_recovery(), + NestingRequirement::None, + )?; if child_state.contains(SelectorParsingState::AFTER_NESTING) { state.insert(SelectorParsingState::AFTER_NESTING) } diff --git a/src/lib.rs b/src/lib.rs index 7750d530..de75adb7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ mod tests { use crate::css_modules::{CssModuleExport, CssModuleExports, CssModuleReference, CssModuleReferences}; use crate::dependencies::Dependency; use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind, SelectorError}; + use crate::parser::NestingSpec; use crate::properties::custom::Token; use crate::properties::Property; use crate::rules::CssRule; @@ -111,7 +112,7 @@ mod tests { assert_eq!(res.code, expected); } - fn nesting_test(source: &str, expected: &str) { + fn nesting_test(source: &str, expected: &str, spec: NestingSpec) { let targets = Some(Browsers { chrome: Some(95 << 16), ..Browsers::default() @@ -119,7 +120,7 @@ mod tests { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - nesting: true, + nesting: spec, ..ParserOptions::default() }, ) @@ -143,7 +144,7 @@ mod tests { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - nesting: true, + nesting: NestingSpec::V1, ..ParserOptions::default() }, ) @@ -206,6 +207,20 @@ mod tests { } } + fn nesting_error_test(source: &str, spec: NestingSpec, error: ParserError) { + let res = StyleSheet::parse( + &source, + ParserOptions { + nesting: spec, + ..Default::default() + }, + ); + match res { + Ok(_) => unreachable!(), + Err(e) => assert_eq!(e.kind, error), + } + } + macro_rules! map( { $($key:expr => $name:literal $(referenced: $referenced: literal)? $($value:literal $(global: $global: literal)? $(from $from:literal)?)*),* } => { { @@ -18280,167 +18295,686 @@ mod tests { #[test] fn test_nesting() { + for spec in [NestingSpec::V1, NestingSpec::V2] { + nesting_test( + r#" + .foo { + color: blue; + & > .bar { color: red; } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } + + .foo > .bar { + color: red; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo { + color: blue; + &.bar { color: red; } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } + + .foo.bar { + color: red; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo, .bar { + color: blue; + & + .baz, &.qux { color: red; } + } + "#, + indoc! {r#" + .foo, .bar { + color: #00f; + } + + :is(.foo, .bar) + .baz, :is(.foo, .bar).qux { + color: red; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo { + color: blue; + & .bar & .baz & .qux { color: red; } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } + + .foo .bar .foo .baz .foo .qux { + color: red; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo { + color: blue; + & { padding: 2ch; } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } + + .foo { + padding: 2ch; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo { + color: blue; + && { padding: 2ch; } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } + + .foo.foo { + padding: 2ch; + } + "#}, + spec, + ); + + nesting_test( + r#" + .error, .invalid { + &:hover > .baz { color: red; } + } + "#, + indoc! {r#" + :is(.error, .invalid):hover > .baz { + color: red; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo { + &:is(.bar, &.baz) { color: red; } + } + "#, + indoc! {r#" + .foo:is(.bar, .foo.baz) { + color: red; + } + "#}, + spec, + ); + + nesting_test( + r#" + figure { + margin: 0; + + & > figcaption { + background: hsl(0 0% 0% / 50%); + + & > p { + font-size: .9rem; + } + } + } + "#, + indoc! {r#" + figure { + margin: 0; + } + + figure > figcaption { + background: #00000080; + } + + figure > figcaption > p { + font-size: .9rem; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo { + display: grid; + + @media (orientation: landscape) { + grid-auto-flow: column; + } + } + "#, + indoc! {r#" + .foo { + display: grid; + } + + @media (orientation: landscape) { + .foo { + grid-auto-flow: column; + } + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo { + display: grid; + + @media (orientation: landscape) { + grid-auto-flow: column; + + @media (width > 1024px) { + max-inline-size: 1024px; + } + } + } + "#, + indoc! {r#" + .foo { + display: grid; + } + + @media (orientation: landscape) { + .foo { + grid-auto-flow: column; + } + + @media (min-width: 1024px) { + .foo { + max-inline-size: 1024px; + } + } + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo { + display: grid; + + @supports (foo: bar) { + grid-auto-flow: column; + } + } + "#, + indoc! {r#" + .foo { + display: grid; + } + + @supports (foo: bar) { + .foo { + grid-auto-flow: column; + } + } + "#}, + spec, + ); + + nesting_test( + r#" + @namespace "http://example.com/foo"; + @namespace toto "http://toto.example.org"; + + .foo { + &div { + color: red; + } + + &* { + color: green; + } + + &|x { + color: red; + } + + &*|x { + color: green; + } + + &toto|x { + color: red; + } + } + "#, + indoc! {r#" + @namespace "http://example.com/foo"; + @namespace toto "http://toto.example.org"; + + div.foo { + color: red; + } + + *.foo { + color: green; + } + + |x.foo { + color: red; + } + + *|x.foo { + color: green; + } + + toto|x.foo { + color: red; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo { + &article > figure { + color: red; + } + } + "#, + indoc! {r#" + article.foo > figure { + color: red; + } + "#}, + spec, + ); + + nesting_test( + r#" + div { + &.bar { + background: green; + } + } + "#, + indoc! {r#" + div.bar { + background: green; + } + "#}, + spec, + ); + + nesting_test( + r#" + div > .foo { + &span { + background: green; + } + } + "#, + indoc! {r#" + span:is(div > .foo) { + background: green; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo { + & h1 { + background: green; + } + } + "#, + indoc! {r#" + .foo h1 { + background: green; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo .bar { + &h1 { + background: green; + } + } + "#, + indoc! {r#" + h1:is(.foo .bar) { + background: green; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo.bar { + &h1 { + background: green; + } + } + "#, + indoc! {r#" + h1.foo.bar { + background: green; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo .bar { + &h1 .baz { + background: green; + } + } + "#, + indoc! {r#" + h1:is(.foo .bar) .baz { + background: green; + } + "#}, + spec, + ); + + nesting_test( + r#" + .foo .bar { + &.baz { + background: green; + } + } + "#, + indoc! {r#" + .foo .bar.baz { + background: green; + } + "#}, + spec, + ); + } + nesting_test( r#" .foo { - color: blue; - & > .bar { color: red; } + color: red; + @nest .parent & { + color: blue; + } } "#, indoc! {r#" .foo { - color: #00f; + color: red; } - .foo > .bar { - color: red; + .parent .foo { + color: #00f; } "#}, + NestingSpec::V1, ); nesting_test( r#" .foo { - color: blue; - &.bar { color: red; } + color: red; + @nest :not(&) { + color: blue; + } } "#, indoc! {r#" .foo { - color: #00f; + color: red; } - .foo.bar { - color: red; + :not(.foo) { + color: #00f; } "#}, + NestingSpec::V1, ); nesting_test( r#" - .foo, .bar { + .foo { color: blue; - & + .baz, &.qux { color: red; } + @nest .bar & { + color: red; + &.baz { + color: green; + } + } } "#, indoc! {r#" - .foo, .bar { + .foo { color: #00f; } - :is(.foo, .bar) + .baz, :is(.foo, .bar).qux { + .bar .foo { color: red; } + + .bar .foo.baz { + color: green; + } "#}, + NestingSpec::V1, ); nesting_test( r#" .foo { - color: blue; - & .bar & .baz & .qux { color: red; } - } - "#, - indoc! {r#" - .foo { - color: #00f; - } + @nest :not(&) { + color: red; + } - .foo .bar .foo .baz .foo .qux { + & h1 { + background: green; + } + } + "#, + indoc! {r#" + :not(.foo) { color: red; } + + .foo h1 { + background: green; + } "#}, + NestingSpec::V1, ); nesting_test( r#" .foo { - color: blue; - & { padding: 2ch; } + & h1 { + background: green; + } + + @nest :not(&) { + color: red; + } } "#, indoc! {r#" - .foo { - color: #00f; + .foo h1 { + background: green; } - .foo { - padding: 2ch; + :not(.foo) { + color: red; } "#}, + NestingSpec::V1, ); nesting_test( r#" - .foo { - color: blue; - && { padding: 2ch; } + .foo .bar { + @nest h1& { + background: green; + } } "#, indoc! {r#" - .foo { - color: #00f; - } - - .foo.foo { - padding: 2ch; + h1:is(.foo .bar) { + background: green; } "#}, + NestingSpec::V1, ); nesting_test( r#" - .error, .invalid { - &:hover > .baz { color: red; } + @namespace "http://example.com/foo"; + @namespace toto "http://toto.example.org"; + + div { + @nest .foo& { + color: red; + } + } + + * { + @nest .foo& { + color: red; + } + } + + |x { + @nest .foo& { + color: red; + } + } + + *|x { + @nest .foo& { + color: red; + } + } + + toto|x { + @nest .foo& { + color: red; + } } "#, indoc! {r#" - :is(.error, .invalid):hover > .baz { + @namespace "http://example.com/foo"; + @namespace toto "http://toto.example.org"; + + .foo:is(div) { + color: red; + } + + .foo:is(*) { + color: red; + } + + .foo:is(|x) { + color: red; + } + + .foo:is(*|x) { + color: red; + } + + .foo:is(toto|x) { color: red; } "#}, + NestingSpec::V1, ); nesting_test( r#" - .foo { - &:is(.bar, &.baz) { color: red; } + .foo .bar { + @nest h1 .baz& { + background: green; + } } "#, indoc! {r#" - .foo:is(.bar, .foo.baz) { - color: red; + h1 .baz:is(.foo .bar) { + background: green; } "#}, + NestingSpec::V1, ); nesting_test( r#" - figure { - margin: 0; - - & > figcaption { - background: hsl(0 0% 0% / 50%); - - & > p { - font-size: .9rem; - } + .foo .bar { + @nest .baz& { + background: green; } } "#, indoc! {r#" - figure { - margin: 0; + .baz:is(.foo .bar) { + background: green; } + "#}, + NestingSpec::V1, + ); - figure > figcaption { - background: #00000080; + nesting_test( + r#" + .foo .bar { + @nest .baz & { + background: green; + } } - - figure > figcaption > p { - font-size: .9rem; + "#, + indoc! {r#" + .baz :is(.foo .bar) { + background: green; } "#}, + NestingSpec::V1, ); nesting_test( @@ -18461,13 +18995,122 @@ mod tests { color: #00f; } "#}, + NestingSpec::V1, + ); + + nesting_test( + r#" + .foo { + color: red; + .bar { + color: blue; + } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .foo .bar { + color: #00f; + } + "#}, + NestingSpec::V2, + ); + + nesting_test( + r#" + .foo { + color: red; + .bar & { + color: blue; + } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .bar .foo { + color: #00f; + } + "#}, + NestingSpec::V2, + ); + + nesting_test( + r#" + .foo { + color: red; + + .bar + & { color: blue; } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .foo + .bar + .foo { + color: #00f; + } + "#}, + NestingSpec::V2, + ); + + nesting_test( + r#" + .foo { + color: red; + .bar & { + color: blue; + } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .bar .foo { + color: #00f; + } + "#}, + NestingSpec::V2, + ); + + nesting_error_test( + r#" + .foo { + color: blue; + div { + color: red; + } + } + "#, + NestingSpec::V2, + ParserError::UnexpectedToken(crate::properties::custom::Token::CurlyBracketBlock), + ); + + nesting_error_test( + r#" + .foo { + color: blue; + @nest .bar { + color: red; + } + } + "#, + NestingSpec::V2, + ParserError::AtRuleInvalid("nest".into()), ); nesting_test( r#" .foo { color: red; - @nest .parent & { + .parent & { color: blue; } } @@ -18481,13 +19124,14 @@ mod tests { color: #00f; } "#}, + NestingSpec::V2, ); nesting_test( r#" .foo { color: red; - @nest :not(&) { + :not(&) { color: blue; } } @@ -18501,13 +19145,14 @@ mod tests { color: #00f; } "#}, + NestingSpec::V2, ); nesting_test( r#" .foo { color: blue; - @nest .bar & { + .bar & { color: red; &.baz { color: green; @@ -18528,153 +19173,71 @@ mod tests { color: green; } "#}, + NestingSpec::V2, ); nesting_test( r#" .foo { - display: grid; - - @media (orientation: landscape) { - grid-auto-flow: column; - } - } - "#, - indoc! {r#" - .foo { - display: grid; - } - - @media (orientation: landscape) { - .foo { - grid-auto-flow: column; - } - } - "#}, - ); - - nesting_test( - r#" - .foo { - display: grid; - - @media (orientation: landscape) { - grid-auto-flow: column; - - @media (width > 1024px) { - max-inline-size: 1024px; - } - } - } - "#, - indoc! {r#" - .foo { - display: grid; - } - - @media (orientation: landscape) { - .foo { - grid-auto-flow: column; - } - - @media (min-width: 1024px) { - .foo { - max-inline-size: 1024px; - } + :not(&) { + color: red; } - } - "#}, - ); - nesting_test( - r#" - .foo { - display: grid; - - @supports (foo: bar) { - grid-auto-flow: column; + & h1 { + background: green; } } "#, indoc! {r#" - .foo { - display: grid; + :not(.foo) { + color: red; } - @supports (foo: bar) { - .foo { - grid-auto-flow: column; - } + .foo h1 { + background: green; } "#}, + NestingSpec::V2, ); nesting_test( r#" - @namespace "http://example.com/foo"; - @namespace toto "http://toto.example.org"; - .foo { - &div { - color: red; - } - - &* { - color: green; - } - - &|x { - color: red; - } - - &*|x { - color: green; + & h1 { + background: green; } - &toto|x { + :not(&) { color: red; } } "#, indoc! {r#" - @namespace "http://example.com/foo"; - @namespace toto "http://toto.example.org"; - - div.foo { - color: red; - } - - *.foo { - color: green; - } - - |x.foo { - color: red; - } - - *|x.foo { - color: green; + .foo h1 { + background: green; } - toto|x.foo { + :not(.foo) { color: red; } "#}, + NestingSpec::V2, ); nesting_test( r#" - .foo { - &article > figure { - color: red; + .foo .bar { + :is(h1)& { + background: green; } } "#, indoc! {r#" - article.foo > figure { - color: red; + :is(h1):is(.foo .bar) { + background: green; } "#}, + NestingSpec::V2, ); nesting_test( @@ -18683,31 +19246,31 @@ mod tests { @namespace toto "http://toto.example.org"; div { - @nest .foo& { + .foo& { color: red; } } * { - @nest .foo& { + .foo& { color: red; } } |x { - @nest .foo& { + .foo& { color: red; } } *|x { - @nest .foo& { + .foo& { color: red; } } toto|x { - @nest .foo& { + .foo& { color: red; } } @@ -18736,217 +19299,97 @@ mod tests { color: red; } "#}, - ); - - nesting_test( - r#" - div { - &.bar { - background: green; - } - } - "#, - indoc! {r#" - div.bar { - background: green; - } - "#}, - ); - - nesting_test( - r#" - div > .foo { - &span { - background: green; - } - } - "#, - indoc! {r#" - span:is(div > .foo) { - background: green; - } - "#}, - ); - - nesting_test( - r#" - .foo { - & h1 { - background: green; - } - } - "#, - indoc! {r#" - .foo h1 { - background: green; - } - "#}, - ); - - nesting_test( - r#" - .foo { - @nest :not(&) { - color: red; - } - - & h1 { - background: green; - } - } - "#, - indoc! {r#" - :not(.foo) { - color: red; - } - - .foo h1 { - background: green; - } - "#}, - ); - - nesting_test( - r#" - .foo { - & h1 { - background: green; - } - - @nest :not(&) { - color: red; - } - } - "#, - indoc! {r#" - .foo h1 { - background: green; - } - - :not(.foo) { - color: red; - } - "#}, + NestingSpec::V2, ); nesting_test( r#" .foo .bar { - &h1 { + :is(h1) .baz& { background: green; } } "#, indoc! {r#" - h1:is(.foo .bar) { + :is(h1) .baz:is(.foo .bar) { background: green; } "#}, + NestingSpec::V2, ); nesting_test( r#" .foo .bar { - @nest h1& { - background: green; - } - } - "#, - indoc! {r#" - h1:is(.foo .bar) { - background: green; - } - "#}, - ); - - nesting_test( - r#" - .foo.bar { - &h1 { + .baz& { background: green; } } "#, indoc! {r#" - h1.foo.bar { + .baz:is(.foo .bar) { background: green; } "#}, + NestingSpec::V2, ); nesting_test( r#" .foo .bar { - &h1 .baz { + .baz & { background: green; } } "#, indoc! {r#" - h1:is(.foo .bar) .baz { + .baz :is(.foo .bar) { background: green; } "#}, + NestingSpec::V2, ); nesting_test( r#" - .foo .bar { - @nest h1 .baz& { - background: green; + .foo { + .bar { + color: blue; } + color: red; } "#, indoc! {r#" - h1 .baz:is(.foo .bar) { - background: green; + .foo { + color: red; } - "#}, - ); - nesting_test( - r#" .foo .bar { - &.baz { - background: green; - } - } - "#, - indoc! {r#" - .foo .bar.baz { - background: green; + color: #00f; } "#}, + NestingSpec::V2, ); nesting_test( r#" - .foo .bar { - @nest .baz& { - background: green; - } - } + article { + color: green; + & { color: blue; } + color: red; + } "#, indoc! {r#" - .baz:is(.foo .bar) { - background: green; + article { + color: green; + color: red; } - "#}, - ); - nesting_test( - r#" - .foo .bar { - @nest .baz & { - background: green; - } - } - "#, - indoc! {r#" - .baz :is(.foo .bar) { - background: green; + article { + color: #00f; } "#}, + NestingSpec::V2, ); nesting_test_no_targets( @@ -19777,7 +20220,7 @@ mod tests { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - nesting: true, + nesting: NestingSpec::V2, ..ParserOptions::default() }, ) @@ -19830,7 +20273,7 @@ mod tests { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - nesting: true, + nesting: NestingSpec::V1, ..ParserOptions::default() }, ) @@ -19883,7 +20326,7 @@ mod tests { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - nesting: true, + nesting: NestingSpec::V2, ..ParserOptions::default() }, ) diff --git a/src/main.rs b/src/main.rs index b95e9908..5a0c3701 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use clap::{ArgGroup, Parser}; use lightningcss::bundler::{Bundler, FileProvider}; -use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet}; +use lightningcss::stylesheet::{MinifyOptions, NestingSpec, ParserOptions, PrinterOptions, StyleSheet}; use lightningcss::targets::Browsers; use parcel_sourcemap::SourceMap; use serde::Serialize; @@ -110,7 +110,11 @@ pub fn main() -> Result<(), std::io::Error> { let res = { let mut options = ParserOptions { - nesting: cli_args.nesting, + nesting: if cli_args.nesting { + NestingSpec::V1 // TODO + } else { + NestingSpec::None + }, css_modules, custom_media: cli_args.custom_media, error_recovery: cli_args.error_recovery, diff --git a/src/parser.rs b/src/parser.rs index 079b76b5..f80a452c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -40,7 +40,7 @@ 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. - pub nesting: bool, + pub nesting: NestingSpec, /// Whether to enable the [custom media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) draft syntax. pub custom_media: bool, /// Whether the enable [CSS modules](https://github.com/css-modules/css-modules). @@ -54,6 +54,24 @@ pub struct ParserOptions<'o, 'i> { pub warnings: Option>>>>>, } +/// Version of the CSS nesting spec to enable. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum NestingSpec { + /// No nesting allowed. + None, + /// V1 spec, with required & and @nest rule. + /// This is deprecated, but available for backward compatibility. + V1, + /// V2 spec, with implicit &. + V2, +} + +impl Default for NestingSpec { + fn default() -> Self { + NestingSpec::None + } +} + impl<'o, 'i> ParserOptions<'o, 'i> { #[inline] pub(crate) fn warn(&self, warning: ParseError<'i, ParserError<'i>>) { @@ -640,7 +658,7 @@ impl<'a, 'o, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'o, 'i> { input: &mut Parser<'i, 't>, ) -> Result, ParseError<'i, Self::Error>> { let loc = self.loc(start); - let (declarations, rules) = if self.options.nesting { + let (declarations, rules) = if self.options.nesting != NestingSpec::None { parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)? } else { (DeclarationBlock::parse(input, self.options)?, CssRuleList(vec![])) @@ -664,7 +682,7 @@ fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't>( let mut important_declarations = DeclarationList::new(); let mut declarations = DeclarationList::new(); let mut rules = CssRuleList(vec![]); - let parser = StyleRuleParser { + let mut parser = StyleRuleParser { default_namespace, namespace_prefixes, options, @@ -673,28 +691,36 @@ fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't>( rules: &mut rules, }; - let mut declaration_parser = DeclarationListParser::new(input, parser); - let mut last = declaration_parser.input.state(); - while let Some(decl) = declaration_parser.next() { - match decl { - Ok(_) => {} - _ => { - declaration_parser.input.reset(&last); - break; + // In the v2 nesting spec, declarations and nested rules may be mixed. + // https://drafts.csswg.org/css-syntax/#consume-style-block + loop { + let start = input.state(); + match input.next_including_whitespace_and_comments() { + Ok(&Token::WhiteSpace(_)) | Ok(&Token::Comment(_)) | Ok(&Token::Semicolon) => continue, + Ok(&Token::Ident(ref name)) => { + let name = name.clone(); + let callback = |input: &mut Parser<'i, '_>| { + input.expect_colon()?; + parser.parse_value(name, input) + }; + input.parse_until_after(Delimiter::Semicolon, callback)?; } - } - - last = declaration_parser.input.state(); - } - - let mut iter = RuleListParser::new_for_nested_rule(declaration_parser.input, declaration_parser.parser); - while let Some(result) = iter.next() { - if let Err((err, _)) = result { - if options.error_recovery { - options.warn(err); - continue; + Ok(_) => { + input.reset(&start); + let mut iter = RuleListParser::new_for_nested_rule(input, parser); + if let Some(result) = iter.next() { + if let Err((err, _)) = result { + if options.error_recovery { + options.warn(err); + parser = iter.parser; + continue; + } + return Err(err); + } + } + parser = iter.parser; } - return Err(err); + Err(_) => break, } } @@ -726,7 +752,7 @@ impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for StyleRuleParser<'a, 'o, 'i name: CowRcStr<'i>, input: &mut cssparser::Parser<'i, 't>, ) -> Result> { - if !self.rules.0.is_empty() { + if !self.rules.0.is_empty() && self.options.nesting == NestingSpec::V1 { // Declarations cannot come after nested rules. return Err(input.new_custom_error(ParserError::InvalidNesting)); } @@ -759,7 +785,7 @@ impl<'a, 'o, 'i> AtRuleParser<'i> for StyleRuleParser<'a, 'o, 'i> { let cond = SupportsCondition::parse(input)?; Ok(AtRulePrelude::Supports(cond)) }, - "nest" => { + "nest" if self.options.nesting == NestingSpec::V1 => { let selector_parser = SelectorParser { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes, @@ -893,7 +919,11 @@ impl<'a, 'o, 'b, 'i> QualifiedRuleParser<'i> for StyleRuleParser<'a, 'o, 'i> { is_nesting_allowed: true, options: &self.options, }; - SelectorList::parse(&selector_parser, input, NestingRequirement::Prefixed) + match self.options.nesting { + NestingSpec::V1 => SelectorList::parse(&selector_parser, input, NestingRequirement::Prefixed), + NestingSpec::V2 => SelectorList::parse_relative(&selector_parser, input, NestingRequirement::Implicit), + NestingSpec::None => unreachable!(), + } } fn parse_block<'t>( diff --git a/src/stylesheet.rs b/src/stylesheet.rs index fa7cd592..08cd8c8f 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -18,7 +18,7 @@ use cssparser::{Parser, ParserInput, RuleListParser}; use parcel_sourcemap::SourceMap; use std::collections::{HashMap, HashSet}; -pub use crate::parser::ParserOptions; +pub use crate::parser::{NestingSpec, ParserOptions}; pub use crate::printer::PrinterOptions; pub use crate::printer::PseudoClasses; From 08fe9be24e904c94ca21510b5e1d76dfcd9353f1 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 21 Nov 2022 16:46:58 -0600 Subject: [PATCH 2/3] Add nesting v2 option to CLI --- src/main.rs | 32 +++++++++++++++++++++++++++----- tests/cli_integration_tests.rs | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5a0c3701..f1910f78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use clap::{ArgGroup, Parser}; +use clap::{ArgGroup, Parser, ValueEnum}; use lightningcss::bundler::{Bundler, FileProvider}; use lightningcss::stylesheet::{MinifyOptions, NestingSpec, ParserOptions, PrinterOptions, StyleSheet}; use lightningcss::targets::Browsers; @@ -28,8 +28,8 @@ struct CliArgs { #[clap(short, long, value_parser)] minify: bool, /// Enable parsing CSS nesting - #[clap(long, value_parser)] - nesting: bool, + #[clap(long, value_parser, require_equals(true))] + nesting: Option>, /// Enable parsing custom media queries #[clap(long, value_parser)] custom_media: bool, @@ -55,6 +55,28 @@ struct CliArgs { error_recovery: bool, } +#[derive(ValueEnum, Clone, Debug)] +#[clap(rename_all = "lower")] +enum Nesting { + V1, + V2, +} + +impl Default for Nesting { + fn default() -> Self { + Nesting::V1 + } +} + +impl Into for Nesting { + fn into(self) -> NestingSpec { + match self { + Nesting::V1 => NestingSpec::V1, + Nesting::V2 => NestingSpec::V2, + } + } +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct SourceMapJson<'a> { @@ -110,8 +132,8 @@ pub fn main() -> Result<(), std::io::Error> { let res = { let mut options = ParserOptions { - nesting: if cli_args.nesting { - NestingSpec::V1 // TODO + nesting: if let Some(nesting) = cli_args.nesting { + nesting.map_or(NestingSpec::V1, |n| n.into()) } else { NestingSpec::None }, diff --git a/tests/cli_integration_tests.rs b/tests/cli_integration_tests.rs index 2379b7e8..b210ae6c 100644 --- a/tests/cli_integration_tests.rs +++ b/tests/cli_integration_tests.rs @@ -207,8 +207,6 @@ fn minify_option() -> Result<(), Box> { } #[test] -// nesting doesn't do anything with the default targets. until cli supports more targets, this option is a noop -#[ignore] fn nesting_option() -> Result<(), Box> { let infile = assert_fs::NamedTempFile::new("test.css")?; infile.write_str( @@ -222,6 +220,7 @@ fn nesting_option() -> Result<(), Box> { let mut cmd = Command::cargo_bin("lightningcss")?; cmd.arg(infile.path()); + cmd.arg("--targets=defaults"); cmd.arg("--nesting"); cmd.assert().success().stdout(predicate::str::contains(indoc! {r#" .foo { @@ -236,6 +235,35 @@ fn nesting_option() -> Result<(), Box> { Ok(()) } +#[test] +fn nesting_v2() -> Result<(), Box> { + let infile = assert_fs::NamedTempFile::new("test.css")?; + infile.write_str( + r#" + .foo { + color: blue; + .bar { color: red; } + } + "#, + )?; + + let mut cmd = Command::cargo_bin("lightningcss")?; + cmd.arg(infile.path()); + cmd.arg("--targets=defaults"); + cmd.arg("--nesting=v2"); + cmd.assert().success().stdout(predicate::str::contains(indoc! {r#" + .foo { + color: #00f; + } + + .foo .bar { + color: red; + } + "#})); + + Ok(()) +} + #[test] fn css_modules_infer_output_file() -> Result<(), Box> { let (input, _, exports) = css_module_test_vals(); From ca9d3c3a654e7ebae7ee998ec5a6fd38353f359c Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 21 Nov 2022 18:00:12 -0600 Subject: [PATCH 3/3] Remove two separate nesting versions, just deprecate @nest --- node/index.d.ts | 9 +- node/src/lib.rs | 50 +-- src/error.rs | 3 + src/lib.rs | 745 +++++++++++++++------------------ src/main.rs | 36 +- src/parser.rs | 35 +- src/stylesheet.rs | 2 +- tests/cli_integration_tests.rs | 29 -- 8 files changed, 360 insertions(+), 549 deletions(-) diff --git a/node/index.d.ts b/node/index.d.ts index 79b766ba..36a62fda 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -67,13 +67,8 @@ export interface Resolver { } export interface Drafts { - /** - * Whether to enable CSS nesting. - * If a number is provided, that version of the spec is used. - * v1 required an & in every selector, and supported the @nest rule. - * v2 has implicit nesting for descendant combinators. - */ - nesting?: boolean | 1 | 2, + /** Whether to enable CSS nesting. */ + nesting?: boolean, /** Whether to enable @custom-media rules. */ customMedia?: boolean } diff --git a/node/src/lib.rs b/node/src/lib.rs index a6370d30..4b1072ab 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -7,7 +7,7 @@ use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternPa use lightningcss::dependencies::{Dependency, DependencyOptions}; use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind}; use lightningcss::stylesheet::{ - MinifyOptions, NestingSpec, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet, + MinifyOptions, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet, }; use lightningcss::targets::Browsers; use parcel_sourcemap::SourceMap; @@ -540,43 +540,11 @@ impl<'a> Into> for &'a OwnedPseudoClasses { #[serde(rename_all = "camelCase")] struct Drafts { #[serde(default)] - nesting: NestingOption, + nesting: bool, #[serde(default)] custom_media: bool, } -#[derive(Serialize, Debug, Deserialize)] -#[serde(untagged)] -enum NestingOption { - Bool(bool), - Version(u8), -} - -impl Default for NestingOption { - fn default() -> Self { - NestingOption::Bool(false) - } -} - -impl NestingOption { - fn into_nesting_spec<'i, E: std::error::Error>(&self) -> Result> { - Ok(match self { - NestingOption::Bool(n) => { - if *n { - NestingSpec::V1 - } else { - NestingSpec::None - } - } - NestingOption::Version(v) => match *v { - 1 => NestingSpec::V1, - 2 => NestingSpec::V2, - v => return Err(CompileError::InvalidNestingSpec(v)), - }, - }) - } -} - fn compile<'i>(code: &'i str, config: &Config) -> Result, CompileError<'i, std::io::Error>> { let drafts = config.drafts.as_ref(); let warnings = Some(Arc::new(RwLock::new(Vec::new()))); @@ -596,11 +564,7 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result, Co &code, ParserOptions { filename: filename.clone(), - nesting: if let Some(drafts) = &config.drafts { - drafts.nesting.into_nesting_spec()? - } else { - NestingSpec::None - }, + nesting: matches!(drafts, Some(d) if d.nesting), custom_media: matches!(drafts, Some(d) if d.custom_media), css_modules: if let Some(css_modules) = &config.css_modules { match css_modules { @@ -693,11 +657,7 @@ fn compile_bundle<'i, P: SourceProvider>( let res = { let drafts = config.drafts.as_ref(); let parser_options = ParserOptions { - nesting: if let Some(drafts) = &config.drafts { - drafts.nesting.into_nesting_spec()? - } else { - NestingSpec::None - }, + nesting: matches!(drafts, Some(d) if d.nesting), custom_media: matches!(drafts, Some(d) if d.custom_media), css_modules: if let Some(css_modules) = &config.css_modules { match css_modules { @@ -870,7 +830,6 @@ enum CompileError<'i, E: std::error::Error> { SourceMapError(parcel_sourcemap::SourceMapError), BundleError(Error>), PatternError(PatternParseError), - InvalidNestingSpec(u8), } impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> { @@ -882,7 +841,6 @@ impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> { CompileError::BundleError(err) => err.kind.fmt(f), CompileError::PatternError(err) => err.fmt(f), CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this - CompileError::InvalidNestingSpec(v) => write!(f, "Invalid nesting version {}", v), } } } diff --git a/src/error.rs b/src/error.rs index 6fc1bdad..ecd9c9fd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -75,6 +75,8 @@ pub enum ParserError<'i> { InvalidMediaQuery, /// Invalid CSS nesting. InvalidNesting, + /// The @nest rule is deprecated. + DeprecatedNestRule, /// An invalid selector in an `@page` rule. InvalidPageSelector, /// An invalid value was encountered. @@ -103,6 +105,7 @@ impl<'i> fmt::Display for ParserError<'i> { InvalidDeclaration => write!(f, "Invalid declaration"), InvalidMediaQuery => write!(f, "Invalid media query"), InvalidNesting => write!(f, "Invalid nesting"), + DeprecatedNestRule => write!(f, "The @nest rule is deprecated"), InvalidPageSelector => write!(f, "Invalid page selector"), InvalidValue => write!(f, "Invalid value"), QualifiedRuleInvalid => write!(f, "Invalid qualified rule"), diff --git a/src/lib.rs b/src/lib.rs index de75adb7..f42ee151 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,6 @@ mod tests { use crate::css_modules::{CssModuleExport, CssModuleExports, CssModuleReference, CssModuleReferences}; use crate::dependencies::Dependency; use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind, SelectorError}; - use crate::parser::NestingSpec; use crate::properties::custom::Token; use crate::properties::Property; use crate::rules::CssRule; @@ -112,7 +111,7 @@ mod tests { assert_eq!(res.code, expected); } - fn nesting_test(source: &str, expected: &str, spec: NestingSpec) { + fn nesting_test(source: &str, expected: &str) { let targets = Some(Browsers { chrome: Some(95 << 16), ..Browsers::default() @@ -120,7 +119,7 @@ mod tests { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - nesting: spec, + nesting: true, ..ParserOptions::default() }, ) @@ -144,7 +143,7 @@ mod tests { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - nesting: NestingSpec::V1, + nesting: true, ..ParserOptions::default() }, ) @@ -207,11 +206,11 @@ mod tests { } } - fn nesting_error_test(source: &str, spec: NestingSpec, error: ParserError) { + fn nesting_error_test(source: &str, error: ParserError) { let res = StyleSheet::parse( &source, ParserOptions { - nesting: spec, + nesting: true, ..Default::default() }, ); @@ -18295,443 +18294,420 @@ mod tests { #[test] fn test_nesting() { - for spec in [NestingSpec::V1, NestingSpec::V2] { - nesting_test( - r#" - .foo { - color: blue; - & > .bar { color: red; } - } - "#, - indoc! {r#" - .foo { - color: #00f; - } + nesting_test( + r#" + .foo { + color: blue; + & > .bar { color: red; } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } - .foo > .bar { - color: red; - } - "#}, - spec, - ); + .foo > .bar { + color: red; + } + "#}, + ); - nesting_test( - r#" - .foo { - color: blue; - &.bar { color: red; } - } - "#, - indoc! {r#" - .foo { - color: #00f; - } + nesting_test( + r#" + .foo { + color: blue; + &.bar { color: red; } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } - .foo.bar { - color: red; - } - "#}, - spec, - ); + .foo.bar { + color: red; + } + "#}, + ); - nesting_test( - r#" - .foo, .bar { - color: blue; - & + .baz, &.qux { color: red; } - } - "#, - indoc! {r#" - .foo, .bar { - color: #00f; - } + nesting_test( + r#" + .foo, .bar { + color: blue; + & + .baz, &.qux { color: red; } + } + "#, + indoc! {r#" + .foo, .bar { + color: #00f; + } - :is(.foo, .bar) + .baz, :is(.foo, .bar).qux { - color: red; - } - "#}, - spec, - ); + :is(.foo, .bar) + .baz, :is(.foo, .bar).qux { + color: red; + } + "#}, + ); - nesting_test( - r#" - .foo { - color: blue; - & .bar & .baz & .qux { color: red; } - } - "#, - indoc! {r#" - .foo { - color: #00f; - } + nesting_test( + r#" + .foo { + color: blue; + & .bar & .baz & .qux { color: red; } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } - .foo .bar .foo .baz .foo .qux { - color: red; - } - "#}, - spec, - ); + .foo .bar .foo .baz .foo .qux { + color: red; + } + "#}, + ); - nesting_test( - r#" - .foo { - color: blue; - & { padding: 2ch; } - } - "#, - indoc! {r#" - .foo { - color: #00f; - } + nesting_test( + r#" + .foo { + color: blue; + & { padding: 2ch; } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } - .foo { - padding: 2ch; - } - "#}, - spec, - ); + .foo { + padding: 2ch; + } + "#}, + ); - nesting_test( - r#" - .foo { - color: blue; - && { padding: 2ch; } - } - "#, - indoc! {r#" - .foo { - color: #00f; - } + nesting_test( + r#" + .foo { + color: blue; + && { padding: 2ch; } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } - .foo.foo { - padding: 2ch; - } - "#}, - spec, - ); + .foo.foo { + padding: 2ch; + } + "#}, + ); - nesting_test( - r#" - .error, .invalid { - &:hover > .baz { color: red; } - } - "#, - indoc! {r#" - :is(.error, .invalid):hover > .baz { - color: red; - } - "#}, - spec, - ); + nesting_test( + r#" + .error, .invalid { + &:hover > .baz { color: red; } + } + "#, + indoc! {r#" + :is(.error, .invalid):hover > .baz { + color: red; + } + "#}, + ); - nesting_test( - r#" - .foo { - &:is(.bar, &.baz) { color: red; } - } - "#, - indoc! {r#" - .foo:is(.bar, .foo.baz) { - color: red; - } - "#}, - spec, - ); + nesting_test( + r#" + .foo { + &:is(.bar, &.baz) { color: red; } + } + "#, + indoc! {r#" + .foo:is(.bar, .foo.baz) { + color: red; + } + "#}, + ); - nesting_test( - r#" - figure { - margin: 0; + nesting_test( + r#" + figure { + margin: 0; - & > figcaption { - background: hsl(0 0% 0% / 50%); + & > figcaption { + background: hsl(0 0% 0% / 50%); - & > p { - font-size: .9rem; - } + & > p { + font-size: .9rem; } } - "#, - indoc! {r#" - figure { - margin: 0; - } - - figure > figcaption { - background: #00000080; - } + } + "#, + indoc! {r#" + figure { + margin: 0; + } - figure > figcaption > p { - font-size: .9rem; - } - "#}, - spec, - ); + figure > figcaption { + background: #00000080; + } - nesting_test( - r#" - .foo { - display: grid; + figure > figcaption > p { + font-size: .9rem; + } + "#}, + ); - @media (orientation: landscape) { - grid-auto-flow: column; - } - } - "#, - indoc! {r#" - .foo { - display: grid; - } + nesting_test( + r#" + .foo { + display: grid; @media (orientation: landscape) { - .foo { - grid-auto-flow: column; - } + grid-auto-flow: column; } - "#}, - spec, - ); - - nesting_test( - r#" - .foo { - display: grid; - - @media (orientation: landscape) { - grid-auto-flow: column; + } + "#, + indoc! {r#" + .foo { + display: grid; + } - @media (width > 1024px) { - max-inline-size: 1024px; - } - } - } - "#, - indoc! {r#" + @media (orientation: landscape) { .foo { - display: grid; + grid-auto-flow: column; } + } + "#}, + ); + + nesting_test( + r#" + .foo { + display: grid; @media (orientation: landscape) { - .foo { - grid-auto-flow: column; - } + grid-auto-flow: column; - @media (min-width: 1024px) { - .foo { - max-inline-size: 1024px; - } + @media (width > 1024px) { + max-inline-size: 1024px; } } - "#}, - spec, - ); - - nesting_test( - r#" - .foo { - display: grid; + } + "#, + indoc! {r#" + .foo { + display: grid; + } - @supports (foo: bar) { - grid-auto-flow: column; - } - } - "#, - indoc! {r#" + @media (orientation: landscape) { .foo { - display: grid; + grid-auto-flow: column; } - @supports (foo: bar) { + @media (min-width: 1024px) { .foo { - grid-auto-flow: column; + max-inline-size: 1024px; } } - "#}, - spec, - ); - - nesting_test( - r#" - @namespace "http://example.com/foo"; - @namespace toto "http://toto.example.org"; - - .foo { - &div { - color: red; - } - - &* { - color: green; - } + } + "#}, + ); - &|x { - color: red; - } + nesting_test( + r#" + .foo { + display: grid; - &*|x { - color: green; - } + @supports (foo: bar) { + grid-auto-flow: column; + } + } + "#, + indoc! {r#" + .foo { + display: grid; + } - &toto|x { - color: red; - } + @supports (foo: bar) { + .foo { + grid-auto-flow: column; } - "#, - indoc! {r#" - @namespace "http://example.com/foo"; - @namespace toto "http://toto.example.org"; + } + "#}, + ); + + nesting_test( + r#" + @namespace "http://example.com/foo"; + @namespace toto "http://toto.example.org"; - div.foo { + .foo { + &div { color: red; } - *.foo { + &* { color: green; } - |x.foo { + &|x { color: red; } - *|x.foo { + &*|x { color: green; } - toto|x.foo { + &toto|x { color: red; } - "#}, - spec, - ); + } + "#, + indoc! {r#" + @namespace "http://example.com/foo"; + @namespace toto "http://toto.example.org"; - nesting_test( - r#" - .foo { - &article > figure { - color: red; - } - } - "#, - indoc! {r#" - article.foo > figure { + div.foo { + color: red; + } + + *.foo { + color: green; + } + + |x.foo { + color: red; + } + + *|x.foo { + color: green; + } + + toto|x.foo { + color: red; + } + "#}, + ); + + nesting_test( + r#" + .foo { + &article > figure { color: red; } - "#}, - spec, - ); + } + "#, + indoc! {r#" + article.foo > figure { + color: red; + } + "#}, + ); - nesting_test( - r#" - div { - &.bar { - background: green; - } - } - "#, - indoc! {r#" - div.bar { + nesting_test( + r#" + div { + &.bar { background: green; } - "#}, - spec, - ); + } + "#, + indoc! {r#" + div.bar { + background: green; + } + "#}, + ); - nesting_test( - r#" - div > .foo { - &span { - background: green; - } - } - "#, - indoc! {r#" - span:is(div > .foo) { + nesting_test( + r#" + div > .foo { + &span { background: green; } - "#}, - spec, - ); + } + "#, + indoc! {r#" + span:is(div > .foo) { + background: green; + } + "#}, + ); - nesting_test( - r#" - .foo { - & h1 { - background: green; - } - } - "#, - indoc! {r#" - .foo h1 { + nesting_test( + r#" + .foo { + & h1 { background: green; } - "#}, - spec, - ); + } + "#, + indoc! {r#" + .foo h1 { + background: green; + } + "#}, + ); - nesting_test( - r#" - .foo .bar { - &h1 { - background: green; - } - } - "#, - indoc! {r#" - h1:is(.foo .bar) { + nesting_test( + r#" + .foo .bar { + &h1 { background: green; } - "#}, - spec, - ); + } + "#, + indoc! {r#" + h1:is(.foo .bar) { + background: green; + } + "#}, + ); - nesting_test( - r#" - .foo.bar { - &h1 { - background: green; - } - } - "#, - indoc! {r#" - h1.foo.bar { + nesting_test( + r#" + .foo.bar { + &h1 { background: green; } - "#}, - spec, - ); + } + "#, + indoc! {r#" + h1.foo.bar { + background: green; + } + "#}, + ); - nesting_test( - r#" - .foo .bar { - &h1 .baz { - background: green; - } - } - "#, - indoc! {r#" - h1:is(.foo .bar) .baz { + nesting_test( + r#" + .foo .bar { + &h1 .baz { background: green; } - "#}, - spec, - ); + } + "#, + indoc! {r#" + h1:is(.foo .bar) .baz { + background: green; + } + "#}, + ); - nesting_test( - r#" - .foo .bar { - &.baz { - background: green; - } - } - "#, - indoc! {r#" - .foo .bar.baz { + nesting_test( + r#" + .foo .bar { + &.baz { background: green; } - "#}, - spec, - ); - } + } + "#, + indoc! {r#" + .foo .bar.baz { + background: green; + } + "#}, + ); nesting_test( r#" @@ -18751,7 +18727,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V1, ); nesting_test( @@ -18772,7 +18747,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V1, ); nesting_test( @@ -18800,7 +18774,6 @@ mod tests { color: green; } "#}, - NestingSpec::V1, ); nesting_test( @@ -18824,7 +18797,6 @@ mod tests { background: green; } "#}, - NestingSpec::V1, ); nesting_test( @@ -18848,7 +18820,6 @@ mod tests { color: red; } "#}, - NestingSpec::V1, ); nesting_test( @@ -18864,7 +18835,6 @@ mod tests { background: green; } "#}, - NestingSpec::V1, ); nesting_test( @@ -18926,7 +18896,6 @@ mod tests { color: red; } "#}, - NestingSpec::V1, ); nesting_test( @@ -18942,7 +18911,6 @@ mod tests { background: green; } "#}, - NestingSpec::V1, ); nesting_test( @@ -18958,7 +18926,6 @@ mod tests { background: green; } "#}, - NestingSpec::V1, ); nesting_test( @@ -18974,7 +18941,6 @@ mod tests { background: green; } "#}, - NestingSpec::V1, ); nesting_test( @@ -18995,7 +18961,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V1, ); nesting_test( @@ -19016,7 +18981,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19037,7 +19001,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19056,7 +19019,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19077,7 +19039,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V2, ); nesting_error_test( @@ -19089,23 +19050,9 @@ mod tests { } } "#, - NestingSpec::V2, ParserError::UnexpectedToken(crate::properties::custom::Token::CurlyBracketBlock), ); - nesting_error_test( - r#" - .foo { - color: blue; - @nest .bar { - color: red; - } - } - "#, - NestingSpec::V2, - ParserError::AtRuleInvalid("nest".into()), - ); - nesting_test( r#" .foo { @@ -19124,7 +19071,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19145,7 +19091,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19173,7 +19118,6 @@ mod tests { color: green; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19197,7 +19141,6 @@ mod tests { background: green; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19221,7 +19164,6 @@ mod tests { color: red; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19237,7 +19179,6 @@ mod tests { background: green; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19299,7 +19240,6 @@ mod tests { color: red; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19315,7 +19255,6 @@ mod tests { background: green; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19331,7 +19270,6 @@ mod tests { background: green; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19347,7 +19285,6 @@ mod tests { background: green; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19368,7 +19305,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V2, ); nesting_test( @@ -19389,7 +19325,6 @@ mod tests { color: #00f; } "#}, - NestingSpec::V2, ); nesting_test_no_targets( @@ -20220,7 +20155,7 @@ mod tests { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - nesting: NestingSpec::V2, + nesting: true, ..ParserOptions::default() }, ) @@ -20273,7 +20208,7 @@ mod tests { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - nesting: NestingSpec::V1, + nesting: true, ..ParserOptions::default() }, ) @@ -20326,7 +20261,7 @@ mod tests { let mut stylesheet = StyleSheet::parse( &source, ParserOptions { - nesting: NestingSpec::V2, + nesting: true, ..ParserOptions::default() }, ) diff --git a/src/main.rs b/src/main.rs index f1910f78..b95e9908 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ -use clap::{ArgGroup, Parser, ValueEnum}; +use clap::{ArgGroup, Parser}; use lightningcss::bundler::{Bundler, FileProvider}; -use lightningcss::stylesheet::{MinifyOptions, NestingSpec, ParserOptions, PrinterOptions, StyleSheet}; +use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet}; use lightningcss::targets::Browsers; use parcel_sourcemap::SourceMap; use serde::Serialize; @@ -28,8 +28,8 @@ struct CliArgs { #[clap(short, long, value_parser)] minify: bool, /// Enable parsing CSS nesting - #[clap(long, value_parser, require_equals(true))] - nesting: Option>, + #[clap(long, value_parser)] + nesting: bool, /// Enable parsing custom media queries #[clap(long, value_parser)] custom_media: bool, @@ -55,28 +55,6 @@ struct CliArgs { error_recovery: bool, } -#[derive(ValueEnum, Clone, Debug)] -#[clap(rename_all = "lower")] -enum Nesting { - V1, - V2, -} - -impl Default for Nesting { - fn default() -> Self { - Nesting::V1 - } -} - -impl Into for Nesting { - fn into(self) -> NestingSpec { - match self { - Nesting::V1 => NestingSpec::V1, - Nesting::V2 => NestingSpec::V2, - } - } -} - #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct SourceMapJson<'a> { @@ -132,11 +110,7 @@ pub fn main() -> Result<(), std::io::Error> { let res = { let mut options = ParserOptions { - nesting: if let Some(nesting) = cli_args.nesting { - nesting.map_or(NestingSpec::V1, |n| n.into()) - } else { - NestingSpec::None - }, + nesting: cli_args.nesting, css_modules, custom_media: cli_args.custom_media, error_recovery: cli_args.error_recovery, diff --git a/src/parser.rs b/src/parser.rs index f80a452c..701e0bc3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -40,7 +40,7 @@ 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. - pub nesting: NestingSpec, + pub nesting: bool, /// Whether to enable the [custom media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) draft syntax. pub custom_media: bool, /// Whether the enable [CSS modules](https://github.com/css-modules/css-modules). @@ -54,24 +54,6 @@ pub struct ParserOptions<'o, 'i> { pub warnings: Option>>>>>, } -/// Version of the CSS nesting spec to enable. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum NestingSpec { - /// No nesting allowed. - None, - /// V1 spec, with required & and @nest rule. - /// This is deprecated, but available for backward compatibility. - V1, - /// V2 spec, with implicit &. - V2, -} - -impl Default for NestingSpec { - fn default() -> Self { - NestingSpec::None - } -} - impl<'o, 'i> ParserOptions<'o, 'i> { #[inline] pub(crate) fn warn(&self, warning: ParseError<'i, ParserError<'i>>) { @@ -658,7 +640,7 @@ impl<'a, 'o, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'o, 'i> { input: &mut Parser<'i, 't>, ) -> Result, ParseError<'i, Self::Error>> { let loc = self.loc(start); - let (declarations, rules) = if self.options.nesting != NestingSpec::None { + let (declarations, rules) = if self.options.nesting { parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)? } else { (DeclarationBlock::parse(input, self.options)?, CssRuleList(vec![])) @@ -752,10 +734,6 @@ impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for StyleRuleParser<'a, 'o, 'i name: CowRcStr<'i>, input: &mut cssparser::Parser<'i, 't>, ) -> Result> { - if !self.rules.0.is_empty() && self.options.nesting == NestingSpec::V1 { - // Declarations cannot come after nested rules. - return Err(input.new_custom_error(ParserError::InvalidNesting)); - } parse_declaration( name, input, @@ -785,7 +763,8 @@ impl<'a, 'o, 'i> AtRuleParser<'i> for StyleRuleParser<'a, 'o, 'i> { let cond = SupportsCondition::parse(input)?; Ok(AtRulePrelude::Supports(cond)) }, - "nest" if self.options.nesting == NestingSpec::V1 => { + "nest" => { + self.options.warn(input.new_custom_error(ParserError::DeprecatedNestRule)); let selector_parser = SelectorParser { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes, @@ -919,11 +898,7 @@ impl<'a, 'o, 'b, 'i> QualifiedRuleParser<'i> for StyleRuleParser<'a, 'o, 'i> { is_nesting_allowed: true, options: &self.options, }; - match self.options.nesting { - NestingSpec::V1 => SelectorList::parse(&selector_parser, input, NestingRequirement::Prefixed), - NestingSpec::V2 => SelectorList::parse_relative(&selector_parser, input, NestingRequirement::Implicit), - NestingSpec::None => unreachable!(), - } + SelectorList::parse_relative(&selector_parser, input, NestingRequirement::Implicit) } fn parse_block<'t>( diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 08cd8c8f..fa7cd592 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -18,7 +18,7 @@ use cssparser::{Parser, ParserInput, RuleListParser}; use parcel_sourcemap::SourceMap; use std::collections::{HashMap, HashSet}; -pub use crate::parser::{NestingSpec, ParserOptions}; +pub use crate::parser::ParserOptions; pub use crate::printer::PrinterOptions; pub use crate::printer::PseudoClasses; diff --git a/tests/cli_integration_tests.rs b/tests/cli_integration_tests.rs index b210ae6c..fff378ea 100644 --- a/tests/cli_integration_tests.rs +++ b/tests/cli_integration_tests.rs @@ -235,35 +235,6 @@ fn nesting_option() -> Result<(), Box> { Ok(()) } -#[test] -fn nesting_v2() -> Result<(), Box> { - let infile = assert_fs::NamedTempFile::new("test.css")?; - infile.write_str( - r#" - .foo { - color: blue; - .bar { color: red; } - } - "#, - )?; - - let mut cmd = Command::cargo_bin("lightningcss")?; - cmd.arg(infile.path()); - cmd.arg("--targets=defaults"); - cmd.arg("--nesting=v2"); - cmd.assert().success().stdout(predicate::str::contains(indoc! {r#" - .foo { - color: #00f; - } - - .foo .bar { - color: red; - } - "#})); - - Ok(()) -} - #[test] fn css_modules_infer_output_file() -> Result<(), Box> { let (input, _, exports) = css_module_test_vals();