diff --git a/build-prefixes.js b/build-prefixes.js index ff1b6274..594ec4b4 100644 --- a/build-prefixes.js +++ b/build-prefixes.js @@ -155,7 +155,8 @@ let cssFeatures = [ 'css-autofill', 'css-namespaces', 'shadowdomv1', - 'css-rrggbbaa' + 'css-rrggbbaa', + 'css-nesting' ]; let compat = new Map(); diff --git a/node/index.d.ts b/node/index.d.ts index ea83f4b7..0c6c8564 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -10,7 +10,14 @@ export interface TransformOptions { /** Whether to output a source map. */ source_map?: boolean, /** The browser targets for the generated code. */ - targets?: Targets + targets?: Targets, + /** Whether to enable various draft syntax. */ + drafts?: Drafts +} + +export interface Drafts { + /** Whether to enable CSS nesting. */ + nesting?: boolean } export interface TransformResult { diff --git a/node/src/lib.rs b/node/src/lib.rs index 0ba5a1f2..67809ab8 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -3,7 +3,7 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; use serde::{Serialize, Deserialize}; -use parcel_css::stylesheet::{StyleSheet, StyleAttribute}; +use parcel_css::stylesheet::{StyleSheet, StyleAttribute, ParserOptions}; use parcel_css::targets::Browsers; // --------------------------------------------- @@ -103,11 +103,23 @@ struct Config { pub code: Vec, pub targets: Option, pub minify: Option, - pub source_map: Option + pub source_map: Option, + pub drafts: Option +} + +#[derive(Serialize, Debug, Deserialize, Default)] +struct Drafts { + nesting: bool } fn compile<'i>(code: &'i str, config: &Config) -> Result> { - let mut stylesheet = StyleSheet::parse(config.filename.clone(), &code)?; + let options = config.drafts.as_ref(); + let mut stylesheet = StyleSheet::parse(config.filename.clone(), &code, ParserOptions { + nesting: match options { + Some(o) => o.nesting, + None => false + } + })?; stylesheet.minify(config.targets); // TODO: should this be conditional? let (res, source_map) = stylesheet.to_css( config.minify.unwrap_or(false), diff --git a/playground/index.html b/playground/index.html index 3616b860..e10bf9be 100644 --- a/playground/index.html +++ b/playground/index.html @@ -6,7 +6,9 @@ html, body { margin: 0; height: 100%; - box-sizing: border-box + box-sizing: border-box; + font-family: -apple-system, system-ui; + color-scheme: dark light; } body { @@ -39,13 +41,25 @@ flex: 1; font: 14px monospace; } + + h3 { + margin-bottom: 4px; + } + + h3:first-child { + margin-top: 0; + }

Parcel CSS Playground

+

Options

+

Draft syntax

+ +

Targets

@@ -56,7 +70,6 @@

Parcel CSS Playground

-
diff --git a/playground/playground.js b/playground/playground.js index 38b60797..14265c23 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -11,21 +11,10 @@ function loadPlaygroundState() { reflectPlaygroundState(playgroundState); } catch { const initialPlaygroundState = { - minify: true, - targets: { - chrome: 95 << 16, - }, - source: `.foo { - background: yellow; - - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - - -webkit-transition: background 200ms; - -moz-transition: background 200ms; - transition: background 200ms; -}`, + minify: minify.checked, + nesting: nesting.checked, + targets: getTargets(), + source: source.value, }; reflectPlaygroundState(initialPlaygroundState); @@ -37,17 +26,15 @@ function reflectPlaygroundState(playgroundState) { minify.checked = playgroundState.minify; } + if (typeof playgroundState.nesting !== 'undefined') { + nesting.checked = playgroundState.nesting; + } + if (playgroundState.targets) { const {targets} = playgroundState; - for (const target in targets) { - const value = targets[target]; - if (value) { - for (const input of Array.from(inputs)) { - if (input.id === target) { - input.value = value >> 16; - } - } - } + for (let input of inputs) { + let value = targets[input.id]; + input.value = value == null ? '' : value >> 16; } } @@ -59,6 +46,7 @@ function reflectPlaygroundState(playgroundState) { function savePlaygroundState() { const playgroundState = { minify: minify.checked, + nesting: nesting.checked, targets: getTargets(), source: source.value, }; @@ -98,6 +86,9 @@ async function update() { code: enc.encode(source.value), minify: minify.checked, targets: Object.keys(targets).length === 0 ? null : targets, + drafts: { + nesting: nesting.checked + } }); compiled.value = dec.decode(res.code); diff --git a/selectors/builder.rs b/selectors/builder.rs index 2ac278e5..256be35e 100644 --- a/selectors/builder.rs +++ b/selectors/builder.rs @@ -346,6 +346,9 @@ where Component::Namespace(..) => { // Does not affect specificity }, + Component::Nesting => { + // TODO + } } } diff --git a/selectors/matching.rs b/selectors/matching.rs index 1d116063..18f74309 100644 --- a/selectors/matching.rs +++ b/selectors/matching.rs @@ -858,6 +858,7 @@ where } true }), + Component::Nesting => unreachable!() } } diff --git a/selectors/parser.rs b/selectors/parser.rs index ff897ae9..2ec0323d 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -110,6 +110,9 @@ bitflags! { /// Whether we explicitly disallow pseudo-element-like things. const DISALLOW_PSEUDOS = 1 << 6; + + /// Whether we have seen a nesting selector. + const AFTER_NESTING = 1 << 7; } } @@ -167,6 +170,7 @@ pub enum SelectorParseErrorKind<'i> { InvalidPseudoElementAfterSlotted, InvalidPseudoElementInsideWhere, InvalidState, + MissingNestingSelector, UnexpectedTokenInAttributeSelector(Token<'i>), PseudoElementExpectedColon(Token<'i>), PseudoElementExpectedIdent(Token<'i>), @@ -325,6 +329,10 @@ pub trait Parser<'i> { ) -> Option<::NamespaceUrl> { None } + + fn is_nesting_allowed(&self) -> bool { + false + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -343,6 +351,13 @@ pub enum ParseErrorRecovery { IgnoreInvalidSelector, } +#[derive(Eq, PartialEq, Clone, Copy)] +pub enum NestingRequirement { + None, + Prefixed, + Contained +} + impl SelectorList { /// Parse a comma-separated list of Selectors. /// @@ -351,6 +366,7 @@ impl SelectorList { pub fn parse<'i, 't, P>( parser: &P, input: &mut CssParser<'i, 't>, + nesting_requirement: NestingRequirement, ) -> Result> where P: Parser<'i, Impl = Impl>, @@ -358,8 +374,9 @@ impl SelectorList { Self::parse_with_state( parser, input, - SelectorParsingState::empty(), + &mut SelectorParsingState::empty(), ParseErrorRecovery::DiscardList, + nesting_requirement, ) } @@ -367,16 +384,23 @@ impl SelectorList { fn parse_with_state<'i, 't, P>( parser: &P, input: &mut CssParser<'i, 't>, - state: SelectorParsingState, + state: &mut SelectorParsingState, recovery: ParseErrorRecovery, + nesting_requirement: NestingRequirement, ) -> Result> where P: Parser<'i, Impl = Impl>, { + let original_state = *state; let mut values = SmallVec::new(); loop { let selector = input.parse_until_before(Delimiter::Comma, |input| { - parse_selector(parser, input, state) + let mut selector_state = original_state; + let result = parse_selector(parser, input, &mut selector_state, nesting_requirement); + if selector_state.contains(SelectorParsingState::AFTER_NESTING) { + state.insert(SelectorParsingState::AFTER_NESTING) + } + result }); let was_ok = selector.is_ok(); @@ -410,17 +434,23 @@ impl SelectorList { fn parse_inner_compound_selector<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, - state: SelectorParsingState, + state: &mut SelectorParsingState, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { - parse_selector( + let mut child_state = *state | SelectorParsingState::DISALLOW_PSEUDOS | SelectorParsingState::DISALLOW_COMBINATORS; + let result = parse_selector( parser, input, - state | SelectorParsingState::DISALLOW_PSEUDOS | SelectorParsingState::DISALLOW_COMBINATORS, - ) + &mut child_state, + NestingRequirement::None, + )?; + if child_state.contains(SelectorParsingState::AFTER_NESTING) { + state.insert(SelectorParsingState::AFTER_NESTING) + } + Ok(result) } /// Ancestor hashes for the bloom filter. We precompute these and store them @@ -742,6 +772,18 @@ impl Selector { Selector(builder.build_with_specificity_and_flags(spec)) } + pub fn from_vec2(vec: Vec>) -> Self { + let mut builder = SelectorBuilder::default(); + for component in vec.into_iter() { + if let Some(combinator) = component.as_combinator() { + builder.push_combinator(combinator); + } else { + builder.push_simple_selector(component); + } + } + Selector(builder.build(false, false, false)) + } + /// Returns count of simple selectors and combinators in the Selector. #[inline] pub fn len(&self) -> usize { @@ -1072,6 +1114,12 @@ pub enum Component { Is(Box<[Selector]>), /// An implementation-dependent pseudo-element selector. PseudoElement(Impl::PseudoElement), + /// A nesting selector: + /// + /// https://drafts.csswg.org/css-nesting-1/#nest-selector + /// + /// NOTE: This is a parcel_css addition. + Nesting } impl Component { @@ -1542,6 +1590,7 @@ impl ToCss for Component { dest.write_str(")") }, NonTSPseudoClass(ref pseudo) => pseudo.to_css(dest), + Nesting => dest.write_char('&') } } } @@ -1600,12 +1649,19 @@ impl ToCss for LocalName { fn parse_selector<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, - mut state: SelectorParsingState, + state: &mut SelectorParsingState, + nesting_requirement: NestingRequirement, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { + if nesting_requirement == NestingRequirement::Prefixed { + let state = input.state(); + input.expect_delim('&')?; + input.reset(&state); + } + let mut builder = SelectorBuilder::default(); let mut has_pseudo_element = false; @@ -1613,7 +1669,7 @@ where let mut part = false; 'outer_loop: loop { // Parse a sequence of simple selectors. - let empty = parse_compound_selector(parser, &mut state, input, &mut builder)?; + let empty = parse_compound_selector(parser, state, input, &mut builder)?; if empty { return Err(input.new_custom_error(if builder.has_combinators() { SelectorParseErrorKind::DanglingCombinator @@ -1669,6 +1725,10 @@ where builder.push_combinator(combinator); } + if nesting_requirement == NestingRequirement::Contained && !state.contains(SelectorParsingState::AFTER_NESTING) { + return Err(input.new_custom_error(SelectorParseErrorKind::MissingNestingSelector)) + } + Ok(Selector(builder.build(has_pseudo_element, slotted, part))) } @@ -1682,7 +1742,7 @@ impl Selector { where P: Parser<'i, Impl = Impl>, { - parse_selector(parser, input, SelectorParsingState::empty()) + parse_selector(parser, input, &mut SelectorParsingState::empty(), NestingRequirement::None) } } @@ -2081,21 +2141,27 @@ fn parse_attribute_flags<'i, 't>( fn parse_negation<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, - state: SelectorParsingState, + state: &mut SelectorParsingState, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { + let mut child_state = *state | + SelectorParsingState::SKIP_DEFAULT_NAMESPACE | + SelectorParsingState::DISALLOW_PSEUDOS; let list = SelectorList::parse_with_state( parser, input, - state | - SelectorParsingState::SKIP_DEFAULT_NAMESPACE | - SelectorParsingState::DISALLOW_PSEUDOS, + &mut child_state, ParseErrorRecovery::DiscardList, + NestingRequirement::None, )?; + if child_state.contains(SelectorParsingState::AFTER_NESTING) { + state.insert(SelectorParsingState::AFTER_NESTING) + } + Ok(Component::Negation(list.0.into_vec().into_boxed_slice())) } @@ -2118,12 +2184,18 @@ where input.skip_whitespace(); let mut empty = true; + if parser.is_nesting_allowed() && input.try_parse(|input| input.expect_delim('&')).is_ok() { + state.insert(SelectorParsingState::AFTER_NESTING); + builder.push_simple_selector(Component::Nesting); + empty = false; + } + if parse_type_selector(parser, input, *state, builder)? { empty = false; } loop { - let result = match parse_one_simple_selector(parser, input, *state)? { + let result = match parse_one_simple_selector(parser, input, state)? { None => break, Some(result) => result, }; @@ -2201,7 +2273,7 @@ where fn parse_is_or_where<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, - state: SelectorParsingState, + state: &mut SelectorParsingState, component: impl FnOnce(Box<[Selector]>) -> Component, ) -> Result, ParseError<'i, P::Error>> where @@ -2214,14 +2286,19 @@ where // Pseudo-elements cannot be represented by the matches-any // pseudo-class; they are not valid within :is(). // + let mut child_state = *state | + SelectorParsingState::SKIP_DEFAULT_NAMESPACE | + SelectorParsingState::DISALLOW_PSEUDOS; let inner = SelectorList::parse_with_state( parser, input, - state | - SelectorParsingState::SKIP_DEFAULT_NAMESPACE | - SelectorParsingState::DISALLOW_PSEUDOS, + &mut child_state, parser.is_and_where_error_recovery(), + NestingRequirement::None, )?; + if child_state.contains(SelectorParsingState::AFTER_NESTING) { + state.insert(SelectorParsingState::AFTER_NESTING) + } Ok(component(inner.0.into_vec().into_boxed_slice())) } @@ -2229,17 +2306,17 @@ fn parse_functional_pseudo_class<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, name: CowRcStr<'i>, - state: SelectorParsingState, + state: &mut SelectorParsingState, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { match_ignore_ascii_case! { &name, - "nth-child" => return parse_nth_pseudo_class(parser, input, state, Component::NthChild), - "nth-of-type" => return parse_nth_pseudo_class(parser, input, state, Component::NthOfType), - "nth-last-child" => return parse_nth_pseudo_class(parser, input, state, Component::NthLastChild), - "nth-last-of-type" => return parse_nth_pseudo_class(parser, input, state, Component::NthLastOfType), + "nth-child" => return parse_nth_pseudo_class(parser, input, *state, Component::NthChild), + "nth-of-type" => return parse_nth_pseudo_class(parser, input, *state, Component::NthOfType), + "nth-last-child" => return parse_nth_pseudo_class(parser, input, *state, Component::NthLastChild), + "nth-last-of-type" => return parse_nth_pseudo_class(parser, input, *state, Component::NthLastOfType), "is" if parser.parse_is_and_where() => return parse_is_or_where(parser, input, state, Component::Is), "where" if parser.parse_is_and_where() => return parse_is_or_where(parser, input, state, Component::Where), "host" => { @@ -2302,7 +2379,7 @@ fn is_css2_pseudo_element(name: &str) -> bool { fn parse_one_simple_selector<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, - state: SelectorParsingState, + state: &mut SelectorParsingState, ) -> Result>, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, @@ -2413,11 +2490,15 @@ where parse_functional_pseudo_class(parser, input, name, state) })? } else { - parse_simple_pseudo_class(parser, location, name, state)? + parse_simple_pseudo_class(parser, location, name, *state)? }; SimpleSelectorParseResult::SimpleSelector(pseudo_class) } }, + Token::Delim('&') if parser.is_nesting_allowed() => { + *state |= SelectorParsingState::AFTER_NESTING; + SimpleSelectorParseResult::SimpleSelector(Component::Nesting) + }, _ => { input.reset(&start); return Ok(None); @@ -2725,7 +2806,7 @@ pub mod tests { expected: Option<&'a str>, ) -> Result, SelectorParseError<'i>> { let mut parser_input = ParserInput::new(input); - let result = SelectorList::parse(parser, &mut CssParser::new(&mut parser_input)); + let result = SelectorList::parse(parser, &mut CssParser::new(&mut parser_input), NestingRequirement::None); if let Ok(ref selectors) = result { assert_eq!(selectors.0.len(), 1); // We can't assume that the serialized parsed selector will equal @@ -2749,7 +2830,7 @@ pub mod tests { #[test] fn test_empty() { let mut input = ParserInput::new(":empty"); - let list = SelectorList::parse(&DummyParser::default(), &mut CssParser::new(&mut input)); + let list = SelectorList::parse(&DummyParser::default(), &mut CssParser::new(&mut input), NestingRequirement::None); assert!(list.is_ok()); } diff --git a/src/compat.rs b/src/compat.rs index 34af4097..61fa9bde 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -19,6 +19,7 @@ pub enum Feature { CssMarkerPseudo, CssMatchesPseudo, CssNamespaces, + CssNesting, CssOptionalPseudo, CssPlaceholder, CssPlaceholderShown, @@ -1076,6 +1077,8 @@ impl Feature { } } } + Feature::CssNesting | + Feature::MediaIntervalSyntax => {} Feature::DoublePositionGradients => { if let Some(version) = browsers.chrome { if version >= 4653056 { @@ -1284,7 +1287,6 @@ impl Feature { } } } - Feature::MediaIntervalSyntax => {} } false } diff --git a/src/lib.rs b/src/lib.rs index 7f0fd6a5..ca779f5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,25 +17,26 @@ pub mod targets; #[cfg(test)] mod tests { use crate::stylesheet::*; + use crate::parser::ParserOptions; use crate::targets::Browsers; use indoc::indoc; fn test(source: &str, expected: &str) { - let mut stylesheet = StyleSheet::parse("test.css".into(), source).unwrap(); + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); stylesheet.minify(None); let (res, _) = stylesheet.to_css(false, false, None).unwrap(); assert_eq!(res, expected); } fn minify_test(source: &str, expected: &str) { - let mut stylesheet = StyleSheet::parse("test.css".into(), source).unwrap(); + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); stylesheet.minify(None); let (res, _) = stylesheet.to_css(true, false, None).unwrap(); assert_eq!(res, expected); } fn prefix_test(source: &str, expected: &str, targets: Browsers) { - let mut stylesheet = StyleSheet::parse("test.css".into(), source).unwrap(); + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); stylesheet.minify(Some(targets)); let (res, _) = stylesheet.to_css(false, false, Some(targets)).unwrap(); assert_eq!(res, expected); @@ -48,6 +49,24 @@ mod tests { assert_eq!(res, expected); } + fn nesting_test(source: &str, expected: &str) { + let targets = Some(Browsers { + chrome: Some(95 << 16), + ..Browsers::default() + }); + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { nesting: true }).unwrap(); + stylesheet.minify(targets); + let (res, _) = stylesheet.to_css(false, false, targets).unwrap(); + assert_eq!(res, expected); + } + + fn nesting_test_no_targets(source: &str, expected: &str) { + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { nesting: true }).unwrap(); + stylesheet.minify(None); + let (res, _) = stylesheet.to_css(false, false, None).unwrap(); + assert_eq!(res, expected); + } + #[test] pub fn test_border() { test(r#" @@ -4718,6 +4737,86 @@ mod tests { minify_test("@namespace \"http://toto.example.org\";", "@namespace \"http://toto.example.org\";"); minify_test("@namespace toto \"http://toto.example.org\";", "@namespace toto \"http://toto.example.org\";"); minify_test("@namespace toto url(http://toto.example.org);", "@namespace toto \"http://toto.example.org\";"); + + test(r#" + @namespace "http://example.com/foo"; + + x { + color: red; + } + "#, indoc! {r#" + @namespace "http://example.com/foo"; + + x { + color: red; + } + "#}); + + test(r#" + @namespace toto "http://toto.example.org"; + + toto|x { + color: red; + } + + [toto|att=val] { + color: blue + } + "#, indoc! {r#" + @namespace toto "http://toto.example.org"; + + toto|x { + color: red; + } + + [toto|att="val"] { + color: #00f; + } + "#}); + + test(r#" + @namespace "http://example.com/foo"; + + |x { + color: red; + } + + [|att=val] { + color: blue + } + "#, indoc! {r#" + @namespace "http://example.com/foo"; + + |x { + color: red; + } + + [att="val"] { + color: #00f; + } + "#}); + + test(r#" + @namespace "http://example.com/foo"; + + *|x { + color: red; + } + + [*|att=val] { + color: blue + } + "#, indoc! {r#" + @namespace "http://example.com/foo"; + + *|x { + color: red; + } + + [*|att="val"] { + color: #00f; + } + "#}); } #[test] @@ -6783,4 +6882,611 @@ mod tests { attr_test("color: yellow; flex: 1 1 auto", "color: #ff0; flex: auto", false); attr_test("color: yellow; flex: 1 1 auto", "color:#ff0;flex:auto", true); } + + #[test] + fn test_nesting() { + nesting_test( + r#" + .foo { + color: blue; + & > .bar { color: red; } + } + "#, + indoc!{r#" + .foo { + color: #00f; + } + + .foo > .bar { + color: red; + } + "#} + ); + + nesting_test( + r#" + .foo { + color: blue; + &.bar { color: red; } + } + "#, + indoc!{r#" + .foo { + color: #00f; + } + + .foo.bar { + color: red; + } + "#} + ); + + 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; + } + "#} + ); + + nesting_test( + r#" + .foo { + color: blue; + & .bar & .baz & .qux { color: red; } + } + "#, + indoc!{r#" + .foo { + color: #00f; + } + + .foo .bar .foo .baz .foo .qux { + color: red; + } + "#} + ); + + nesting_test( + r#" + .foo { + color: blue; + & { padding: 2ch; } + } + "#, + indoc!{r#" + .foo { + color: #00f; + } + + .foo { + padding: 2ch; + } + "#} + ); + + nesting_test( + r#" + .foo { + color: blue; + && { padding: 2ch; } + } + "#, + indoc!{r#" + .foo { + color: #00f; + } + + .foo.foo { + padding: 2ch; + } + "#} + ); + + 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; + } + "#} + ); + + 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; + } + "#} + ); + + 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 { + 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; + } + } + } + "#} + ); + + 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; + } + } + "#} + ); + + nesting_test( + r#" + @namespace "http://example.com/foo"; + @namespace toto "http://toto.example.org"; + + .foo { + &div { + color: red; + } + + &* { + color: red; + } + + &|x { + color: red; + } + + &*|x { + color: red; + } + + &toto|x { + color: red; + } + } + "#, + indoc!{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; + } + "#} + ); + + nesting_test( + r#" + .foo { + &article > figure { + color: red; + } + } + "#, + indoc!{r#" + article.foo > figure { + color: red; + } + "#} + ); + + 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 { + &.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; + } + "#} + ); + + nesting_test_no_targets( + r#" + .foo { + color: blue; + @nest .bar & { + color: red; + &.baz { + color: green; + } + } + } + "#, + indoc!{r#" + .foo { + color: #00f; + + @nest .bar & { + color: red; + + &.baz { + color: green; + } + } + } + "#} + ); + + nesting_test_no_targets( + r#" + .foo { + color: blue; + &div { + color: red; + } + + &span { + color: purple; + } + } + "#, + indoc!{r#" + .foo { + color: #00f; + + &div { + color: red; + } + + &span { + color: purple; + } + } + "#} + ); + } } diff --git a/src/parser.rs b/src/parser.rs index ae728ccc..f77e446c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,5 +1,5 @@ use cssparser::*; -use parcel_selectors::SelectorList; +use parcel_selectors::{SelectorList, parser::NestingRequirement}; use crate::media_query::*; use crate::traits::Parse; use crate::selector::{Selectors, SelectorParser}; @@ -15,18 +15,41 @@ use crate::rules::{ import::ImportRule, media::MediaRule, style::StyleRule, - document::MozDocumentRule + document::MozDocumentRule, + nesting::NestingRule }; use crate::values::ident::CustomIdent; -use crate::declaration::DeclarationBlock; +use crate::declaration::{DeclarationBlock, Declaration}; use crate::vendor_prefix::VendorPrefix; +use std::collections::HashMap; + +#[derive(Default)] +pub struct ParserOptions { + pub nesting: bool +} /// The parser for the top-level rules in a stylesheet. -pub struct TopLevelRuleParser {} +pub struct TopLevelRuleParser { + default_namespace: Option, + namespace_prefixes: HashMap, + options: ParserOptions +} impl<'b> TopLevelRuleParser { - fn nested<'a: 'b>(&'a self) -> NestedRuleParser { - NestedRuleParser {} + pub fn new(options: ParserOptions) -> TopLevelRuleParser { + TopLevelRuleParser { + default_namespace: None, + namespace_prefixes: HashMap::new(), + options + } + } + + fn nested<'a: 'b>(&'a mut self) -> NestedRuleParser { + NestedRuleParser { + default_namespace: &mut self.default_namespace, + namespace_prefixes: &mut self.namespace_prefixes, + options: &self.options + } } } @@ -57,7 +80,9 @@ pub enum AtRulePrelude { /// A @namespace rule prelude. Namespace(Option, String), /// A @charset rule prelude. - Charset + Charset, + /// A @nest prelude. + Nest(SelectorList) } impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser { @@ -130,6 +155,12 @@ impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser { }) }, AtRulePrelude::Namespace(prefix, url) => { + if let Some(prefix) = &prefix { + self.namespace_prefixes.insert(prefix.clone(), url.clone()); + } else { + self.default_namespace = Some(url.clone()); + } + CssRule::Namespace(NamespaceRule { prefix, url, @@ -170,11 +201,19 @@ impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser { } #[derive(Clone)] -struct NestedRuleParser {} +struct NestedRuleParser<'a> { + default_namespace: &'a Option, + namespace_prefixes: &'a HashMap, + options: &'a ParserOptions +} -impl<'a, 'b> NestedRuleParser { +impl<'a, 'b> NestedRuleParser<'a> { fn parse_nested_rules(&mut self, input: &mut Parser) -> CssRuleList { - let nested_parser = NestedRuleParser {}; + let nested_parser = NestedRuleParser { + default_namespace: self.default_namespace, + namespace_prefixes: self.namespace_prefixes, + options: self.options + }; let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser); let mut rules = Vec::new(); @@ -192,7 +231,7 @@ impl<'a, 'b> NestedRuleParser { } } -impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser { +impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a> { type Prelude = AtRulePrelude; type AtRule = CssRule; type Error = (); @@ -371,7 +410,7 @@ impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser { } } -impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser { +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a> { type Prelude = SelectorList; type QualifiedRule = CssRule; type Error = (); @@ -380,8 +419,12 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser { &mut self, input: &mut Parser<'i, 't>, ) -> Result> { - let selector_parser = SelectorParser {}; - match SelectorList::parse(&selector_parser, input) { + let selector_parser = SelectorParser { + default_namespace: self.default_namespace, + namespace_prefixes: self.namespace_prefixes, + is_nesting_allowed: false + }; + match SelectorList::parse(&selector_parser, input, NestingRequirement::None) { Ok(x) => Ok(x), Err(_) => Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) } @@ -394,13 +437,234 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser { input: &mut Parser<'i, 't>, ) -> Result> { let loc = start.source_location(); + let (declarations, rules) = if self.options.nesting { + parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes)? + } else { + (DeclarationBlock::parse(input)?, CssRuleList(vec![])) + }; Ok(CssRule::Style(StyleRule { selectors, vendor_prefix: VendorPrefix::empty(), - declarations: DeclarationBlock::parse(input)?, + declarations, + rules, + loc + })) + } +} + +#[derive(Debug)] +pub enum DeclarationOrRule { + Declaration(Declaration), + Rule(CssRule) +} + +fn parse_declarations_and_nested_rules<'a, 'i, 't>( + input: &mut Parser<'i, 't>, + default_namespace: &'a Option, + namespace_prefixes: &'a HashMap +) -> Result<(DeclarationBlock, CssRuleList), ParseError<'i, ()>> { + let mut declarations = vec![]; + let mut rules = vec![]; + let parser = StyleRuleParser { + default_namespace, + namespace_prefixes + }; + + 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(DeclarationOrRule::Declaration(decl)) => { + if rules.len() > 0 { + // Declarations cannot come after nested rules. + return Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) + } + + declarations.push(decl); + } + Ok(DeclarationOrRule::Rule(rule)) => rules.push(rule), + _ => { + declaration_parser.input.reset(&last); + break + } + } + + 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() { + match result { + Ok(DeclarationOrRule::Rule(CssRule::Ignored)) => {}, + Ok(DeclarationOrRule::Rule(rule)) => rules.push(rule), + Ok(DeclarationOrRule::Declaration(_)) => unreachable!(), + Err(_) => { + // TODO + }, + } + } + + Ok((DeclarationBlock { declarations }, CssRuleList(rules))) +} + +pub struct StyleRuleParser<'a> { + default_namespace: &'a Option, + namespace_prefixes: &'a HashMap +} + +/// Parse a declaration within {} block: `color: blue` +impl<'a, 'i> cssparser::DeclarationParser<'i> for StyleRuleParser<'a> { + type Declaration = DeclarationOrRule; + type Error = (); + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut cssparser::Parser<'i, 't>, + ) -> Result> { + Ok(DeclarationOrRule::Declaration(Declaration::parse(name, input)?)) + } +} + +impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a> { + type Prelude = AtRulePrelude; + type AtRule = DeclarationOrRule; + type Error = (); + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result> { + match_ignore_ascii_case! { &*name, + "media" => { + let media = MediaList::parse(input); + Ok(AtRulePrelude::Media(media)) + }, + "supports" => { + let cond = SupportsCondition::parse(input)?; + Ok(AtRulePrelude::Supports(cond)) + }, + "nest" => { + let selector_parser = SelectorParser { + default_namespace: self.default_namespace, + namespace_prefixes: self.namespace_prefixes, + is_nesting_allowed: true + }; + let selectors = match SelectorList::parse(&selector_parser, input, NestingRequirement::Contained) { + Ok(x) => x, + Err(_) => return Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) + }; + Ok(AtRulePrelude::Nest(selectors)) + }, + _ => Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) + } + } + + fn parse_block<'t>( + &mut self, + prelude: AtRulePrelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result> { + let loc = start.source_location(); + match prelude { + AtRulePrelude::Media(query) => { + Ok(DeclarationOrRule::Rule(CssRule::Media(MediaRule { + query, + rules: parse_nested_at_rule(input, self.default_namespace, self.namespace_prefixes)?, + loc + }))) + }, + AtRulePrelude::Supports(condition) => { + Ok(DeclarationOrRule::Rule(CssRule::Supports(SupportsRule { + condition, + rules: parse_nested_at_rule(input, self.default_namespace, self.namespace_prefixes)?, + loc + }))) + }, + AtRulePrelude::Nest(selectors) => { + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes)?; + Ok(DeclarationOrRule::Rule(CssRule::Nesting(NestingRule { + style: StyleRule { + selectors, + declarations, + vendor_prefix: VendorPrefix::empty(), + rules, + loc + }, + loc + }))) + }, + _ => { + println!("{:?}", prelude); + unreachable!() + } + } + } +} + +#[inline] +fn parse_nested_at_rule<'a, 'i, 't>( + input: &mut Parser<'i, 't>, + default_namespace: &'a Option, + namespace_prefixes: &'a HashMap +) -> Result> { + let loc = input.current_source_location(); + + // Declarations can be immediately within @media and @supports blocks that are nested within a parent style rule. + // These act the same way as if they were nested within a `& { ... }` block. + let (declarations, mut rules) = parse_declarations_and_nested_rules(input, default_namespace, namespace_prefixes)?; + + if declarations.declarations.len() > 0 { + rules.0.insert(0, CssRule::Style(StyleRule { + selectors: SelectorList(smallvec::smallvec![parcel_selectors::parser::Selector::from_vec2(vec![parcel_selectors::parser::Component::Nesting])]), + declarations, + vendor_prefix: VendorPrefix::empty(), + rules: CssRuleList(vec![]), loc })) } + + Ok(rules) +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for StyleRuleParser<'a> { + type Prelude = SelectorList; + type QualifiedRule = DeclarationOrRule; + type Error = (); + + fn parse_prelude<'t>( + &mut self, + input: &mut Parser<'i, 't>, + ) -> Result> { + let selector_parser = SelectorParser { + default_namespace: self.default_namespace, + namespace_prefixes: self.namespace_prefixes, + is_nesting_allowed: true + }; + match SelectorList::parse(&selector_parser, input, NestingRequirement::Prefixed) { + Ok(x) => Ok(x), + Err(_) => Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) + } + } + + fn parse_block<'t>( + &mut self, + selectors: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result> { + let loc = start.source_location(); + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes)?; + Ok(DeclarationOrRule::Rule(CssRule::Style(StyleRule { + selectors, + vendor_prefix: VendorPrefix::empty(), + declarations, + rules, + loc + }))) + } } fn starts_with_ignore_ascii_case(string: &str, prefix: &str) -> bool { diff --git a/src/rules/document.rs b/src/rules/document.rs index 2dac9f89..1f56cec3 100644 --- a/src/rules/document.rs +++ b/src/rules/document.rs @@ -24,10 +24,8 @@ impl ToCss for MozDocumentRule { dest.whitespace()?; dest.write_char('{')?; dest.indent(); - for rule in self.rules.0.iter() { - dest.newline()?; - rule.to_css(dest)?; - } + dest.newline()?; + 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 a9504b5f..c349a491 100644 --- a/src/rules/media.rs +++ b/src/rules/media.rs @@ -5,6 +5,7 @@ use crate::printer::Printer; use super::CssRuleList; use crate::declaration::DeclarationHandler; use crate::targets::Browsers; +use crate::rules::{ToCssWithContext, StyleContext}; #[derive(Debug, PartialEq)] pub struct MediaRule { @@ -19,18 +20,16 @@ impl MediaRule { } } -impl ToCss for MediaRule { - fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { +impl ToCssWithContext for MediaRule { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { dest.add_mapping(self.loc); dest.write_str("@media ")?; self.query.to_css(dest)?; dest.whitespace()?; dest.write_char('{')?; dest.indent(); - for rule in self.rules.0.iter() { - dest.newline()?; - rule.to_css(dest)?; - } + dest.newline()?; + self.rules.to_css_with_context(dest, context)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/rules/mod.rs b/src/rules/mod.rs index da006d2c..9e77b204 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -8,6 +8,7 @@ pub mod import; pub mod media; pub mod style; pub mod document; +pub mod nesting; use media::MediaRule; use import::ImportRule; @@ -19,6 +20,7 @@ use supports::SupportsRule; use counter_style::CounterStyleRule; use namespace::NamespaceRule; use document::MozDocumentRule; +use nesting::NestingRule; use crate::traits::ToCss; use crate::printer::Printer; use crate::declaration::DeclarationHandler; @@ -28,6 +30,15 @@ use crate::targets::Browsers; use std::collections::HashMap; use crate::selector::{is_equivalent, get_prefix, get_necessary_prefixes}; +pub(crate) trait ToCssWithContext { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write; +} + +pub(crate) struct StyleContext<'a> { + pub rule: &'a StyleRule, + pub parent: Option<&'a StyleContext<'a>> +} + #[derive(Debug, PartialEq)] pub enum CssRule { Media(MediaRule), @@ -40,27 +51,35 @@ pub enum CssRule { CounterStyle(CounterStyleRule), Namespace(NamespaceRule), MozDocument(MozDocumentRule), + Nesting(NestingRule), Ignored } -impl ToCss for CssRule { - fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { +impl ToCssWithContext for CssRule { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { match self { - CssRule::Media(media) => media.to_css(dest), + CssRule::Media(media) => media.to_css_with_context(dest, context), CssRule::Import(import) => import.to_css(dest), - CssRule::Style(style) => style.to_css(dest), + CssRule::Style(style) => style.to_css_with_context(dest, context), CssRule::Keyframes(keyframes) => keyframes.to_css(dest), CssRule::FontFace(font_face) => font_face.to_css(dest), CssRule::Page(font_face) => font_face.to_css(dest), - CssRule::Supports(supports) => supports.to_css(dest), + CssRule::Supports(supports) => supports.to_css_with_context(dest, context), 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::Ignored => Ok(()) } } } +impl ToCss for CssRule { + fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { + self.to_css_with_context(dest, None) + } +} + #[derive(Debug, PartialEq)] pub struct CssRuleList(pub Vec); @@ -113,11 +132,11 @@ impl CssRuleList { if let Some(CssRule::Style(last_style_rule)) = rules.last_mut() { // Merge declarations if the selectors are equivalent, and both are compatible with all targets. - if style.selectors == last_style_rule.selectors && style.is_compatible(targets) && last_style_rule.is_compatible(targets) { + if style.selectors == last_style_rule.selectors && style.is_compatible(targets) && last_style_rule.is_compatible(targets) && style.rules.0.is_empty() && last_style_rule.rules.0.is_empty() { last_style_rule.declarations.declarations.extend(style.declarations.declarations.drain(..)); last_style_rule.declarations.minify(handler, important_handler); continue - } else if style.declarations == last_style_rule.declarations { + } else if style.declarations == last_style_rule.declarations && style.rules.0.is_empty() && last_style_rule.rules.0.is_empty() { // Append the selectors to the last rule if the declarations are the same, and all selectors are compatible. if style.is_compatible(targets) && last_style_rule.is_compatible(targets) { last_style_rule.selectors.0.extend(style.selectors.0.drain(..)); @@ -142,6 +161,7 @@ impl CssRuleList { } } }, + CssRule::Nesting(nesting) => nesting.minify(handler, important_handler), _ => {} } @@ -151,3 +171,31 @@ impl CssRuleList { self.0 = rules; } } + +impl ToCss for CssRuleList { + fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { + self.to_css_with_context(dest, None) + } +} + +impl ToCssWithContext for CssRuleList { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { + let mut first = true; + let mut last_without_block = false; + + for rule in &self.0 { + if first { + first = false; + } else { + if !dest.minify && !(last_without_block && matches!(rule, CssRule::Import(..) | CssRule::Namespace(..))) { + dest.write_char('\n')?; + } + dest.newline()?; + } + rule.to_css_with_context(dest, context)?; + last_without_block = matches!(rule, CssRule::Import(..) | CssRule::Namespace(..)); + } + + Ok(()) + } +} diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs new file mode 100644 index 00000000..61cfd7dd --- /dev/null +++ b/src/rules/nesting.rs @@ -0,0 +1,27 @@ +use cssparser::SourceLocation; +use crate::printer::Printer; +use crate::declaration::DeclarationHandler; +use super::style::StyleRule; +use crate::rules::{ToCssWithContext, StyleContext}; + +#[derive(Debug, PartialEq)] +pub struct NestingRule { + pub style: StyleRule, + pub loc: SourceLocation +} + +impl NestingRule { + pub(crate) fn minify(&mut self, handler: &mut DeclarationHandler, important_handler: &mut DeclarationHandler) { + self.style.minify(handler, important_handler) + } +} + +impl ToCssWithContext for NestingRule { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { + dest.add_mapping(self.loc); + if context.is_none() { + dest.write_str("@nest ")?; + } + self.style.to_css_with_context(dest, context) + } +} diff --git a/src/rules/style.rs b/src/rules/style.rs index a35f5ee4..97dfdb4c 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -6,12 +6,15 @@ use crate::printer::Printer; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::vendor_prefix::VendorPrefix; use crate::targets::Browsers; +use crate::rules::{CssRuleList, ToCssWithContext, StyleContext}; +use crate::compat::Feature; #[derive(Debug, PartialEq)] pub struct StyleRule { pub selectors: SelectorList, pub vendor_prefix: VendorPrefix, pub declarations: DeclarationBlock, + pub rules: CssRuleList, pub loc: SourceLocation } @@ -25,10 +28,10 @@ impl StyleRule { } } -impl ToCss for StyleRule { - fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { +impl ToCssWithContext for StyleRule { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { if self.vendor_prefix.is_empty() { - self.to_css_base(dest) + self.to_css_base(dest, context) } else { let mut first_rule = true; macro_rules! prefix { @@ -44,7 +47,7 @@ impl ToCss for StyleRule { dest.newline()?; } dest.vendor_prefix = VendorPrefix::$prefix; - self.to_css_base(dest)?; + self.to_css_base(dest, context)?; } }; } @@ -62,9 +65,54 @@ impl ToCss for StyleRule { } impl StyleRule { - fn to_css_base(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { - dest.add_mapping(self.loc); - self.selectors.to_css(dest)?; - self.declarations.to_css(dest) + fn to_css_base(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { + // If supported, or there are no targets, preserve nesting. Otherwise, write nested rules after parent. + if self.rules.0.is_empty() || (dest.targets.is_none() || Feature::CssNesting.is_compatible(dest.targets.unwrap())) { + dest.add_mapping(self.loc); + self.selectors.to_css_with_context(dest, context)?; + dest.whitespace()?; + dest.write_char('{')?; + dest.indent(); + let len = self.declarations.declarations.len(); + for (i, decl) in self.declarations.declarations.iter().enumerate() { + dest.newline()?; + decl.to_css(dest)?; + if i != len - 1 || !dest.minify { + dest.write_char(';')?; + } + } + + if !dest.minify && len > 0 && !self.rules.0.is_empty() { + dest.write_char('\n')?; + dest.newline()?; + } + + self.rules.to_css(dest)?; + + dest.dedent(); + dest.newline()?; + dest.write_char('}')?; + } else { + let has_declarations = self.declarations.declarations.len() > 0 || self.rules.0.is_empty(); + + // If there are any declarations in the rule, or no child rules, write the parent. + if has_declarations { + dest.add_mapping(self.loc); + self.selectors.to_css_with_context(dest, context)?; + self.declarations.to_css(dest)?; + if !dest.minify && !self.rules.0.is_empty() { + dest.write_char('\n')?; + dest.newline()?; + } + } + + // Write nested rules after the parent. + self.rules.to_css_with_context(dest, Some(&StyleContext { + rule: self, + parent: context + }))?; + } + + Ok(()) } } diff --git a/src/rules/supports.rs b/src/rules/supports.rs index 10adc4a5..d40f6f21 100644 --- a/src/rules/supports.rs +++ b/src/rules/supports.rs @@ -4,6 +4,7 @@ use crate::printer::Printer; use super::CssRuleList; use crate::declaration::DeclarationHandler; use crate::targets::Browsers; +use crate::rules::{ToCssWithContext, StyleContext}; #[derive(Debug, PartialEq)] pub struct SupportsRule { @@ -18,18 +19,16 @@ impl SupportsRule { } } -impl ToCss for SupportsRule { - fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { +impl ToCssWithContext for SupportsRule { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { dest.add_mapping(self.loc); dest.write_str("@supports ")?; self.condition.to_css(dest)?; dest.whitespace()?; dest.write_char('{')?; dest.indent(); - for rule in self.rules.0.iter() { - dest.newline()?; - rule.to_css(dest)?; - } + dest.newline()?; + self.rules.to_css_with_context(dest, context)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/selector.rs b/src/selector.rs index d659aa79..ff7dcc13 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -7,6 +7,8 @@ use crate::traits::ToCss; use crate::compat::Feature; use crate::vendor_prefix::VendorPrefix; use crate::targets::Browsers; +use crate::rules::{ToCssWithContext, StyleContext}; +use std::collections::HashMap; #[derive(Debug, Clone, PartialEq)] pub struct Selectors; @@ -32,7 +34,7 @@ impl SelectorString { } } -#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)] pub struct SelectorIdent(pub String); impl<'a> std::convert::From<&'a str> for SelectorIdent { @@ -62,8 +64,13 @@ impl SelectorImpl for Selectors { type ExtraMatchingData = (); } -pub struct SelectorParser; -impl<'i> parcel_selectors::parser::Parser<'i> for SelectorParser { +pub struct SelectorParser<'a> { + pub default_namespace: &'a Option, + pub namespace_prefixes: &'a HashMap, + pub is_nesting_allowed: bool +} + +impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a> { type Impl = Selectors; type Error = parcel_selectors::parser::SelectorParseErrorKind<'i>; @@ -211,6 +218,19 @@ impl<'i> parcel_selectors::parser::Parser<'i> for SelectorParser { fn parse_part(&self) -> bool { true } + + fn default_namespace(&self) -> Option { + self.default_namespace.clone().map(SelectorIdent) + } + + fn namespace_for_prefix(&self, prefix: &SelectorIdent) -> Option { + self.namespace_prefixes.get(&prefix.0).cloned().map(SelectorIdent) + } + + #[inline] + fn is_nesting_allowed(&self) -> bool { + self.is_nesting_allowed + } } /// https://drafts.csswg.org/selectors-4/#structural-pseudos @@ -293,8 +313,8 @@ impl parcel_selectors::parser::NonTSPseudoClass for PseudoClass { } impl cssparser::ToCss for PseudoClass { - fn to_css(&self, _: &mut W) -> fmt::Result where W: fmt::Write { - unreachable!(); + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + ToCss::to_css(self, &mut Printer::new(dest, None, false, None)) } } @@ -597,9 +617,9 @@ impl PseudoElement { } } -impl ToCss for SelectorList { - fn to_css(&self, dest: &mut Printer) -> fmt::Result where W: fmt::Write { - serialize_selector_list(self.0.iter(), dest) +impl ToCssWithContext for SelectorList { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> fmt::Result where W: fmt::Write { + serialize_selector_list(self.0.iter(), dest, context) } } @@ -619,8 +639,8 @@ impl ToCss for Combinator { } // Copied from the selectors crate and modified to override to_css implementation. -impl ToCss for parcel_selectors::parser::Selector { - fn to_css(&self, dest: &mut Printer) -> fmt::Result +impl ToCssWithContext for parcel_selectors::parser::Selector { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> fmt::Result where W: fmt::Write, { @@ -656,6 +676,9 @@ impl ToCss for parcel_selectors::parser::Selector { continue; } + let has_leading_nesting = matches!(compound[0], Component::Nesting); + let first_index = if has_leading_nesting { 1 } else { 0 }; + // 1. If there is only one simple selector in the compound selectors // which is a universal selector, append the result of // serializing the universal selector to s. @@ -666,12 +689,12 @@ impl ToCss for parcel_selectors::parser::Selector { // // If we are in this case, after we have serialized the universal // selector, we skip Step 2 and continue with the algorithm. - let (can_elide_namespace, first_non_namespace) = match compound[0] { - Component::ExplicitAnyNamespace | - Component::ExplicitNoNamespace | - Component::Namespace(..) => (false, 1), - Component::DefaultNamespace(..) => (true, 1), - _ => (true, 0), + let (can_elide_namespace, first_non_namespace) = match compound.get(first_index) { + Some(Component::ExplicitAnyNamespace) | + Some(Component::ExplicitNoNamespace) | + Some(Component::Namespace(..)) => (false, first_index + 1), + Some(Component::DefaultNamespace(..)) => (true, first_index + 1), + _ => (true, first_index), }; let mut perform_step_2 = true; let next_combinator = combinators.next(); @@ -689,9 +712,21 @@ impl ToCss for parcel_selectors::parser::Selector { (_, &Component::ExplicitUniversalType) => { // Iterate over everything so we serialize the namespace // too. - for simple in compound.iter() { - simple.to_css(dest)?; + let mut iter = compound.iter(); + let swap_nesting = has_leading_nesting && context.is_some() && is_type_selector(compound.get(first_index)); + if swap_nesting { + // Swap nesting and type selector (e.g. &div -> div&). + iter.next(); + } + + for simple in iter { + simple.to_css_with_context(dest, context)?; } + + if swap_nesting { + serialize_nesting(dest, context, false)?; + } + // Skip step 2, which is an "otherwise". perform_step_2 = false; }, @@ -709,7 +744,28 @@ impl ToCss for parcel_selectors::parser::Selector { // in cssom/serialize-namespaced-type-selectors.html, which the // following code tries to match. if perform_step_2 { - for simple in compound.iter() { + let mut iter = compound.iter(); + if has_leading_nesting && context.is_some() && is_type_selector(compound.get(first_index)) { + // Swap nesting and type selector (e.g. &div -> div&). + // 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)?; + + // Also check the next item in case of namespaces. + if is_type_selector(compound.get(first_index + 1)) { + let local = iter.next().unwrap(); + local.to_css_with_context(dest, context)?; + } + + nesting.to_css_with_context(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(); + serialize_nesting(dest, context, true)?; + } + + for simple in iter { if let Component::ExplicitUniversalType = *simple { // Can't have a namespace followed by a pseudo-element // selector followed by a universal selector in the same @@ -719,7 +775,7 @@ impl ToCss for parcel_selectors::parser::Selector { continue; } } - simple.to_css(dest)?; + simple.to_css_with_context(dest, context)?; } } @@ -744,8 +800,8 @@ impl ToCss for parcel_selectors::parser::Selector { } } -impl ToCss for Component { - fn to_css(&self, dest: &mut Printer) -> fmt::Result where W: fmt::Write { +impl ToCssWithContext for Component { + fn to_css_with_context(&self, dest: &mut Printer, context: Option<&StyleContext>) -> fmt::Result where W: fmt::Write { use Component::*; match &self { Combinator(ref c) => c.to_css(dest), @@ -797,7 +853,7 @@ impl ToCss for Component { Negation(..) => dest.write_str(":not(")?, _ => unreachable!(), } - serialize_selector_list(list.iter(), dest)?; + serialize_selector_list(list.iter(), dest, context)?; dest.write_str(")") }, NonTSPseudoClass(pseudo) => { @@ -806,6 +862,9 @@ impl ToCss for Component { PseudoElement(pseudo) => { pseudo.to_css(dest) }, + Nesting => { + serialize_nesting(dest, context, false) + }, _ => { cssparser::ToCss::to_css(self, dest) } @@ -813,7 +872,43 @@ impl ToCss for Component { } } -fn serialize_selector_list<'a, I, W>(iter: I, dest: &mut Printer) -> fmt::Result +fn serialize_nesting(dest: &mut Printer, context: Option<&StyleContext>, first: bool) -> fmt::Result where W: fmt::Write { + if let Some(ctx) = context { + // If there's only one selector, just serialize it directly. + // 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])) { + ctx.rule.selectors.0.first().unwrap().to_css_with_context(dest, ctx.parent) + } else { + dest.write_str(":is(")?; + serialize_selector_list(ctx.rule.selectors.0.iter(), dest, ctx.parent)?; + dest.write_char(')') + } + } else { + dest.write_char('&') + } +} + +fn has_type_selector(selector: &parcel_selectors::parser::Selector) -> bool { + let mut iter = selector.iter_raw_parse_order_from(0); + let first = iter.next(); + is_type_selector(first) +} + +fn is_type_selector(component: Option<&Component>) -> bool { + matches!( + component, + Some(Component::ExplicitAnyNamespace) | + Some(Component::ExplicitNoNamespace) | + Some(Component::Namespace(..)) | + Some(Component::DefaultNamespace(_)) | + Some(Component::LocalName(_)) | + Some(Component::ExplicitUniversalType) + ) +} + +fn serialize_selector_list<'a, I, W>(iter: I, dest: &mut Printer, context: Option<&StyleContext>) -> fmt::Result where I: Iterator>, W: fmt::Write, @@ -824,7 +919,7 @@ where dest.delim(',', false)?; } first = false; - selector.to_css(dest)?; + selector.to_css_with_context(dest, context)?; } Ok(()) } @@ -895,7 +990,7 @@ pub fn is_compatible(selectors: &SelectorList, targets: Option Feature::CssSel3, - Component::Is(_) => Feature::CssMatchesPseudo, + Component::Is(_) | Component::Nesting => Feature::CssMatchesPseudo, Component::Scope | Component::Host(_) | diff --git a/src/stylesheet.rs b/src/stylesheet.rs index e1fbdab3..4c049a79 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -8,16 +8,18 @@ use crate::targets::Browsers; use crate::declaration::{DeclarationHandler, DeclarationBlock}; use crate::traits::Parse; +pub use crate::parser::ParserOptions; + pub struct StyleSheet { pub filename: String, pub rules: CssRuleList } impl StyleSheet { - pub fn parse<'i>(filename: String, code: &'i str) -> Result> { + pub fn parse<'i>(filename: String, code: &'i str, options: ParserOptions) -> Result> { let mut input = ParserInput::new(&code); let mut parser = Parser::new(&mut input); - let rule_list_parser = RuleListParser::new_for_stylesheet(&mut parser, TopLevelRuleParser {}); + let rule_list_parser = RuleListParser::new_for_stylesheet(&mut parser, TopLevelRuleParser::new(options)); let mut rules = vec![]; for rule in rule_list_parser { @@ -53,18 +55,8 @@ impl StyleSheet { }; let mut printer = Printer::new(&mut dest, source_map.as_mut(), minify, targets); - let mut first = true; - - for rule in &self.rules.0 { - if first { - first = false; - } else { - printer.newline()?; - } - - rule.to_css(&mut printer)?; - printer.newline()?; - } + self.rules.to_css(&mut printer)?; + printer.newline()?; Ok((dest, source_map)) } diff --git a/test.js b/test.js index 02255315..adfe0ad3 100644 --- a/test.js +++ b/test.js @@ -30,7 +30,7 @@ if (process.argv[process.argv.length - 1] !== __filename) { let res = css.transform({ filename: __filename, - minify: true, + minify: false, targets: { safari: 4 << 16, firefox: 3 << 16 | 5 << 8, @@ -38,11 +38,25 @@ let res = css.transform({ }, code: Buffer.from(` -.foo + .bar:not(.baz) { - -webkit-box-shadow: 2px 0 0 blue; - box-shadow: 2px 0 0 red; - box-sizing: border-box; -} + .foo { + display: grid; + + & h1, & h2, &.bar { + color: red; + } + + @media (orientation: landscape) { + grid-auto-flow: column; + + & h1, & h2, &.bar { + color: red; + } + } + + @nest :not(&), .bar & { + color: blue; + } + } `)}); console.log(res.code.toString());