Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 58 additions & 11 deletions src/css/css_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,7 @@ impl<'a, AtRuleParserT: CustomAtRuleParser> TopLevelRuleParser<'a, AtRuleParserT
important_declarations: DeclarationList::new_in(bump),
rules: &mut *self.rules,
is_in_style_rule: false,
is_in_scope_rule: false,
allow_declarations: false,
composes_state: ComposesState::DisallowEntirely,
composes: &mut *self.composes,
Expand Down Expand Up @@ -1023,6 +1024,10 @@ pub struct NestedRuleParser<'a, T: CustomAtRuleParser> {
// todo_stuff.think_mem_mgmt
pub(crate) rules: &'a mut CssRuleList<T::AtRule>,
pub(crate) is_in_style_rule: bool,
/// The nearest enclosing style rule or `@scope` rule is an `@scope`. Its
/// style rules are scoped rules: they take relative selectors and get no
/// implicit `&` prefix, because `&` inside `@scope` is the scoping root.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) is_in_scope_rule: bool,
pub(crate) allow_declarations: bool,

pub(crate) composes_state: ComposesState,
Expand Down Expand Up @@ -1400,6 +1405,7 @@ mod rule_parsers {
&mut self,
input: &mut Parser,
is_style_rule: bool,
is_scope_rule: bool,
) -> CssResult<(DeclarationBlock<'static>, CssRuleList<T::AtRule>)> {
// TODO: think about memory management in error cases
let mut rules = CssRuleList::<T::AtRule>::default();
Expand All @@ -1425,9 +1431,13 @@ mod rule_parsers {
important_declarations: DeclarationList::new_in(bump),
rules: &mut rules,
is_in_style_rule: self.is_in_style_rule || is_style_rule,
is_in_scope_rule: is_scope_rule || (self.is_in_scope_rule && !is_style_rule),
// Declarations directly in `@scope` apply to the scoping root.
// https://github.com/w3c/csswg-drafts/issues/10389
Comment thread
robobun marked this conversation as resolved.
Resolved
allow_declarations: self.allow_declarations
|| self.is_in_style_rule
|| is_style_rule,
|| is_style_rule
|| is_scope_rule,
composes_state,
composes: &mut *self.composes,
composes_refs: &mut *self.composes_refs,
Expand Down Expand Up @@ -1481,6 +1491,23 @@ mod rule_parsers {
pub(crate) fn parse_style_block(
&mut self,
input: &mut Parser,
) -> CssResult<CssRuleList<T::AtRule>> {
self.parse_style_block_impl(input, false)
}

/// Parses the block of an `@scope` rule. Its style rules are scoped
/// rules, and its declarations apply to the scoping root.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) fn parse_scope_block(
&mut self,
input: &mut Parser,
) -> CssResult<CssRuleList<T::AtRule>> {
self.parse_style_block_impl(input, true)
}

fn parse_style_block_impl(
&mut self,
input: &mut Parser,
is_scope_rule: bool,
) -> CssResult<CssRuleList<T::AtRule>> {
let srcloc = input.current_source_location();
let loc = Location {
Expand All @@ -1490,9 +1517,9 @@ mod rule_parsers {
};

// 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) = self.parse_nested(input, false)?;
// that are nested within a parent style rule, and within @scope.
// These act the same way as if they were nested within a `& { ... }` block.
Comment thread
robobun marked this conversation as resolved.
Resolved
let (declarations, mut rules) = self.parse_nested(input, false, is_scope_rule)?;

if declarations.len() > 0 {
rules.v.insert(
Comment thread
robobun marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -1605,13 +1632,24 @@ mod rule_parsers {
is_nesting_allowed: true,
options: this.options,
};
// Inside a style rule, <scope-start> is relative to the
// parent rule, like a nested style rule's selector.
// https://drafts.csswg.org/css-cascade-6/#scope-nesting
let scope_start_nesting = if this.is_in_style_rule && !this.is_in_scope_rule {
selector_parser::NestingRequirement::Implicit
} else {
selector_parser::NestingRequirement::None
};
Comment thread
robobun marked this conversation as resolved.
Resolved
// Both selector lists are unforgiving: one invalid
// selector invalidates the whole rule.
// https://github.com/w3c/csswg-drafts/issues/10042
Comment thread
robobun marked this conversation as resolved.
Resolved
let scope_start = if input.try_parse(|p| p.expect_parenthesis_block()).is_ok() {
Some(input.parse_nested_block(|input2| {
SelectorList::parse_relative(
&mut selector_parser,
input2,
selector_parser::ParseErrorRecovery::IgnoreInvalidSelector,
selector_parser::NestingRequirement::None,
selector_parser::ParseErrorRecovery::DiscardList,
scope_start_nesting,
)
})?)
} else {
Expand All @@ -1623,7 +1661,7 @@ mod rule_parsers {
SelectorList::parse_relative(
&mut selector_parser,
input2,
selector_parser::ParseErrorRecovery::IgnoreInvalidSelector,
selector_parser::ParseErrorRecovery::DiscardList,
selector_parser::NestingRequirement::None,
)
})?)
Expand Down Expand Up @@ -1740,7 +1778,7 @@ mod rule_parsers {
scope_start,
scope_end,
} => {
let rules = this.parse_style_block(input)?;
let rules = this.parse_scope_block(input)?;
this.rules
.v
.push(CssRule::Scope(css_rules::scope::ScopeRule {
Expand Down Expand Up @@ -1858,7 +1896,7 @@ mod rule_parsers {
Ok(())
}
AtRulePrelude::Nest(selectors) => {
let (declarations, rules) = this.parse_nested(input, true)?;
let (declarations, rules) = this.parse_nested(input, true, false)?;
this.rules
.v
.push(CssRule::Nesting(css_rules::nesting::NestingRule {
Expand Down Expand Up @@ -1951,7 +1989,16 @@ mod rule_parsers {
is_nesting_allowed: true,
options: this.options,
};
if this.is_in_style_rule {
if this.is_in_scope_rule {
// Scoped style rules take a <relative-selector-list>.
// https://drafts.csswg.org/css-cascade-6/#scoped-rules
Comment thread
robobun marked this conversation as resolved.
Resolved
SelectorList::parse_relative(
&mut selector_parser,
input,
selector_parser::ParseErrorRecovery::DiscardList,
selector_parser::NestingRequirement::Scoped,
)
} else if this.is_in_style_rule {
SelectorList::parse_relative(
&mut selector_parser,
input,
Expand Down Expand Up @@ -2034,7 +2081,7 @@ mod rule_parsers {
}
}
let location = input.position();
let (declarations, rules) = this.parse_nested(input, true)?;
let (declarations, rules) = this.parse_nested(input, true, false)?;

// We parsed a style rule with the `composes` property. Track which
// properties it used so we can validate it later.
Expand Down
6 changes: 6 additions & 0 deletions src/css/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ pub struct Printer<'a> {
/// `serialize::serialize_nesting` so deeply nested rules with multiple
/// `&` references per level cannot expand exponentially.
pub(crate) nesting_expansions: u32,
/// True while the rules nested in an `@scope` block are serialized with no
/// `ctx`. There `&` means the scoping root, which compiles to
/// `:where(:scope)`, and not `:scope` as at the top level of a stylesheet.
/// https://drafts.csswg.org/css-cascade-6/#scoped-rules
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) in_scope_rule: bool,
/// Running total of bytes emitted by duplicate vendor-prefix passes. A rule
/// whose selector list carries more than one vendor prefix (e.g. a list
/// mixing `:-webkit-autofill` with an unprefixed pseudo-class, or a single
Expand Down Expand Up @@ -310,6 +315,7 @@ impl<'a> Printer<'a> {
css_module: None,
ctx: None,
nesting_expansions: 0,
in_scope_rule: false,
prefix_expansion_bytes: 0,
error_kind: None,
}
Expand Down
52 changes: 30 additions & 22 deletions src/css/rules/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,37 @@ impl<R> ScopeRule<R> {
dest.write_char(b')')?;
dest.whitespace()?;
}
if let Some(scope_end) = &self.scope_end {
if dest.minify {
dest.write_char(b' ')?;
}
dest.write_str("to (")?;
// `&` in <scope-end> is the scoping root (css-cascade-6), not <scope-start>.
dest.with_cleared_context(scope_end, |scope_end, d: &mut Printer| {
serialize_selector_list(scope_end.v.slice(), d, None, false)
})?;
dest.write_char(b')')?;
dest.whitespace()?;
}
dest.write_char(b'{')?;
dest.indent();
dest.newline()?;
// Nested style rules within @scope are implicitly relative to the <scope-start>
// so clear our style context while printing them to avoid replacing & ourselves.
// From here on `&` is the scoping root: in <scope-end> and in the
// nested rules. With no nesting in the targets it prints as
// `:where(:scope)` (see `serialize::serialize_nesting`).
Comment thread
robobun marked this conversation as resolved.
Resolved
// https://drafts.csswg.org/css-cascade-6/#scoped-rules
dest.with_cleared_context(&self.rules, |rules, d: &mut Printer| rules.to_css(d))?;
dest.dedent();
dest.newline()?;
dest.write_char(b'}')?;
Ok(())
let was_in_scope_rule = dest.in_scope_rule;
dest.in_scope_rule = true;
let result = (|dest: &mut Printer| -> Result<(), PrintErr> {
if let Some(scope_end) = &self.scope_end {
if dest.minify {
dest.write_char(b' ')?;
}
dest.write_str("to (")?;
// `&` in <scope-end> is the scoping root (css-cascade-6), not <scope-start>.
dest.with_cleared_context(scope_end, |scope_end, d: &mut Printer| {
serialize_selector_list(scope_end.v.slice(), d, None, false)
})?;
dest.write_char(b')')?;
dest.whitespace()?;
}
dest.write_char(b'{')?;
dest.indent();
dest.newline()?;
// Nested style rules within @scope are implicitly relative to the <scope-start>
// so clear our style context while printing them to avoid replacing & ourselves.
Comment thread
robobun marked this conversation as resolved.
Resolved
dest.with_cleared_context(&self.rules, |rules, d: &mut Printer| rules.to_css(d))?;
dest.dedent();
dest.newline()?;
dest.write_char(b'}')
})(dest);
dest.in_scope_rule = was_in_scope_rule;
result
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/css/selectors/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,10 @@ fn parse_relative_selector<Impl: BunSelectorImpl>(
None
};

let scope: GenericComponent<Impl> = if nesting_requirement == NestingRequirement::Implicit {
let scope: GenericComponent<Impl> = if matches!(
nesting_requirement,
NestingRequirement::Implicit | NestingRequirement::Scoped
) {
GenericComponent::Nesting
} else {
GenericComponent::Scope
Expand Down Expand Up @@ -2766,6 +2769,11 @@ pub enum NestingRequirement {
Prefixed,
Contained,
Implicit,
/// A scoped style rule (a rule nested in `@scope`). A leading combinator is
/// anchored to `&` (the scoping root), but a selector without one gets no
/// implicit prefix: scoped rules already match within the scope.
/// https://drafts.csswg.org/css-cascade-6/#scoped-rules
Comment thread
robobun marked this conversation as resolved.
Resolved
Scoped,
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, strum::IntoStaticStr, CssHash)]
Expand Down
10 changes: 7 additions & 3 deletions src/css/selectors/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1327,11 +1327,15 @@ pub(crate) mod serialize {
}
} else {
// If there is no context, we are at the root if nesting is supported. This is equivalent to :scope.
// Inside @scope, `&` is the scoping root with no specificity: :where(:scope).
// https://drafts.csswg.org/css-cascade-6/#scoped-rules
Comment thread
robobun marked this conversation as resolved.
Resolved
// Otherwise, if nesting is supported, serialize the nesting selector directly.
if dest.targets.should_compile_same(Feature::Nesting) {
dest.write_str(b":scope")?;
} else {
if !dest.targets.should_compile_same(Feature::Nesting) {
dest.write_char(b'&')?;
} else if dest.in_scope_rule {
dest.write_str(b":where(:scope)")?;
} else {
dest.write_str(b":scope")?;
}
}
Ok(())
Expand Down
Loading
Loading