From e6775f70a8d6c47dd328b39ffe0514feb4153f8f Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sun, 19 Dec 2021 22:40:16 -0500 Subject: [PATCH 01/14] WIP: parse CSS nesting --- selectors/builder.rs | 3 + selectors/matching.rs | 1 + selectors/parser.rs | 126 +++++++++++++++++++------ src/parser.rs | 211 ++++++++++++++++++++++++++++++++++++++++-- src/rules/mod.rs | 5 + src/rules/nesting.rs | 28 ++++++ src/rules/style.rs | 28 +++++- src/selector.rs | 2 + test.js | 26 ++++-- 9 files changed, 386 insertions(+), 44 deletions(-) create mode 100644 src/rules/nesting.rs 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..26f5042e 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -110,6 +110,8 @@ bitflags! { /// Whether we explicitly disallow pseudo-element-like things. const DISALLOW_PSEUDOS = 1 << 6; + + const AFTER_NESTING = 1 << 7; } } @@ -167,6 +169,7 @@ pub enum SelectorParseErrorKind<'i> { InvalidPseudoElementAfterSlotted, InvalidPseudoElementInsideWhere, InvalidState, + MissingNestingSelector, UnexpectedTokenInAttributeSelector(Token<'i>), PseudoElementExpectedColon(Token<'i>), PseudoElementExpectedIdent(Token<'i>), @@ -343,6 +346,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 +361,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 +369,9 @@ impl SelectorList { Self::parse_with_state( parser, input, - SelectorParsingState::empty(), + &mut SelectorParsingState::empty(), ParseErrorRecovery::DiscardList, + nesting_requirement, ) } @@ -367,16 +379,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 +429,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 +767,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 +1109,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 +1585,7 @@ impl ToCss for Component { dest.write_str(")") }, NonTSPseudoClass(ref pseudo) => pseudo.to_css(dest), + Nesting => dest.write_char('&') } } } @@ -1600,12 +1644,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 +1664,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 +1720,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 +1737,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 +2136,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())) } @@ -2123,7 +2184,7 @@ where } 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 +2262,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 +2275,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 +2295,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 +2368,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 +2479,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('&') => { + *state |= SelectorParsingState::AFTER_NESTING; + SimpleSelectorParseResult::SimpleSelector(Component::Nesting) + }, _ => { input.reset(&start); return Ok(None); @@ -2725,7 +2795,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 +2819,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/parser.rs b/src/parser.rs index ae728ccc..1c17aabb 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,10 +15,11 @@ 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; /// The parser for the top-level rules in a stylesheet. @@ -26,7 +27,9 @@ pub struct TopLevelRuleParser {} impl<'b> TopLevelRuleParser { fn nested<'a: 'b>(&'a self) -> NestedRuleParser { - NestedRuleParser {} + NestedRuleParser { + nesting_requirement: NestingRequirement::None + } } } @@ -57,7 +60,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 { @@ -170,11 +175,15 @@ impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser { } #[derive(Clone)] -struct NestedRuleParser {} +struct NestedRuleParser { + nesting_requirement: NestingRequirement +} impl<'a, 'b> NestedRuleParser { fn parse_nested_rules(&mut self, input: &mut Parser) -> CssRuleList { - let nested_parser = NestedRuleParser {}; + let nested_parser = NestedRuleParser { + nesting_requirement: NestingRequirement::None + }; let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser); let mut rules = Vec::new(); @@ -381,7 +390,7 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser { input: &mut Parser<'i, 't>, ) -> Result> { let selector_parser = SelectorParser {}; - match SelectorList::parse(&selector_parser, input) { + match SelectorList::parse(&selector_parser, input, self.nesting_requirement) { Ok(x) => Ok(x), Err(_) => Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) } @@ -394,15 +403,199 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser { input: &mut Parser<'i, 't>, ) -> Result> { let loc = start.source_location(); + let (declarations, rules) = parse_declaration_list(input)?; 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_declaration_list<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(DeclarationBlock, CssRuleList), ParseError<'i, ()>> { + let mut declarations = vec![]; + let mut rules = vec![]; + loop { + let mut parser = DeclarationListParser::new(input, PropertyDeclarationParser); + let mut last = parser.input.state(); + while let Some(decl) = parser.next() { + println!("DECL {:?}", decl); + 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), + _ => { + parser.input.reset(&last); + break + } + } + + last = parser.input.state(); + } + + println!("BREAK {:?} {:?}", declarations, rules); + + let mut parser = NestedRuleParser { + nesting_requirement: NestingRequirement::Prefixed + }; + + let mut parsed_one = false; + loop { + println!("HERE"); + let state = input.state(); + let rule = parse_qualified_rule(input, &mut parser); + if let Ok(rule) = rule { + println!("RULE {:?}", rule); + rules.push(rule); + parsed_one = true; + } else { + println!("ERR {:?}", rule); + input.reset(&state); + break + } + } + + if !parsed_one { + break + } + } + + Ok((DeclarationBlock { declarations }, CssRuleList(rules))) +} + +pub struct PropertyDeclarationParser; + +/// Parse a declaration within {} block: `color: blue` +impl<'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser { + 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<'i> AtRuleParser<'i> for PropertyDeclarationParser { + type Prelude = AtRulePrelude; + type AtRule = DeclarationOrRule; + type Error = (); + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result> { + println!("{:?}", name); + match_ignore_ascii_case! { &*name, + "media" => { + let media = MediaList::parse(input); + Ok(AtRulePrelude::Media(media)) + }, + "nest" => { + let selector_parser = SelectorParser {}; + // TODO: require nesting selector to be contained in every selector + 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) => { + let (declarations, mut rules) = parse_declaration_list(input)?; + + println!("{:?}", declarations); + + 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: loc.clone() + })) + } + + Ok(DeclarationOrRule::Rule(CssRule::Media(MediaRule { + query, + rules, + loc + }))) + }, + AtRulePrelude::Nest(selectors) => { + let (declarations, rules) = parse_declaration_list(input)?; + Ok(DeclarationOrRule::Rule(CssRule::Nesting(NestingRule { + style: StyleRule { + selectors, + declarations, + vendor_prefix: VendorPrefix::empty(), + rules, + loc + }, + loc + }))) + }, + _ => { + println!("{:?}", prelude); + unreachable!() + } + } + } +} + fn starts_with_ignore_ascii_case(string: &str, prefix: &str) -> bool { string.len() >= prefix.len() && string.as_bytes()[0..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()) } + +// copied from cssparser +fn parse_qualified_rule<'i, 't, P, E>( + input: &mut Parser<'i, 't>, + parser: &mut P, +) -> Result<

>::QualifiedRule, ParseError<'i, E>> +where + P: QualifiedRuleParser<'i, Error = E>, +{ + let start = input.state(); + // FIXME: https://github.com/servo/rust-cssparser/issues/254 + let callback = |input: &mut Parser<'i, '_>| parser.parse_prelude(input); + let prelude = input.parse_until_before(Delimiter::CurlyBracketBlock, callback); + match *input.next()? { + Token::CurlyBracketBlock => { + // Do this here so that we consume the `{` even if the prelude is `Err`. + let prelude = prelude?; + // FIXME: https://github.com/servo/rust-cssparser/issues/254 + let callback = |input: &mut Parser<'i, '_>| parser.parse_block(prelude, &start, input); + input.parse_nested_block(callback) + } + _ => unreachable!(), + } +} diff --git a/src/rules/mod.rs b/src/rules/mod.rs index da006d2c..48c63184 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; @@ -40,6 +42,7 @@ pub enum CssRule { CounterStyle(CounterStyleRule), Namespace(NamespaceRule), MozDocument(MozDocumentRule), + Nesting(NestingRule), Ignored } @@ -56,6 +59,7 @@ impl ToCss for CssRule { 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(dest), CssRule::Ignored => Ok(()) } } @@ -142,6 +146,7 @@ impl CssRuleList { } } }, + CssRule::Nesting(nesting) => nesting.minify(handler, important_handler), _ => {} } diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs new file mode 100644 index 00000000..84298465 --- /dev/null +++ b/src/rules/nesting.rs @@ -0,0 +1,28 @@ +use cssparser::SourceLocation; +use crate::media_query::MediaList; +use crate::traits::ToCss; +use crate::printer::Printer; +use super::CssRuleList; +use crate::declaration::DeclarationHandler; +use crate::targets::Browsers; +use super::style::StyleRule; + +#[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 ToCss for NestingRule { + fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { + dest.add_mapping(self.loc); + dest.write_str("@nest ")?; + self.style.to_css(dest) + } +} diff --git a/src/rules/style.rs b/src/rules/style.rs index a35f5ee4..5434fa06 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -6,12 +6,14 @@ use crate::printer::Printer; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::vendor_prefix::VendorPrefix; use crate::targets::Browsers; +use crate::rules::CssRuleList; #[derive(Debug, PartialEq)] pub struct StyleRule { pub selectors: SelectorList, pub vendor_prefix: VendorPrefix, pub declarations: DeclarationBlock, + pub rules: CssRuleList, pub loc: SourceLocation } @@ -65,6 +67,30 @@ 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) + // self.declarations.to_css(dest) + 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 self.rules.0.len() > 0 { + dest.newline()?; + } + + for rule in &self.rules.0 { + dest.newline()?; + rule.to_css(dest)?; + } + + dest.dedent(); + dest.newline()?; + dest.write_char('}') } } diff --git a/src/selector.rs b/src/selector.rs index d659aa79..05db9cc8 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -985,6 +985,8 @@ pub fn is_compatible(selectors: &SelectorList, targets: Option continue } } + + Component::Nesting => return false, // TODO }; if let Some(targets) = targets { 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()); From 66626be29196dae4ef7d44955bd41c7a46a5a173 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 20 Dec 2021 23:26:59 -0500 Subject: [PATCH 02/14] Downlevel nesting --- src/lib.rs | 293 +++++++++++++++++++++++++++++++++++++++++++ src/rules/media.rs | 7 +- src/rules/mod.rs | 25 +++- src/rules/nesting.rs | 9 +- src/rules/style.rs | 65 ++++++---- src/selector.rs | 49 +++++--- 6 files changed, 392 insertions(+), 56 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7f0fd6a5..f5746e14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6783,4 +6783,297 @@ 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() { + test( + r#" + .foo { + color: blue; + & > .bar { color: red; } + } + "#, + indoc!{r#" + .foo { + color: #00f; + } + .foo > .bar { + color: red; + } + "#} + ); + + test( + r#" + .foo { + color: blue; + &.bar { color: red; } + } + "#, + indoc!{r#" + .foo { + color: #00f; + } + .foo.bar { + color: red; + } + "#} + ); + + 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; + } + "#} + ); + + test( + r#" + .foo { + color: blue; + & .bar & .baz & .qux { color: red; } + } + "#, + indoc!{r#" + .foo { + color: #00f; + } + .foo .bar .foo .baz .foo .qux { + color: red; + } + "#} + ); + + test( + r#" + .foo { + color: blue; + & { padding: 2ch; } + } + "#, + indoc!{r#" + .foo { + color: #00f; + } + .foo { + padding: 2ch; + } + "#} + ); + + test( + r#" + .foo { + color: blue; + && { padding: 2ch; } + } + "#, + indoc!{r#" + .foo { + color: #00f; + } + .foo.foo { + padding: 2ch; + } + "#} + ); + + test( + r#" + .error, .invalid { + &:hover > .baz { color: red; } + } + "#, + indoc!{r#" + :is(.error, .invalid):hover > .baz { + color: red; + } + "#} + ); + + test( + r#" + .foo { + &:is(.bar, &.baz) { color: red; } + } + "#, + indoc!{r#" + .foo:is(.bar, .foo.baz) { + color: red; + } + "#} + ); + + 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; + } + "#} + ); + + test( + r#" + .foo { + color: red; + @nest & > .bar { + color: blue; + } + } + "#, + indoc!{r#" + .foo { + color: red; + } + .foo > .bar { + color: #00f; + } + "#} + ); + + test( + r#" + .foo { + color: red; + @nest .parent & { + color: blue; + } + } + "#, + indoc!{r#" + .foo { + color: red; + } + .parent .foo { + color: #00f; + } + "#} + ); + + test( + r#" + .foo { + color: red; + @nest :not(&) { + color: blue; + } + } + "#, + indoc!{r#" + .foo { + color: red; + } + :not(.foo) { + color: #00f; + } + "#} + ); + + 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; + } + "#} + ); + + 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; + } + } + "#} + ); + + test( + r#" + .foo { + display: grid; + + @media (orientation: landscape) { + grid-auto-flow: column; + + @media (min-inline-size > 1024px) { + max-inline-size: 1024px; + } + } + } + "#, + indoc!{r#" + .foo { + display: grid; + } + @media (orientation: landscape) { + .foo { + grid-auto-flow: column; + } + @media (min-inline-size > 1024px) { + .foo { + max-inline-size: 1024px; + } + } + } + "#} + ); + } } diff --git a/src/rules/media.rs b/src/rules/media.rs index a9504b5f..0e0b64b3 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,8 +20,8 @@ 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)?; @@ -29,7 +30,7 @@ impl ToCss for MediaRule { dest.indent(); for rule in self.rules.0.iter() { dest.newline()?; - rule.to_css(dest)?; + rule.to_css_with_context(dest, context)?; } dest.dedent(); dest.newline()?; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 48c63184..9860f0a8 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -30,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), @@ -46,12 +55,12 @@ pub enum CssRule { 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), @@ -59,12 +68,18 @@ impl ToCss for CssRule { 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(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); diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs index 84298465..e77ff565 100644 --- a/src/rules/nesting.rs +++ b/src/rules/nesting.rs @@ -6,6 +6,7 @@ use super::CssRuleList; use crate::declaration::DeclarationHandler; use crate::targets::Browsers; use super::style::StyleRule; +use crate::rules::{ToCssWithContext, StyleContext}; #[derive(Debug, PartialEq)] pub struct NestingRule { @@ -19,10 +20,10 @@ impl NestingRule { } } -impl ToCss for NestingRule { - fn to_css(&self, dest: &mut Printer) -> std::fmt::Result where W: std::fmt::Write { +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); - dest.write_str("@nest ")?; - self.style.to_css(dest) + // 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 5434fa06..ae773b27 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -6,7 +6,7 @@ use crate::printer::Printer; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::vendor_prefix::VendorPrefix; use crate::targets::Browsers; -use crate::rules::CssRuleList; +use crate::rules::{CssRuleList, ToCssWithContext, StyleContext}; #[derive(Debug, PartialEq)] pub struct StyleRule { @@ -27,10 +27,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 { @@ -46,7 +46,7 @@ impl ToCss for StyleRule { dest.newline()?; } dest.vendor_prefix = VendorPrefix::$prefix; - self.to_css_base(dest)?; + self.to_css_base(dest, context)?; } }; } @@ -64,33 +64,44 @@ 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) - 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(';')?; - } + fn to_css_base(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { + let has_declarations = self.declarations.declarations.len() > 0 || self.rules.0.is_empty(); + if self.declarations.declarations.len() > 0 || self.rules.0.is_empty() { + dest.add_mapping(self.loc); + self.selectors.to_css_with_context(dest, context)?; + self.declarations.to_css(dest)?; } + // 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 self.rules.0.len() > 0 { - dest.newline()?; - } + // if self.rules.0.len() > 0 { + // dest.newline()?; + // } + let mut newline = has_declarations; for rule in &self.rules.0 { - dest.newline()?; - rule.to_css(dest)?; + if newline { + dest.newline()?; + } + rule.to_css_with_context(dest, Some(&StyleContext { + rule: self, + parent: context + }))?; + newline = true; } - dest.dedent(); - dest.newline()?; - dest.write_char('}') + // dest.dedent(); + // dest.newline()?; + // dest.write_char('}') + Ok(()) } } diff --git a/src/selector.rs b/src/selector.rs index 05db9cc8..b25811c3 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -7,6 +7,7 @@ use crate::traits::ToCss; use crate::compat::Feature; use crate::vendor_prefix::VendorPrefix; use crate::targets::Browsers; +use crate::rules::{ToCssWithContext, StyleContext}; #[derive(Debug, Clone, PartialEq)] pub struct Selectors; @@ -293,8 +294,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 +598,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 +620,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, { @@ -690,7 +691,7 @@ impl ToCss for parcel_selectors::parser::Selector { // Iterate over everything so we serialize the namespace // too. for simple in compound.iter() { - simple.to_css(dest)?; + simple.to_css_with_context(dest, context)?; } // Skip step 2, which is an "otherwise". perform_step_2 = false; @@ -719,7 +720,7 @@ impl ToCss for parcel_selectors::parser::Selector { continue; } } - simple.to_css(dest)?; + simple.to_css_with_context(dest, context)?; } } @@ -744,8 +745,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 +798,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 +807,22 @@ impl ToCss for Component { PseudoElement(pseudo) => { pseudo.to_css(dest) }, + Nesting => { + if let Some(ctx) = context { + // If there's only one selector, just serialize it directly. + // Otherwise, use an :is() pseudo class. + // TODO: this isn't always safe, e.g. &div or div& + if ctx.rule.selectors.0.len() == 1 { + 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('&') + } + }, _ => { cssparser::ToCss::to_css(self, dest) } @@ -813,7 +830,7 @@ impl ToCss for Component { } } -fn serialize_selector_list<'a, I, W>(iter: I, dest: &mut Printer) -> fmt::Result +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 +841,7 @@ where dest.delim(',', false)?; } first = false; - selector.to_css(dest)?; + selector.to_css_with_context(dest, context)?; } Ok(()) } @@ -895,7 +912,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(_) | @@ -985,8 +1002,6 @@ pub fn is_compatible(selectors: &SelectorList, targets: Option continue } } - - Component::Nesting => return false, // TODO }; if let Some(targets) = targets { From 6713319daaf585ee00c3b2acee72e0fe4432de8a Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 23 Dec 2021 23:48:01 -0500 Subject: [PATCH 03/14] Improve handling of type selectors --- selectors/parser.rs | 6 +++ src/lib.rs | 90 +++++++++++++++++++++++++++++++++++++++++++++ src/selector.rs | 77 +++++++++++++++++++++++++++----------- 3 files changed, 152 insertions(+), 21 deletions(-) diff --git a/selectors/parser.rs b/selectors/parser.rs index 26f5042e..b2c80486 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -2179,6 +2179,12 @@ where input.skip_whitespace(); let mut empty = true; + if 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; } diff --git a/src/lib.rs b/src/lib.rs index f5746e14..1ab4c348 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7075,5 +7075,95 @@ mod tests { } "#} ); + + test( + r#" + .foo { + &div { + color: red; + } + } + "#, + indoc!{r#" + div.foo { + color: red; + } + "#} + ); + + test( + r#" + .foo { + &article > figure { + color: red; + } + } + "#, + indoc!{r#" + article.foo > figure { + color: red; + } + "#} + ); + + test( + r#" + div { + @nest .foo& { + color: red; + } + } + "#, + indoc!{r#" + .foo:is(div) { + color: red; + } + "#} + ); + + test( + r#" + div { + &.bar { + background: green; + } + } + "#, + indoc!{r#" + div.bar { + background: green; + } + "#} + ); + + test( + r#" + div > .foo { + &span { + background: green; + } + } + "#, + indoc!{r#" + span:is(div > .foo) { + background: green; + } + "#} + ); + + test( + r#" + .foo { + & h1 { + background: green; + } + } + "#, + indoc!{r#" + .foo h1 { + background: green; + } + "#} + ); } } diff --git a/src/selector.rs b/src/selector.rs index b25811c3..1b252e05 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -657,6 +657,9 @@ impl ToCssWithContext 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. @@ -667,12 +670,12 @@ impl ToCssWithContext 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(); @@ -710,7 +713,21 @@ impl ToCssWithContext 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 && matches!(compound.get(first_non_namespace), Some(Component::LocalName(_))) { + // 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)?; + nesting.to_css_with_context(dest, context)?; + } else if has_leading_nesting { + // 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 @@ -808,20 +825,7 @@ impl ToCssWithContext for Component { pseudo.to_css(dest) }, Nesting => { - if let Some(ctx) = context { - // If there's only one selector, just serialize it directly. - // Otherwise, use an :is() pseudo class. - // TODO: this isn't always safe, e.g. &div or div& - if ctx.rule.selectors.0.len() == 1 { - 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('&') - } + serialize_nesting(dest, context, false) }, _ => { cssparser::ToCss::to_css(self, dest) @@ -830,6 +834,37 @@ impl ToCssWithContext for Component { } } +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(); + match first { + Some(Component::ExplicitAnyNamespace) | + Some(Component::ExplicitNoNamespace) | + Some(Component::Namespace(..)) | + Some(Component::DefaultNamespace(_)) | + Some(Component::LocalName(_)) => true, + _ => false + } +} + fn serialize_selector_list<'a, I, W>(iter: I, dest: &mut Printer, context: Option<&StyleContext>) -> fmt::Result where I: Iterator>, From 2aae135c0a1fa721a91297d657fe4667f3b89a4a Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 24 Dec 2021 13:10:09 -0500 Subject: [PATCH 04/14] Handle namespaced selectors --- src/lib.rs | 160 ++++++++++++++++++++++++++++++++++++++++++++++ src/parser.rs | 81 +++++++++++++++++------ src/selector.rs | 57 ++++++++++++++--- src/stylesheet.rs | 6 +- 4 files changed, 272 insertions(+), 32 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1ab4c348..f8c0b065 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4718,6 +4718,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] @@ -7078,16 +7158,50 @@ mod tests { 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; + } "#} ); @@ -7108,16 +7222,62 @@ mod tests { 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; + } "#} ); diff --git a/src/parser.rs b/src/parser.rs index 1c17aabb..ccbc23ce 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -21,14 +21,27 @@ use crate::rules::{ use crate::values::ident::CustomIdent; use crate::declaration::{DeclarationBlock, Declaration}; use crate::vendor_prefix::VendorPrefix; +use std::collections::HashMap; /// The parser for the top-level rules in a stylesheet. -pub struct TopLevelRuleParser {} +pub struct TopLevelRuleParser { + default_namespace: Option, + namespace_prefixes: HashMap +} impl<'b> TopLevelRuleParser { - fn nested<'a: 'b>(&'a self) -> NestedRuleParser { + pub fn new() -> TopLevelRuleParser { + TopLevelRuleParser { + default_namespace: None, + namespace_prefixes: HashMap::new() + } + } + + fn nested<'a: 'b>(&'a mut self) -> NestedRuleParser { NestedRuleParser { - nesting_requirement: NestingRequirement::None + nesting_requirement: NestingRequirement::None, + default_namespace: &mut self.default_namespace, + namespace_prefixes: &mut self.namespace_prefixes } } } @@ -135,6 +148,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, @@ -175,14 +194,18 @@ impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser { } #[derive(Clone)] -struct NestedRuleParser { - nesting_requirement: NestingRequirement +struct NestedRuleParser<'a> { + nesting_requirement: NestingRequirement, + default_namespace: &'a Option, + namespace_prefixes: &'a HashMap } -impl<'a, 'b> NestedRuleParser { +impl<'a, 'b> NestedRuleParser<'a> { fn parse_nested_rules(&mut self, input: &mut Parser) -> CssRuleList { let nested_parser = NestedRuleParser { - nesting_requirement: NestingRequirement::None + nesting_requirement: NestingRequirement::None, + default_namespace: self.default_namespace, + namespace_prefixes: self.namespace_prefixes }; let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser); @@ -201,7 +224,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 = (); @@ -380,7 +403,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 = (); @@ -389,7 +412,10 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser { &mut self, input: &mut Parser<'i, 't>, ) -> Result> { - let selector_parser = SelectorParser {}; + let selector_parser = SelectorParser { + default_namespace: self.default_namespace, + namespace_prefixes: self.namespace_prefixes + }; match SelectorList::parse(&selector_parser, input, self.nesting_requirement) { Ok(x) => Ok(x), Err(_) => Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) @@ -403,7 +429,7 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser { input: &mut Parser<'i, 't>, ) -> Result> { let loc = start.source_location(); - let (declarations, rules) = parse_declaration_list(input)?; + let (declarations, rules) = parse_declaration_list(input, self.default_namespace, self.namespace_prefixes)?; Ok(CssRule::Style(StyleRule { selectors, vendor_prefix: VendorPrefix::empty(), @@ -420,11 +446,18 @@ pub enum DeclarationOrRule { Rule(CssRule) } -fn parse_declaration_list<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(DeclarationBlock, CssRuleList), ParseError<'i, ()>> { +fn parse_declaration_list<'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![]; loop { - let mut parser = DeclarationListParser::new(input, PropertyDeclarationParser); + let mut parser = DeclarationListParser::new(input, PropertyDeclarationParser { + default_namespace, + namespace_prefixes + }); let mut last = parser.input.state(); while let Some(decl) = parser.next() { println!("DECL {:?}", decl); @@ -450,7 +483,9 @@ fn parse_declaration_list<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(Declara println!("BREAK {:?} {:?}", declarations, rules); let mut parser = NestedRuleParser { - nesting_requirement: NestingRequirement::Prefixed + nesting_requirement: NestingRequirement::Prefixed, + default_namespace, + namespace_prefixes }; let mut parsed_one = false; @@ -477,10 +512,13 @@ fn parse_declaration_list<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(Declara Ok((DeclarationBlock { declarations }, CssRuleList(rules))) } -pub struct PropertyDeclarationParser; +pub struct PropertyDeclarationParser<'a> { + default_namespace: &'a Option, + namespace_prefixes: &'a HashMap +} /// Parse a declaration within {} block: `color: blue` -impl<'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser { +impl<'a, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a> { type Declaration = DeclarationOrRule; type Error = (); @@ -493,7 +531,7 @@ impl<'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser { } } -impl<'i> AtRuleParser<'i> for PropertyDeclarationParser { +impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> { type Prelude = AtRulePrelude; type AtRule = DeclarationOrRule; type Error = (); @@ -510,7 +548,10 @@ impl<'i> AtRuleParser<'i> for PropertyDeclarationParser { Ok(AtRulePrelude::Media(media)) }, "nest" => { - let selector_parser = SelectorParser {}; + let selector_parser = SelectorParser { + default_namespace: self.default_namespace, + namespace_prefixes: self.namespace_prefixes + }; // TODO: require nesting selector to be contained in every selector let selectors = match SelectorList::parse(&selector_parser, input, NestingRequirement::Contained) { Ok(x) => x, @@ -531,7 +572,7 @@ impl<'i> AtRuleParser<'i> for PropertyDeclarationParser { let loc = start.source_location(); match prelude { AtRulePrelude::Media(query) => { - let (declarations, mut rules) = parse_declaration_list(input)?; + let (declarations, mut rules) = parse_declaration_list(input, self.default_namespace, self.namespace_prefixes)?; println!("{:?}", declarations); @@ -552,7 +593,7 @@ impl<'i> AtRuleParser<'i> for PropertyDeclarationParser { }))) }, AtRulePrelude::Nest(selectors) => { - let (declarations, rules) = parse_declaration_list(input)?; + let (declarations, rules) = parse_declaration_list(input, self.default_namespace, self.namespace_prefixes)?; Ok(DeclarationOrRule::Rule(CssRule::Nesting(NestingRule { style: StyleRule { selectors, diff --git a/src/selector.rs b/src/selector.rs index 1b252e05..63820a03 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -8,6 +8,7 @@ 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; @@ -33,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 { @@ -63,8 +64,12 @@ 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 +} + +impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a> { type Impl = Selectors; type Error = parcel_selectors::parser::SelectorParseErrorKind<'i>; @@ -212,6 +217,14 @@ 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) + } } /// https://drafts.csswg.org/selectors-4/#structural-pseudos @@ -693,9 +706,21 @@ impl ToCssWithContext for parcel_selectors::parser::Selector { (_, &Component::ExplicitUniversalType) => { // Iterate over everything so we serialize the namespace // too. - for simple in compound.iter() { + let mut iter = compound.iter(); + let swap_nesting = has_leading_nesting && 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; }, @@ -714,12 +739,19 @@ impl ToCssWithContext for parcel_selectors::parser::Selector { // following code tries to match. if perform_step_2 { let mut iter = compound.iter(); - if has_leading_nesting && matches!(compound.get(first_non_namespace), Some(Component::LocalName(_))) { + if has_leading_nesting && 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 { // Nesting selector may serialize differently if it is leading, due to type selectors. @@ -855,14 +887,19 @@ fn serialize_nesting(dest: &mut Printer, context: Option<&StyleContext>, f fn has_type_selector(selector: &parcel_selectors::parser::Selector) -> bool { let mut iter = selector.iter_raw_parse_order_from(0); let first = iter.next(); - match first { - Some(Component::ExplicitAnyNamespace) | + 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(_)) => true, - _ => false - } + Some(Component::LocalName(_)) | + Some(Component::ExplicitUniversalType) + ) } fn serialize_selector_list<'a, I, W>(iter: I, dest: &mut Printer, context: Option<&StyleContext>) -> fmt::Result diff --git a/src/stylesheet.rs b/src/stylesheet.rs index e1fbdab3..4eea022b 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -17,7 +17,7 @@ impl StyleSheet { pub fn parse<'i>(filename: String, code: &'i str) -> 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()); let mut rules = vec![]; for rule in rule_list_parser { @@ -54,16 +54,18 @@ impl StyleSheet { let mut printer = Printer::new(&mut dest, source_map.as_mut(), minify, targets); let mut first = true; + let mut last_without_block = false; for rule in &self.rules.0 { if first { first = false; - } else { + } else if !(last_without_block && matches!(rule, CssRule::Import(..) | CssRule::Namespace(..))) { printer.newline()?; } rule.to_css(&mut printer)?; printer.newline()?; + last_without_block = matches!(rule, CssRule::Import(..) | CssRule::Namespace(..)); } Ok((dest, source_map)) From d0f2924e1abc62b555ff5a728f9777ba45fa2e78 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 24 Dec 2021 14:06:22 -0500 Subject: [PATCH 05/14] Simplify parser --- src/lib.rs | 44 +++++++++++++++ src/parser.rs | 147 +++++++++++++++++++++++--------------------------- 2 files changed, 112 insertions(+), 79 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f8c0b065..028c793e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7325,5 +7325,49 @@ mod tests { } "#} ); + + test( + r#" + .foo { + @nest :not(&) { + color: red; + } + + & h1 { + background: green; + } + } + "#, + indoc!{r#" + :not(.foo) { + color: red; + } + .foo h1 { + background: green; + } + "#} + ); + + test( + r#" + .foo { + & h1 { + background: green; + } + + @nest :not(&) { + color: red; + } + } + "#, + indoc!{r#" + .foo h1 { + background: green; + } + :not(.foo) { + color: red; + } + "#} + ); } } diff --git a/src/parser.rs b/src/parser.rs index ccbc23ce..7327a313 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -39,7 +39,6 @@ impl<'b> TopLevelRuleParser { fn nested<'a: 'b>(&'a mut self) -> NestedRuleParser { NestedRuleParser { - nesting_requirement: NestingRequirement::None, default_namespace: &mut self.default_namespace, namespace_prefixes: &mut self.namespace_prefixes } @@ -195,7 +194,6 @@ impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser { #[derive(Clone)] struct NestedRuleParser<'a> { - nesting_requirement: NestingRequirement, default_namespace: &'a Option, namespace_prefixes: &'a HashMap } @@ -203,7 +201,6 @@ struct NestedRuleParser<'a> { impl<'a, 'b> NestedRuleParser<'a> { fn parse_nested_rules(&mut self, input: &mut Parser) -> CssRuleList { let nested_parser = NestedRuleParser { - nesting_requirement: NestingRequirement::None, default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes }; @@ -416,7 +413,7 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a> { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes }; - match SelectorList::parse(&selector_parser, input, self.nesting_requirement) { + match SelectorList::parse(&selector_parser, input, NestingRequirement::None) { Ok(x) => Ok(x), Err(_) => Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) } @@ -453,59 +450,42 @@ fn parse_declaration_list<'a, 'i, 't>( ) -> Result<(DeclarationBlock, CssRuleList), ParseError<'i, ()>> { let mut declarations = vec![]; let mut rules = vec![]; - loop { - let mut parser = DeclarationListParser::new(input, PropertyDeclarationParser { - default_namespace, - namespace_prefixes - }); - let mut last = parser.input.state(); - while let Some(decl) = parser.next() { - println!("DECL {:?}", decl); - 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); + let parser = PropertyDeclarationParser { + 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)) } - Ok(DeclarationOrRule::Rule(rule)) => rules.push(rule), - _ => { - parser.input.reset(&last); - break - } - } - - last = parser.input.state(); - } - - println!("BREAK {:?} {:?}", declarations, rules); - let mut parser = NestedRuleParser { - nesting_requirement: NestingRequirement::Prefixed, - default_namespace, - namespace_prefixes - }; - - let mut parsed_one = false; - loop { - println!("HERE"); - let state = input.state(); - let rule = parse_qualified_rule(input, &mut parser); - if let Ok(rule) = rule { - println!("RULE {:?}", rule); - rules.push(rule); - parsed_one = true; - } else { - println!("ERR {:?}", rule); - input.reset(&state); + declarations.push(decl); + } + Ok(DeclarationOrRule::Rule(rule)) => rules.push(rule), + _ => { + declaration_parser.input.reset(&last); break } } - if !parsed_one { - 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 + }, } } @@ -541,7 +521,6 @@ impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> { name: CowRcStr<'i>, input: &mut Parser<'i, 't>, ) -> Result> { - println!("{:?}", name); match_ignore_ascii_case! { &*name, "media" => { let media = MediaList::parse(input); @@ -552,7 +531,6 @@ impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> { default_namespace: self.default_namespace, namespace_prefixes: self.namespace_prefixes }; - // TODO: require nesting selector to be contained in every selector let selectors = match SelectorList::parse(&selector_parser, input, NestingRequirement::Contained) { Ok(x) => x, Err(_) => return Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) @@ -574,8 +552,6 @@ impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> { AtRulePrelude::Media(query) => { let (declarations, mut rules) = parse_declaration_list(input, self.default_namespace, self.namespace_prefixes)?; - println!("{:?}", declarations); - 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])]), @@ -613,30 +589,43 @@ impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> { } } -fn starts_with_ignore_ascii_case(string: &str, prefix: &str) -> bool { - string.len() >= prefix.len() && string.as_bytes()[0..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()) -} +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for PropertyDeclarationParser<'a> { + type Prelude = SelectorList; + type QualifiedRule = DeclarationOrRule; + type Error = (); -// copied from cssparser -fn parse_qualified_rule<'i, 't, P, E>( - input: &mut Parser<'i, 't>, - parser: &mut P, -) -> Result<

>::QualifiedRule, ParseError<'i, E>> -where - P: QualifiedRuleParser<'i, Error = E>, -{ - let start = input.state(); - // FIXME: https://github.com/servo/rust-cssparser/issues/254 - let callback = |input: &mut Parser<'i, '_>| parser.parse_prelude(input); - let prelude = input.parse_until_before(Delimiter::CurlyBracketBlock, callback); - match *input.next()? { - Token::CurlyBracketBlock => { - // Do this here so that we consume the `{` even if the prelude is `Err`. - let prelude = prelude?; - // FIXME: https://github.com/servo/rust-cssparser/issues/254 - let callback = |input: &mut Parser<'i, '_>| parser.parse_block(prelude, &start, input); - input.parse_nested_block(callback) - } - _ => unreachable!(), + 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 + }; + 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_declaration_list(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 { + string.len() >= prefix.len() && string.as_bytes()[0..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()) +} From 61289d11aa6fcfb3532d16ca521f43f15354c2e9 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 24 Dec 2021 14:10:44 -0500 Subject: [PATCH 06/14] Clarify names --- src/parser.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 7327a313..e3ca259d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -426,7 +426,7 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a> { input: &mut Parser<'i, 't>, ) -> Result> { let loc = start.source_location(); - let (declarations, rules) = parse_declaration_list(input, self.default_namespace, self.namespace_prefixes)?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes)?; Ok(CssRule::Style(StyleRule { selectors, vendor_prefix: VendorPrefix::empty(), @@ -443,14 +443,14 @@ pub enum DeclarationOrRule { Rule(CssRule) } -fn parse_declaration_list<'a, 'i, 't>( +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 = PropertyDeclarationParser { + let parser = StyleRuleParser { default_namespace, namespace_prefixes }; @@ -492,13 +492,13 @@ fn parse_declaration_list<'a, 'i, 't>( Ok((DeclarationBlock { declarations }, CssRuleList(rules))) } -pub struct PropertyDeclarationParser<'a> { +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 PropertyDeclarationParser<'a> { +impl<'a, 'i> cssparser::DeclarationParser<'i> for StyleRuleParser<'a> { type Declaration = DeclarationOrRule; type Error = (); @@ -511,7 +511,7 @@ impl<'a, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a> } } -impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> { +impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a> { type Prelude = AtRulePrelude; type AtRule = DeclarationOrRule; type Error = (); @@ -550,7 +550,9 @@ impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> { let loc = start.source_location(); match prelude { AtRulePrelude::Media(query) => { - let (declarations, mut rules) = parse_declaration_list(input, self.default_namespace, self.namespace_prefixes)?; + // Declarations can be immediately within @media 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, self.default_namespace, self.namespace_prefixes)?; if declarations.declarations.len() > 0 { rules.0.insert(0, CssRule::Style(StyleRule { @@ -569,7 +571,7 @@ impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> { }))) }, AtRulePrelude::Nest(selectors) => { - let (declarations, rules) = parse_declaration_list(input, self.default_namespace, self.namespace_prefixes)?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes)?; Ok(DeclarationOrRule::Rule(CssRule::Nesting(NestingRule { style: StyleRule { selectors, @@ -589,7 +591,7 @@ impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> { } } -impl<'a, 'b, 'i> QualifiedRuleParser<'i> for PropertyDeclarationParser<'a> { +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for StyleRuleParser<'a> { type Prelude = SelectorList; type QualifiedRule = DeclarationOrRule; type Error = (); @@ -615,7 +617,7 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for PropertyDeclarationParser<'a> { input: &mut Parser<'i, 't>, ) -> Result> { let loc = start.source_location(); - let (declarations, rules) = parse_declaration_list(input, self.default_namespace, self.namespace_prefixes)?; + 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(), From 8c7c793a903c7d362f752616c8a86061181b39b5 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 25 Dec 2021 00:38:49 -0500 Subject: [PATCH 07/14] Move behind a flag --- node/src/lib.rs | 18 +++++++++++++++--- selectors/parser.rs | 8 ++++++-- src/lib.rs | 7 ++++--- src/parser.rs | 37 +++++++++++++++++++++++++++---------- src/selector.rs | 8 +++++++- src/stylesheet.rs | 6 ++++-- 6 files changed, 63 insertions(+), 21 deletions(-) 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/selectors/parser.rs b/selectors/parser.rs index b2c80486..f31a47a9 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -328,6 +328,10 @@ pub trait Parser<'i> { ) -> Option<::NamespaceUrl> { None } + + fn is_nesting_allowed(&self) -> bool { + false + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -2179,7 +2183,7 @@ where input.skip_whitespace(); let mut empty = true; - if input.try_parse(|input| input.expect_delim('&')).is_ok() { + 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; @@ -2490,7 +2494,7 @@ where SimpleSelectorParseResult::SimpleSelector(pseudo_class) } }, - Token::Delim('&') => { + Token::Delim('&') if parser.is_nesting_allowed() => { *state |= SelectorParsingState::AFTER_NESTING; SimpleSelectorParseResult::SimpleSelector(Component::Nesting) }, diff --git a/src/lib.rs b/src/lib.rs index 028c793e..3b34e31b 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 { nesting: true }).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); diff --git a/src/parser.rs b/src/parser.rs index e3ca259d..160025dc 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -23,24 +23,32 @@ 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 { default_namespace: Option, - namespace_prefixes: HashMap + namespace_prefixes: HashMap, + options: ParserOptions } impl<'b> TopLevelRuleParser { - pub fn new() -> TopLevelRuleParser { + pub fn new(options: ParserOptions) -> TopLevelRuleParser { TopLevelRuleParser { default_namespace: None, - namespace_prefixes: HashMap::new() + 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 + namespace_prefixes: &mut self.namespace_prefixes, + options: &self.options } } } @@ -195,14 +203,16 @@ impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser { #[derive(Clone)] struct NestedRuleParser<'a> { default_namespace: &'a Option, - namespace_prefixes: &'a HashMap + namespace_prefixes: &'a HashMap, + options: &'a ParserOptions } impl<'a, 'b> NestedRuleParser<'a> { fn parse_nested_rules(&mut self, input: &mut Parser) -> CssRuleList { let nested_parser = NestedRuleParser { default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes + namespace_prefixes: self.namespace_prefixes, + options: self.options }; let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser); @@ -411,7 +421,8 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a> { ) -> Result> { let selector_parser = SelectorParser { default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes + namespace_prefixes: self.namespace_prefixes, + is_nesting_allowed: false }; match SelectorList::parse(&selector_parser, input, NestingRequirement::None) { Ok(x) => Ok(x), @@ -426,7 +437,11 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a> { input: &mut Parser<'i, 't>, ) -> Result> { let loc = start.source_location(); - let (declarations, rules) = parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes)?; + let (declarations, rules) = 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(), @@ -529,7 +544,8 @@ impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a> { "nest" => { let selector_parser = SelectorParser { default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes + namespace_prefixes: self.namespace_prefixes, + is_nesting_allowed: true }; let selectors = match SelectorList::parse(&selector_parser, input, NestingRequirement::Contained) { Ok(x) => x, @@ -602,7 +618,8 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for StyleRuleParser<'a> { ) -> Result> { let selector_parser = SelectorParser { default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes + namespace_prefixes: self.namespace_prefixes, + is_nesting_allowed: true }; match SelectorList::parse(&selector_parser, input, NestingRequirement::Prefixed) { Ok(x) => Ok(x), diff --git a/src/selector.rs b/src/selector.rs index 63820a03..fc1777fa 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -66,7 +66,8 @@ impl SelectorImpl for Selectors { pub struct SelectorParser<'a> { pub default_namespace: &'a Option, - pub namespace_prefixes: &'a HashMap + pub namespace_prefixes: &'a HashMap, + pub is_nesting_allowed: bool } impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a> { @@ -225,6 +226,11 @@ impl<'a, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a> { 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 diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 4eea022b..97e0d368 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::new()); + let rule_list_parser = RuleListParser::new_for_stylesheet(&mut parser, TopLevelRuleParser::new(options)); let mut rules = vec![]; for rule in rule_list_parser { From 5c8501d0726d6f7fd82dacbfd178fb1b68f24447 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 25 Dec 2021 11:20:13 -0500 Subject: [PATCH 08/14] Handle nested @supports rules --- src/lib.rs | 22 ++++++++++++++++++ src/parser.rs | 52 ++++++++++++++++++++++++++++++------------- src/rules/mod.rs | 6 ++--- src/rules/nesting.rs | 4 ---- src/rules/supports.rs | 7 +++--- 5 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3b34e31b..0d4544a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7157,6 +7157,28 @@ mod tests { "#} ); + 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; + } + } + "#} + ); + test( r#" @namespace "http://example.com/foo"; diff --git a/src/parser.rs b/src/parser.rs index 160025dc..f77e446c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -541,6 +541,10 @@ impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a> { 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, @@ -566,23 +570,16 @@ impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a> { let loc = start.source_location(); match prelude { AtRulePrelude::Media(query) => { - // Declarations can be immediately within @media 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, self.default_namespace, self.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: loc.clone() - })) - } - Ok(DeclarationOrRule::Rule(CssRule::Media(MediaRule { query, - rules, + 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 }))) }, @@ -607,6 +604,31 @@ impl<'a, 'i> AtRuleParser<'i> for StyleRuleParser<'a> { } } +#[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; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 9860f0a8..34c0004f 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -64,7 +64,7 @@ impl ToCssWithContext for CssRule { 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), @@ -132,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(..)); diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs index e77ff565..51cf68af 100644 --- a/src/rules/nesting.rs +++ b/src/rules/nesting.rs @@ -1,10 +1,6 @@ use cssparser::SourceLocation; -use crate::media_query::MediaList; -use crate::traits::ToCss; use crate::printer::Printer; -use super::CssRuleList; use crate::declaration::DeclarationHandler; -use crate::targets::Browsers; use super::style::StyleRule; use crate::rules::{ToCssWithContext, StyleContext}; diff --git a/src/rules/supports.rs b/src/rules/supports.rs index 10adc4a5..6a596795 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,8 +19,8 @@ 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)?; @@ -28,7 +29,7 @@ impl ToCss for SupportsRule { dest.indent(); for rule in self.rules.0.iter() { dest.newline()?; - rule.to_css(dest)?; + rule.to_css_with_context(dest, context)?; } dest.dedent(); dest.newline()?; From 9793a7e23df740516a7576261a53e0c787e48764 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 25 Dec 2021 11:59:39 -0500 Subject: [PATCH 09/14] Cleanup --- selectors/parser.rs | 1 + src/lib.rs | 57 +++++++++++++++++++++++++-------------------- src/rules/style.rs | 23 ++++-------------- 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/selectors/parser.rs b/selectors/parser.rs index f31a47a9..2ec0323d 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -111,6 +111,7 @@ 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; } } diff --git a/src/lib.rs b/src/lib.rs index 0d4544a5..246a8865 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ mod tests { use indoc::indoc; fn test(source: &str, expected: &str) { - let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { nesting: true }).unwrap(); + let mut stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap(); stylesheet.minify(None); let (res, _) = stylesheet.to_css(false, false, None).unwrap(); assert_eq!(res, expected); @@ -49,6 +49,13 @@ mod tests { assert_eq!(res, expected); } + fn nesting_test(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#" @@ -6867,7 +6874,7 @@ mod tests { #[test] fn test_nesting() { - test( + nesting_test( r#" .foo { color: blue; @@ -6884,7 +6891,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { color: blue; @@ -6901,7 +6908,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo, .bar { color: blue; @@ -6918,7 +6925,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { color: blue; @@ -6935,7 +6942,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { color: blue; @@ -6952,7 +6959,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { color: blue; @@ -6969,7 +6976,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .error, .invalid { &:hover > .baz { color: red; } @@ -6982,7 +6989,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { &:is(.bar, &.baz) { color: red; } @@ -6995,7 +7002,7 @@ mod tests { "#} ); - test( + nesting_test( r#" figure { margin: 0; @@ -7022,7 +7029,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { color: red; @@ -7041,7 +7048,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { color: red; @@ -7060,7 +7067,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { color: red; @@ -7079,7 +7086,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { color: blue; @@ -7104,7 +7111,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { display: grid; @@ -7126,7 +7133,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { display: grid; @@ -7157,7 +7164,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { display: grid; @@ -7179,7 +7186,7 @@ mod tests { "#} ); - test( + nesting_test( r#" @namespace "http://example.com/foo"; @namespace toto "http://toto.example.org"; @@ -7228,7 +7235,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { &article > figure { @@ -7243,7 +7250,7 @@ mod tests { "#} ); - test( + nesting_test( r#" @namespace "http://example.com/foo"; @namespace toto "http://toto.example.org"; @@ -7304,7 +7311,7 @@ mod tests { "#} ); - test( + nesting_test( r#" div { &.bar { @@ -7319,7 +7326,7 @@ mod tests { "#} ); - test( + nesting_test( r#" div > .foo { &span { @@ -7334,7 +7341,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { & h1 { @@ -7349,7 +7356,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { @nest :not(&) { @@ -7371,7 +7378,7 @@ mod tests { "#} ); - test( + nesting_test( r#" .foo { & h1 { diff --git a/src/rules/style.rs b/src/rules/style.rs index ae773b27..2b7014bb 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -66,27 +66,15 @@ impl ToCssWithContext for StyleRule { impl StyleRule { fn to_css_base(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { 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 self.declarations.declarations.len() > 0 || self.rules.0.is_empty() { dest.add_mapping(self.loc); self.selectors.to_css_with_context(dest, context)?; self.declarations.to_css(dest)?; } - // 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 self.rules.0.len() > 0 { - // dest.newline()?; - // } + // Write nested rules after the parent. let mut newline = has_declarations; for rule in &self.rules.0 { if newline { @@ -98,10 +86,7 @@ impl StyleRule { }))?; newline = true; } - - // dest.dedent(); - // dest.newline()?; - // dest.write_char('}') + Ok(()) } } From 806ad0129379cec742bcb3b68672d9f82e227794 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 25 Dec 2021 12:27:41 -0500 Subject: [PATCH 10/14] Add nesting to playground --- node/index.d.ts | 9 ++++++++- playground/index.html | 16 ++++++++++++++-- playground/playground.js | 9 +++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) 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/playground/index.html b/playground/index.html index 3616b860..2ad19069 100644 --- a/playground/index.html +++ b/playground/index.html @@ -6,7 +6,8 @@ html, body { margin: 0; height: 100%; - box-sizing: border-box + box-sizing: border-box; + font-family: -apple-system, system-ui; } body { @@ -39,13 +40,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 +69,6 @@

Parcel CSS Playground

-
diff --git a/playground/playground.js b/playground/playground.js index f15593d7..f25971cb 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -11,22 +11,10 @@ function loadPlaygroundState() { reflectPlaygroundState(playgroundState); } catch { const initialPlaygroundState = { - minify: true, - nesting: 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); From 26224f74d4f1d2ca75e0b44d29e04ec6e3b0aa16 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 25 Dec 2021 18:40:28 -0500 Subject: [PATCH 12/14] Dark mode --- playground/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/playground/index.html b/playground/index.html index c98b8b45..e10bf9be 100644 --- a/playground/index.html +++ b/playground/index.html @@ -8,6 +8,7 @@ height: 100%; box-sizing: border-box; font-family: -apple-system, system-ui; + color-scheme: dark light; } body { From 6d57db96f424fb8550f9c5527570ff03b98a9c79 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 25 Dec 2021 23:15:50 -0500 Subject: [PATCH 13/14] Preserve nesting if supported or no targets --- build-prefixes.js | 3 +- playground/playground.js | 12 ++------ src/compat.rs | 4 ++- src/lib.rs | 62 ++++++++++++++++++++++++++++++++++++++-- src/rules/nesting.rs | 4 ++- src/rules/style.rs | 61 +++++++++++++++++++++++++++++---------- src/selector.rs | 6 ++-- 7 files changed, 120 insertions(+), 32 deletions(-) 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/playground/playground.js b/playground/playground.js index f25971cb..14265c23 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -32,15 +32,9 @@ function reflectPlaygroundState(playgroundState) { 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; } } 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 246a8865..80f8c6a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,17 @@ mod tests { } 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(); @@ -7141,7 +7152,7 @@ mod tests { @media (orientation: landscape) { grid-auto-flow: column; - @media (min-inline-size > 1024px) { + @media (width > 1024px) { max-inline-size: 1024px; } } @@ -7155,7 +7166,7 @@ mod tests { .foo { grid-auto-flow: column; } - @media (min-inline-size > 1024px) { + @media (min-width: 1024px) { .foo { max-inline-size: 1024px; } @@ -7399,5 +7410,52 @@ mod tests { } "#} ); + + 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; + } + } + "#, + indoc!{r#" + .foo { + color: #00f; + + &div { + color: red; + } + } + "#} + ); } } diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs index 51cf68af..61cfd7dd 100644 --- a/src/rules/nesting.rs +++ b/src/rules/nesting.rs @@ -19,7 +19,9 @@ impl NestingRule { 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); - // dest.write_str("@nest ")?; + 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 2b7014bb..bdbd4165 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -7,6 +7,7 @@ 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 { @@ -65,26 +66,56 @@ impl ToCssWithContext for StyleRule { impl StyleRule { fn to_css_base(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { - 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 self.declarations.declarations.len() > 0 || self.rules.0.is_empty() { + // 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)?; - self.declarations.to_css(dest)?; - } + 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 self.rules.0.len() > 0 && !dest.minify { + dest.write_char('\n')?; + } - // Write nested rules after the parent. - let mut newline = has_declarations; - for rule in &self.rules.0 { - if newline { + for rule in &self.rules.0 { dest.newline()?; + rule.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)?; + } + + // Write nested rules after the parent. + let mut newline = has_declarations; + for rule in &self.rules.0 { + if newline { + dest.newline()?; + } + rule.to_css_with_context(dest, Some(&StyleContext { + rule: self, + parent: context + }))?; + newline = true; } - rule.to_css_with_context(dest, Some(&StyleContext { - rule: self, - parent: context - }))?; - newline = true; } Ok(()) diff --git a/src/selector.rs b/src/selector.rs index fc1777fa..ff7dcc13 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -713,7 +713,7 @@ impl ToCssWithContext for parcel_selectors::parser::Selector { // Iterate over everything so we serialize the namespace // too. let mut iter = compound.iter(); - let swap_nesting = has_leading_nesting && is_type_selector(compound.get(first_index)); + 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(); @@ -745,7 +745,7 @@ impl ToCssWithContext for parcel_selectors::parser::Selector { // following code tries to match. if perform_step_2 { let mut iter = compound.iter(); - if has_leading_nesting && is_type_selector(compound.get(first_index)) { + 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(); @@ -759,7 +759,7 @@ impl ToCssWithContext for parcel_selectors::parser::Selector { } nesting.to_css_with_context(dest, context)?; - } else if has_leading_nesting { + } 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)?; From cee10c11432a0d34937fdb33d3aa6eba6988e6c2 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 25 Dec 2021 23:46:53 -0500 Subject: [PATCH 14/14] Prettier printing --- src/lib.rs | 31 +++++++++++++++++++++++++++++++ src/rules/document.rs | 6 ++---- src/rules/media.rs | 6 ++---- src/rules/mod.rs | 28 ++++++++++++++++++++++++++++ src/rules/style.rs | 29 ++++++++++++----------------- src/rules/supports.rs | 6 ++---- src/stylesheet.rs | 16 ++-------------- 7 files changed, 79 insertions(+), 43 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 80f8c6a4..ca779f5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6896,6 +6896,7 @@ mod tests { .foo { color: #00f; } + .foo > .bar { color: red; } @@ -6913,6 +6914,7 @@ mod tests { .foo { color: #00f; } + .foo.bar { color: red; } @@ -6930,6 +6932,7 @@ mod tests { .foo, .bar { color: #00f; } + :is(.foo, .bar) + .baz, :is(.foo, .bar).qux { color: red; } @@ -6947,6 +6950,7 @@ mod tests { .foo { color: #00f; } + .foo .bar .foo .baz .foo .qux { color: red; } @@ -6964,6 +6968,7 @@ mod tests { .foo { color: #00f; } + .foo { padding: 2ch; } @@ -6981,6 +6986,7 @@ mod tests { .foo { color: #00f; } + .foo.foo { padding: 2ch; } @@ -7031,9 +7037,11 @@ mod tests { figure { margin: 0; } + figure > figcaption { background: #00000080; } + figure > figcaption > p { font-size: .9rem; } @@ -7053,6 +7061,7 @@ mod tests { .foo { color: red; } + .foo > .bar { color: #00f; } @@ -7072,6 +7081,7 @@ mod tests { .foo { color: red; } + .parent .foo { color: #00f; } @@ -7091,6 +7101,7 @@ mod tests { .foo { color: red; } + :not(.foo) { color: #00f; } @@ -7113,9 +7124,11 @@ mod tests { .foo { color: #00f; } + .bar .foo { color: red; } + .bar .foo.baz { color: green; } @@ -7136,6 +7149,7 @@ mod tests { .foo { display: grid; } + @media (orientation: landscape) { .foo { grid-auto-flow: column; @@ -7162,10 +7176,12 @@ mod tests { .foo { display: grid; } + @media (orientation: landscape) { .foo { grid-auto-flow: column; } + @media (min-width: 1024px) { .foo { max-inline-size: 1024px; @@ -7189,6 +7205,7 @@ mod tests { .foo { display: grid; } + @supports (foo: bar) { .foo { grid-auto-flow: column; @@ -7231,15 +7248,19 @@ mod tests { div.foo { color: red; } + *.foo { color: red; } + |x.foo { color: red; } + *|x.foo { color: red; } + toto|x.foo { color: red; } @@ -7383,6 +7404,7 @@ mod tests { :not(.foo) { color: red; } + .foo h1 { background: green; } @@ -7405,6 +7427,7 @@ mod tests { .foo h1 { background: green; } + :not(.foo) { color: red; } @@ -7445,6 +7468,10 @@ mod tests { &div { color: red; } + + &span { + color: purple; + } } "#, indoc!{r#" @@ -7454,6 +7481,10 @@ mod tests { &div { color: red; } + + &span { + color: purple; + } } "#} ); 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 0e0b64b3..c349a491 100644 --- a/src/rules/media.rs +++ b/src/rules/media.rs @@ -28,10 +28,8 @@ impl ToCssWithContext for MediaRule { dest.whitespace()?; dest.write_char('{')?; dest.indent(); - for rule in self.rules.0.iter() { - dest.newline()?; - rule.to_css_with_context(dest, context)?; - } + 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 34c0004f..9e77b204 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -171,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/style.rs b/src/rules/style.rs index bdbd4165..97dfdb4c 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -67,7 +67,7 @@ impl ToCssWithContext for StyleRule { impl StyleRule { fn to_css_base(&self, dest: &mut Printer, context: Option<&StyleContext>) -> std::fmt::Result where W: std::fmt::Write { // If supported, or there are no targets, preserve nesting. Otherwise, write nested rules after parent. - if !self.rules.0.is_empty() && (dest.targets.is_none() || Feature::CssNesting.is_compatible(dest.targets.unwrap())) { + 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()?; @@ -82,15 +82,13 @@ impl StyleRule { } } - if self.rules.0.len() > 0 && !dest.minify { + if !dest.minify && len > 0 && !self.rules.0.is_empty() { dest.write_char('\n')?; - } - - for rule in &self.rules.0 { dest.newline()?; - rule.to_css(dest)?; } + self.rules.to_css(dest)?; + dest.dedent(); dest.newline()?; dest.write_char('}')?; @@ -102,20 +100,17 @@ impl StyleRule { dest.add_mapping(self.loc); self.selectors.to_css_with_context(dest, context)?; self.declarations.to_css(dest)?; - } - - // Write nested rules after the parent. - let mut newline = has_declarations; - for rule in &self.rules.0 { - if newline { + if !dest.minify && !self.rules.0.is_empty() { + dest.write_char('\n')?; dest.newline()?; } - rule.to_css_with_context(dest, Some(&StyleContext { - rule: self, - parent: context - }))?; - newline = true; } + + // 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 6a596795..d40f6f21 100644 --- a/src/rules/supports.rs +++ b/src/rules/supports.rs @@ -27,10 +27,8 @@ impl ToCssWithContext for SupportsRule { dest.whitespace()?; dest.write_char('{')?; dest.indent(); - for rule in self.rules.0.iter() { - dest.newline()?; - rule.to_css_with_context(dest, context)?; - } + dest.newline()?; + self.rules.to_css_with_context(dest, context)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 97e0d368..4c049a79 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -55,20 +55,8 @@ impl StyleSheet { }; let mut printer = Printer::new(&mut dest, source_map.as_mut(), minify, targets); - let mut first = true; - let mut last_without_block = false; - - for rule in &self.rules.0 { - if first { - first = false; - } else if !(last_without_block && matches!(rule, CssRule::Import(..) | CssRule::Namespace(..))) { - printer.newline()?; - } - - rule.to_css(&mut printer)?; - printer.newline()?; - last_without_block = matches!(rule, CssRule::Import(..) | CssRule::Namespace(..)); - } + self.rules.to_css(&mut printer)?; + printer.newline()?; Ok((dest, source_map)) }