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 b534833f..be30a248 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) } @@ -1743,6 +1762,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; @@ -1806,8 +1837,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); @@ -1834,6 +1873,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>, @@ -1851,11 +1891,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) @@ -2399,7 +2449,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/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 64675ff8..1db3edd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,6 +207,20 @@ mod tests { } } + fn nesting_error_test(source: &str, error: ParserError) { + let res = StyleSheet::parse( + &source, + ParserOptions { + nesting: true, + ..ParserOptions::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)?)*),* } => { { @@ -18444,93 +18458,6 @@ mod tests { "#}, ); - nesting_test( - r#" - .foo { - color: red; - @nest & > .bar { - color: blue; - } - } - "#, - indoc! {r#" - .foo { - color: red; - } - - .foo > .bar { - color: #00f; - } - "#}, - ); - - nesting_test( - r#" - .foo { - color: red; - @nest .parent & { - color: blue; - } - } - "#, - indoc! {r#" - .foo { - color: red; - } - - .parent .foo { - color: #00f; - } - "#}, - ); - - nesting_test( - r#" - .foo { - color: red; - @nest :not(&) { - color: blue; - } - } - "#, - indoc! {r#" - .foo { - color: red; - } - - :not(.foo) { - color: #00f; - } - "#}, - ); - - nesting_test( - r#" - .foo { - color: blue; - @nest .bar & { - color: red; - &.baz { - color: green; - } - } - } - "#, - indoc! {r#" - .foo { - color: #00f; - } - - .bar .foo { - color: red; - } - - .bar .foo.baz { - color: green; - } - "#}, - ); - nesting_test( r#" .foo { @@ -18678,67 +18605,6 @@ mod tests { "#}, ); - nesting_test( - r#" - @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#" - @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; - } - "#}, - ); - nesting_test( r#" div { @@ -18786,22 +18652,14 @@ mod tests { nesting_test( r#" - .foo { - @nest :not(&) { - color: red; - } - - & h1 { + .foo .bar { + &h1 { background: green; } } "#, indoc! {r#" - :not(.foo) { - color: red; - } - - .foo h1 { + h1:is(.foo .bar) { background: green; } "#}, @@ -18809,37 +18667,29 @@ mod tests { nesting_test( r#" - .foo { - & h1 { + .foo.bar { + &h1 { background: green; } - - @nest :not(&) { - color: red; - } } "#, indoc! {r#" - .foo h1 { + h1.foo.bar { background: green; } - - :not(.foo) { - color: red; - } "#}, ); nesting_test( r#" .foo .bar { - &h1 { + &h1 .baz { background: green; } } "#, indoc! {r#" - h1:is(.foo .bar) { + h1:is(.foo .bar) .baz { background: green; } "#}, @@ -18848,13 +18698,13 @@ mod tests { nesting_test( r#" .foo .bar { - @nest h1& { + &.baz { background: green; } } "#, indoc! {r#" - h1:is(.foo .bar) { + .foo .bar.baz { background: green; } "#}, @@ -18862,59 +18712,203 @@ mod tests { nesting_test( r#" - .foo.bar { - &h1 { - background: green; + .foo { + color: red; + @nest .parent & { + color: blue; } } "#, indoc! {r#" - h1.foo.bar { - background: green; + .foo { + color: red; + } + + .parent .foo { + color: #00f; } "#}, ); nesting_test( r#" - .foo .bar { - &h1 .baz { - background: green; + .foo { + color: red; + @nest :not(&) { + color: blue; } } "#, indoc! {r#" - h1:is(.foo .bar) .baz { - background: green; + .foo { + color: red; + } + + :not(.foo) { + color: #00f; } "#}, ); nesting_test( r#" - .foo .bar { - @nest h1 .baz& { - background: green; + .foo { + color: blue; + @nest .bar & { + color: red; + &.baz { + color: green; + } } } "#, indoc! {r#" - h1 .baz:is(.foo .bar) { - background: green; + .foo { + color: #00f; + } + + .bar .foo { + color: red; + } + + .bar .foo.baz { + color: green; } "#}, ); nesting_test( r#" - .foo .bar { - &.baz { + .foo { + @nest :not(&) { + color: red; + } + + & h1 { background: green; } } "#, indoc! {r#" - .foo .bar.baz { + :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; + } + "#}, + ); + + nesting_test( + r#" + .foo .bar { + @nest h1& { + background: green; + } + } + "#, + indoc! {r#" + h1:is(.foo .bar) { + background: green; + } + "#}, + ); + + nesting_test( + r#" + @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#" + @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; + } + "#}, + ); + + nesting_test( + r#" + .foo .bar { + @nest h1 .baz& { + background: green; + } + } + "#, + indoc! {r#" + h1 .baz:is(.foo .bar) { background: green; } "#}, @@ -18950,6 +18944,390 @@ mod tests { "#}, ); + nesting_test( + r#" + .foo { + color: red; + @nest & > .bar { + color: blue; + } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .foo > .bar { + color: #00f; + } + "#}, + ); + + nesting_test( + r#" + .foo { + color: red; + .bar { + color: blue; + } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .foo .bar { + color: #00f; + } + "#}, + ); + + nesting_test( + r#" + .foo { + color: red; + .bar & { + color: blue; + } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .bar .foo { + color: #00f; + } + "#}, + ); + + nesting_test( + r#" + .foo { + color: red; + + .bar + & { color: blue; } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .foo + .bar + .foo { + color: #00f; + } + "#}, + ); + + nesting_test( + r#" + .foo { + color: red; + .bar & { + color: blue; + } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .bar .foo { + color: #00f; + } + "#}, + ); + + nesting_error_test( + r#" + .foo { + color: blue; + div { + color: red; + } + } + "#, + ParserError::UnexpectedToken(crate::properties::custom::Token::CurlyBracketBlock), + ); + + nesting_test( + r#" + .foo { + color: red; + .parent & { + color: blue; + } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .parent .foo { + color: #00f; + } + "#}, + ); + + nesting_test( + r#" + .foo { + color: red; + :not(&) { + color: blue; + } + } + "#, + indoc! {r#" + .foo { + color: red; + } + + :not(.foo) { + color: #00f; + } + "#}, + ); + + nesting_test( + r#" + .foo { + color: blue; + .bar & { + color: red; + &.baz { + color: green; + } + } + } + "#, + indoc! {r#" + .foo { + color: #00f; + } + + .bar .foo { + color: red; + } + + .bar .foo.baz { + color: green; + } + "#}, + ); + + nesting_test( + r#" + .foo { + :not(&) { + color: red; + } + + & h1 { + background: green; + } + } + "#, + indoc! {r#" + :not(.foo) { + color: red; + } + + .foo h1 { + background: green; + } + "#}, + ); + + nesting_test( + r#" + .foo { + & h1 { + background: green; + } + + :not(&) { + color: red; + } + } + "#, + indoc! {r#" + .foo h1 { + background: green; + } + + :not(.foo) { + color: red; + } + "#}, + ); + + nesting_test( + r#" + .foo .bar { + :is(h1)& { + background: green; + } + } + "#, + indoc! {r#" + :is(h1):is(.foo .bar) { + background: green; + } + "#}, + ); + + nesting_test( + r#" + @namespace "http://example.com/foo"; + @namespace toto "http://toto.example.org"; + + div { + .foo& { + color: red; + } + } + + * { + .foo& { + color: red; + } + } + + |x { + .foo& { + color: red; + } + } + + *|x { + .foo& { + color: red; + } + } + + toto|x { + .foo& { + color: red; + } + } + "#, + indoc! {r#" + @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; + } + "#}, + ); + + nesting_test( + r#" + .foo .bar { + :is(h1) .baz& { + background: green; + } + } + "#, + indoc! {r#" + :is(h1) .baz:is(.foo .bar) { + background: green; + } + "#}, + ); + + nesting_test( + r#" + .foo .bar { + .baz& { + background: green; + } + } + "#, + indoc! {r#" + .baz:is(.foo .bar) { + background: green; + } + "#}, + ); + + nesting_test( + r#" + .foo .bar { + .baz & { + background: green; + } + } + "#, + indoc! {r#" + .baz :is(.foo .bar) { + background: green; + } + "#}, + ); + + nesting_test( + r#" + .foo { + .bar { + color: blue; + } + color: red; + } + "#, + indoc! {r#" + .foo { + color: red; + } + + .foo .bar { + color: #00f; + } + "#}, + ); + + nesting_test( + r#" + article { + color: green; + & { color: blue; } + color: red; + } + "#, + indoc! {r#" + article { + color: green; + color: red; + } + + article { + color: #00f; + } + "#}, + ); + nesting_test_no_targets( r#" .foo { diff --git a/src/parser.rs b/src/parser.rs index 1a3dd2e0..5f9812cd 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -752,7 +752,7 @@ fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( 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, @@ -761,28 +761,36 @@ fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( 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 iter.parser.options.error_recovery { - iter.parser.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 iter.parser.options.error_recovery { + iter.parser.options.warn(err); + parser = iter.parser; + continue; + } + return Err(err); + } + } + parser = iter.parser; } - return Err(err); + Err(_) => break, } } @@ -814,10 +822,6 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> cssparser::DeclarationParser<'i> for Style name: CowRcStr<'i>, input: &mut cssparser::Parser<'i, 't>, ) -> Result> { - if !self.rules.0.is_empty() { - // Declarations cannot come after nested rules. - return Err(input.new_custom_error(ParserError::InvalidNesting)); - } parse_declaration( name, input, @@ -848,6 +852,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' Ok(AtRulePrelude::Supports(cond)) }, "nest" => { + self.options.warn(input.new_custom_error(ParserError::DeprecatedNestRule)); let selector_parser = SelectorParser { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes, @@ -1039,7 +1044,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for StyleRuleP is_nesting_allowed: true, options: &self.options, }; - SelectorList::parse(&selector_parser, input, NestingRequirement::Prefixed) + SelectorList::parse_relative(&selector_parser, input, NestingRequirement::Implicit) } fn parse_block<'t>( diff --git a/tests/cli_integration_tests.rs b/tests/cli_integration_tests.rs index 2379b7e8..fff378ea 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 {