Skip to content

Commit

Permalink
Support @scope rule (#586)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Sep 17, 2023
1 parent e674bc5 commit 6f71ef5
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 7 deletions.
1 change: 1 addition & 0 deletions node/src/transformer.rs
Expand Up @@ -281,6 +281,7 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor {
CssRule::LayerStatement(..) => "layer-statement",
CssRule::Property(..) => "property",
CssRule::Container(..) => "container",
CssRule::Scope(..) => "scope",
CssRule::MozDocument(..) => "moz-document",
CssRule::Nesting(..) => "nesting",
CssRule::Viewport(..) => "viewport",
Expand Down
14 changes: 11 additions & 3 deletions selectors/parser.rs
Expand Up @@ -405,6 +405,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> {
pub fn parse<'t, P>(
parser: &P,
input: &mut CssParser<'i, 't>,
error_recovery: ParseErrorRecovery,
nesting_requirement: NestingRequirement,
) -> Result<Self, ParseError<'i, P::Error>>
where
Expand All @@ -414,7 +415,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> {
parser,
input,
&mut SelectorParsingState::empty(),
ParseErrorRecovery::DiscardList,
error_recovery,
nesting_requirement,
)
}
Expand Down Expand Up @@ -466,6 +467,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> {
pub fn parse_relative<'t, P>(
parser: &P,
input: &mut CssParser<'i, 't>,
error_recovery: ParseErrorRecovery,
nesting_requirement: NestingRequirement,
) -> Result<Self, ParseError<'i, P::Error>>
where
Expand All @@ -475,7 +477,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> {
parser,
input,
&mut SelectorParsingState::empty(),
ParseErrorRecovery::DiscardList,
error_recovery,
nesting_requirement,
)
}
Expand Down Expand Up @@ -3265,7 +3267,12 @@ pub mod tests {
expected: Option<&'a str>,
) -> Result<SelectorList<'i, DummySelectorImpl>, SelectorParseError<'i>> {
let mut parser_input = ParserInput::new(input);
let result = SelectorList::parse(parser, &mut CssParser::new(&mut parser_input), NestingRequirement::None);
let result = SelectorList::parse(
parser,
&mut CssParser::new(&mut parser_input),
ParseErrorRecovery::DiscardList,
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
Expand All @@ -3292,6 +3299,7 @@ pub mod tests {
let list = SelectorList::parse(
&DummyParser::default(),
&mut CssParser::new(&mut input),
ParseErrorRecovery::DiscardList,
NestingRequirement::None,
);
assert!(list.is_ok());
Expand Down
116 changes: 116 additions & 0 deletions src/lib.rs
Expand Up @@ -24485,6 +24485,122 @@ mod tests {
);
}

#[test]
fn test_at_scope() {
minify_test(
r#"
@scope {
.foo {
display: flex;
}
}
"#,
"@scope{.foo{display:flex}}",
);
minify_test(
r#"
@scope {
:scope {
display: flex;
color: lightblue;
}
}"#,
"@scope{:scope{color:#add8e6;display:flex}}",
);
minify_test(
r#"
@scope (.light-scheme) {
a { color: yellow; }
}
"#,
"@scope(.light-scheme){a{color:#ff0}}",
);
minify_test(
r#"
@scope (.media-object) to (.content > *) {
a { color: yellow; }
}
"#,
"@scope(.media-object) to (.content>*){a{color:#ff0}}",
);
minify_test(
r#"
@scope to (.content > *) {
a { color: yellow; }
}
"#,
"@scope to (.content>*){a{color:#ff0}}",
);
minify_test(
r#"
@scope (#my-component) {
& { color: yellow; }
}
"#,
"@scope(#my-component){&{color:#ff0}}",
);
minify_test(
r#"
@scope (.parent-scope) {
@scope (:scope > .child-scope) to (:scope .limit) {
.content { color: yellow; }
}
}
"#,
"@scope(.parent-scope){@scope(:scope>.child-scope) to (:scope .limit){.content{color:#ff0}}}",
);
minify_test(
r#"
.foo {
@scope (.bar) {
color: yellow;
}
}
"#,
".foo{@scope(.bar){&{color:#ff0}}}",
);
nesting_test(
r#"
.foo {
@scope (.bar) {
color: yellow;
}
}
"#,
indoc! {r#"
@scope (.bar) {
:scope {
color: #ff0;
}
}
"#},
);
nesting_test(
r#"
.parent {
color: blue;

@scope (& > .scope) to (& .limit) {
& .content {
color: yellow;
}
}
}
"#,
indoc! {r#"
.parent {
color: #00f;
}

@scope (.parent > .scope) to (.parent > .scope .limit) {
:scope .content {
color: #ff0;
}
}
"#},
);
}

#[test]
fn test_custom_media() {
custom_media_test(
Expand Down
60 changes: 56 additions & 4 deletions src/parser.rs
Expand Up @@ -7,8 +7,10 @@ use crate::rules::container::{ContainerCondition, ContainerName, ContainerRule};
use crate::rules::font_palette_values::FontPaletteValuesRule;
use crate::rules::layer::{LayerBlockRule, LayerStatementRule};
use crate::rules::property::PropertyRule;
use crate::rules::scope::ScopeRule;
use crate::rules::starting_style::StartingStyleRule;
use crate::rules::viewport::ViewportRule;

use crate::rules::{
counter_style::CounterStyleRule,
custom_media::CustomMediaRule,
Expand All @@ -35,7 +37,7 @@ use crate::vendor_prefix::VendorPrefix;
use crate::visitor::{Visit, VisitTypes, Visitor};
use bitflags::bitflags;
use cssparser::*;
use parcel_selectors::parser::NestingRequirement;
use parcel_selectors::parser::{NestingRequirement, ParseErrorRecovery};
use std::sync::{Arc, RwLock};

bitflags! {
Expand Down Expand Up @@ -202,6 +204,8 @@ pub enum AtRulePrelude<'i, T> {
Container(Option<ContainerName<'i>>, ContainerCondition<'i>),
/// A @starting-style prelude.
StartingStyle,
/// A @scope rule prelude.
Scope(Option<SelectorList<'i>>, Option<SelectorList<'i>>),
/// An unknown prelude.
Unknown(CowArcStr<'i>, TokenList<'i>),
/// A custom prelude.
Expand All @@ -221,6 +225,7 @@ impl<'i, T> AtRulePrelude<'i, T> {
| Self::MozDocument
| Self::Layer(..)
| Self::StartingStyle
| Self::Scope(..)
| Self::Nest(..)
| Self::Unknown(..)
| Self::Custom(..) => true,
Expand Down Expand Up @@ -629,13 +634,40 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne
"starting-style" => {
AtRulePrelude::StartingStyle
},
"scope" => {
let selector_parser = SelectorParser {
is_nesting_allowed: true,
options: &self.options,
};

let scope_start = if input.try_parse(|input| input.expect_parenthesis_block()).is_ok() {
Some(input.parse_nested_block(|input| {
// https://drafts.csswg.org/css-cascade-6/#scoped-rules
// TODO: disallow pseudo elements?
SelectorList::parse_relative(&selector_parser, input, ParseErrorRecovery::IgnoreInvalidSelector, NestingRequirement::None)
})?)
} else {
None
};

let scope_end = if input.try_parse(|input| input.expect_ident_matching("to")).is_ok() {
input.expect_parenthesis_block()?;
Some(input.parse_nested_block(|input| {
SelectorList::parse_relative(&selector_parser, input, ParseErrorRecovery::IgnoreInvalidSelector, NestingRequirement::None)
})?)
} else {
None
};

AtRulePrelude::Scope(scope_start, scope_end)
},
"nest" if self.is_in_style_rule => {
self.options.warn(input.new_custom_error(ParserError::DeprecatedNestRule));
let selector_parser = SelectorParser {
is_nesting_allowed: true,
options: &self.options,
};
let selectors = SelectorList::parse(&selector_parser, input, NestingRequirement::Contained)?;
let selectors = SelectorList::parse(&selector_parser, input, ParseErrorRecovery::DiscardList, NestingRequirement::Contained)?;
AtRulePrelude::Nest(selectors)
},
_ => parse_custom_at_rule_prelude(&name, input, self.options, self.at_rule_parser)?
Expand Down Expand Up @@ -717,6 +749,16 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne
}));
Ok(())
}
AtRulePrelude::Scope(scope_start, scope_end) => {
let rules = self.parse_style_block(input)?;
self.rules.0.push(CssRule::Scope(ScopeRule {
scope_start,
scope_end,
rules,
loc,
}));
Ok(())
}
AtRulePrelude::Viewport(vendor_prefix) => {
self.rules.0.push(CssRule::Viewport(ViewportRule {
vendor_prefix,
Expand Down Expand Up @@ -871,9 +913,19 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i>
options: &self.options,
};
if self.is_in_style_rule {
SelectorList::parse_relative(&selector_parser, input, NestingRequirement::Implicit)
SelectorList::parse_relative(
&selector_parser,
input,
ParseErrorRecovery::DiscardList,
NestingRequirement::Implicit,
)
} else {
SelectorList::parse(&selector_parser, input, NestingRequirement::None)
SelectorList::parse(
&selector_parser,
input,
ParseErrorRecovery::DiscardList,
NestingRequirement::None,
)
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/printer.rs
Expand Up @@ -353,6 +353,16 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> {
res
}

pub(crate) fn with_cleared_context<T, U, F: FnOnce(&mut Printer<'a, 'b, 'c, W>) -> Result<T, U>>(
&mut self,
f: F,
) -> Result<T, U> {
let parent = std::mem::take(&mut self.context);
let res = f(self);
self.context = parent;
res
}

pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'b>> {
self.context.clone()
}
Expand Down
10 changes: 10 additions & 0 deletions src/rules/mod.rs
Expand Up @@ -50,6 +50,7 @@ pub mod namespace;
pub mod nesting;
pub mod page;
pub mod property;
pub mod scope;
pub mod starting_style;
pub mod style;
pub mod supports;
Expand Down Expand Up @@ -88,6 +89,7 @@ use media::MediaRule;
use namespace::NamespaceRule;
use nesting::NestingRule;
use page::PageRule;
use scope::ScopeRule;
use smallvec::{smallvec, SmallVec};
use starting_style::StartingStyleRule;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -166,6 +168,8 @@ pub enum CssRule<'i, R = DefaultAtRule> {
Property(PropertyRule<'i>),
/// A `@container` rule.
Container(ContainerRule<'i, R>),
/// A `@scope` rule.
Scope(ScopeRule<'i, R>),
/// A `@starting-style` rule.
StartingStyle(StartingStyleRule<'i, R>),
/// A placeholder for a rule that was removed.
Expand Down Expand Up @@ -304,6 +308,10 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul
let rule = ContainerRule::deserialize(deserializer)?;
Ok(CssRule::Container(rule))
}
"scope" => {
let rule = ScopeRule::deserialize(deserializer)?;
Ok(CssRule::Scope(rule))
}
"starting-style" => {
let rule = StartingStyleRule::deserialize(deserializer)?;
Ok(CssRule::StartingStyle(rule))
Expand Down Expand Up @@ -347,6 +355,7 @@ impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> {
CssRule::Property(property) => property.to_css(dest),
CssRule::StartingStyle(rule) => rule.to_css(dest),
CssRule::Container(container) => container.to_css(dest),
CssRule::Scope(scope) => scope.to_css(dest),
CssRule::Unknown(unknown) => unknown.to_css(dest),
CssRule::Custom(rule) => rule.to_css(dest).map_err(|_| PrinterError {
kind: PrinterErrorKind::FmtError,
Expand Down Expand Up @@ -758,6 +767,7 @@ impl<'i, T: Clone> CssRuleList<'i, T> {
continue;
}
}
CssRule::Scope(scope) => scope.minify(context)?,
CssRule::Nesting(nesting) => {
if nesting.minify(context, parent_is_unused)? {
continue;
Expand Down

0 comments on commit 6f71ef5

Please sign in to comment.