diff --git a/src/lib.rs b/src/lib.rs index e8163be3..1ff4b19f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21823,6 +21823,29 @@ 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}}", + ); + } + #[test] fn test_custom_media() { custom_media_test( diff --git a/src/parser.rs b/src/parser.rs index 2b6a8078..f6658966 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,7 +7,9 @@ use crate::rules::container::{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::viewport::ViewportRule; + use crate::rules::{ counter_style::CounterStyleRule, custom_media::CustomMediaRule, @@ -137,6 +139,10 @@ impl<'a, 'o, 'b, 'i, T> TopLevelRuleParser<'a, 'o, 'i, T> { pub enum AtRulePrelude<'i, T> { /// A @font-face rule prelude. FontFace, + + /// A @scope rule prelude. + Scope, + /// A @font-feature-values rule prelude, with its FamilyName list. FontFeatureValues, //(Vec), /// A @font-palette-values rule prelude, with its name. @@ -423,6 +429,9 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne "font-face" => { Ok(AtRulePrelude::FontFace) }, + "scope" => { + Ok(AtRulePrelude::Scope) + }, // "font-feature-values" => { // if !cfg!(feature = "gecko") { // // Support for this rule is not fully implemented in Servo yet. @@ -563,6 +572,10 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne rules: self.parse_nested_rules(input)?, loc, })), + AtRulePrelude::Scope => Ok(CssRule::Scope(ScopeRule { + rules: self.parse_nested_rules(input)?, + loc, + })), AtRulePrelude::Viewport(vendor_prefix) => { Ok(CssRule::Viewport(ViewportRule { vendor_prefix, diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 1347c678..7a951394 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -50,6 +50,7 @@ pub mod namespace; pub mod nesting; pub mod page; pub mod property; +pub mod scope; pub mod style; pub mod supports; pub mod unknown; @@ -88,6 +89,7 @@ use media::MediaRule; use namespace::NamespaceRule; use nesting::NestingRule; use page::PageRule; +use scope::ScopeRule; use std::collections::{HashMap, HashSet}; use style::StyleRule; use supports::SupportsRule; @@ -163,6 +165,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 placeholder for a rule that was removed. Ignored, /// An unknown at-rule. @@ -299,6 +303,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)) + } "ignored" => Ok(CssRule::Ignored), "unknown" => { let rule = UnknownAtRule::deserialize(deserializer)?; @@ -337,6 +345,7 @@ impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> { CssRule::LayerBlock(layer) => layer.to_css(dest), CssRule::Property(property) => property.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, @@ -641,6 +650,7 @@ impl<'i, T> CssRuleList<'i, T> { continue; } } + CssRule::Scope(scope) => scope.minify(context)?, CssRule::Nesting(nesting) => { if nesting.minify(context, parent_is_unused)? { continue; diff --git a/src/rules/scope.rs b/src/rules/scope.rs new file mode 100644 index 00000000..e28a97a6 --- /dev/null +++ b/src/rules/scope.rs @@ -0,0 +1,54 @@ +//! The `@scope` rule. + +use super::Location; +use super::{CssRuleList, MinifyContext}; +use crate::error::{MinifyError, PrinterError}; +use crate::parser::DefaultAtRule; +use crate::printer::Printer; +use crate::traits::ToCss; +#[cfg(feature = "visitor")] +use crate::visitor::Visit; + +/// A [@scope](https://drafts.csswg.org/css-cascade-6/#scope-atrule) rule. +/// +/// @scope () [to ()]? { +/// +/// } +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub struct ScopeRule<'i, R = DefaultAtRule> { + // TODO: support () [to ()]? + /// Nested rules within the `@scope` rule. + #[cfg_attr(feature = "serde", serde(borrow))] + pub rules: CssRuleList<'i, R>, + /// The location of the rule in the source file. + #[cfg_attr(feature = "visitor", skip_visit)] + pub loc: Location, +} + +impl<'i, T> ScopeRule<'i, T> { + pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) -> Result<(), MinifyError> { + self.rules.minify(context, false) + } +} + +impl<'i, T: ToCss> ToCss for ScopeRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + #[cfg(feature = "sourcemap")] + dest.add_mapping(self.loc); + dest.write_str("@scope")?; + dest.whitespace()?; + dest.write_char('{')?; + dest.indent(); + dest.newline()?; + self.rules.to_css(dest)?; + dest.dedent(); + dest.newline()?; + dest.write_char('}') + } +}