Skip to main content

css_ast/
stylerule.rs

1use crate::{
2	CssAtomSet, CssMetadata, SelectorList, StyleValue, UnknownAtRule, UnknownQualifiedRule, diagnostics::CssDiagnostic,
3	rules,
4};
5use css_parse::{
6	Box, Cursor, DeclarationGroup, Diagnostic, Parse, Parser, QualifiedRule, Result as ParserResult, RuleVariants,
7};
8use csskit_derives::*;
9use csskit_proc_macro::node;
10
11/// Represents a "Style Rule", such as `body { width: 100% }`. See also the CSS-OM [CSSStyleRule][1] interface.
12///
13/// The Style Rule is comprised of two child nodes: the [SelectorList] represents the selectors of the rule.
14/// Each [Declaration][css_parse::Declaration] will have a [StyleValue], and each rule will be a [NestedGroupRule].
15///
16/// [1]: https://drafts.csswg.org/cssom-1/#the-cssstylerule-interface
17#[node]
18#[derive(Parse, Peek, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
20#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
21#[derive(csskit_derives::NodeWithMetadata)]
22#[metadata(node_kinds = StyleRule)]
23pub struct StyleRule<'a> {
24	#[metadata(block)]
25	pub rule: QualifiedRule<'a, SelectorList<'a>, StyleValue<'a>, NestedGroupRule<'a>, CssMetadata>,
26}
27
28// https://drafts.csswg.org/css-nesting/#conditionals
29macro_rules! apply_rules {
30	($macro: ident) => {
31		$macro! {
32			Container(ContainerRule<'a>): "container",
33			Layer(LayerRule<'a>): "layer",
34			Media(MediaRule<'a>): "media",
35			Scope(ScopeRule<'a>): "scope",
36		}
37	};
38}
39
40macro_rules! nested_group_rule {
41    ( $(
42        $name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
43    )+ ) => {
44		/// <https://drafts.csswg.org/cssom-1/#the-cssrule-interface>
45		#[node]
46		#[derive(ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
47		#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
48		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
49		#[derive(csskit_derives::NodeWithMetadata)]
50		pub enum NestedGroupRule<'a> {
51			$(
52				$name(rules::$ty$(<$a>)?),
53			)+
54			Supports(Box<'a, rules::SupportsRule<'a>>),
55			UnknownAt(UnknownAtRule<'a>),
56			Style(StyleRule<'a>),
57			Unknown(UnknownQualifiedRule<'a>),
58			Declarations(DeclarationGroup<'a, StyleValue<'a>, CssMetadata>),
59		}
60	}
61}
62apply_rules!(nested_group_rule);
63
64impl<'a> RuleVariants<'a> for NestedGroupRule<'a> {
65	type DeclarationValue = StyleValue<'a>;
66	type Metadata = CssMetadata;
67
68	fn parse_at_rule<I>(p: &mut Parser<'a, I>, name: Cursor) -> ParserResult<Self>
69	where
70		I: Iterator<Item = Cursor> + Clone,
71	{
72		macro_rules! parse_rule {
73			( $(
74				$name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
75			)+ ) => {
76				match p.to_atom::<CssAtomSet>(name) {
77					$(CssAtomSet::$name => p.parse::<rules::$ty>().map(Self::$name),)+
78					CssAtomSet::Supports => p.parse::<rules::SupportsRule>().map(|r| Self::Supports(Box::new_in(p.alloc(), r))),
79					_ => Err(Diagnostic::new(name.into(), Diagnostic::unexpected_at_rule))?,
80				}
81			}
82		}
83		apply_rules!(parse_rule)
84	}
85
86	fn parse_unknown_at_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
87	where
88		I: Iterator<Item = Cursor> + Clone,
89	{
90		p.parse::<UnknownAtRule>().map(Self::UnknownAt)
91	}
92
93	fn parse_qualified_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
94	where
95		I: Iterator<Item = Cursor> + Clone,
96	{
97		p.parse::<StyleRule>().map(Self::Style)
98	}
99
100	fn parse_unknown_qualified_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
101	where
102		I: Iterator<Item = Cursor> + Clone,
103	{
104		p.parse::<UnknownQualifiedRule>().map(Self::Unknown)
105	}
106
107	fn is_unknown(&self) -> bool {
108		matches!(self, Self::UnknownAt(_) | Self::Unknown(_))
109	}
110
111	fn from_declaration_group(
112		group: css_parse::DeclarationGroup<'a, Self::DeclarationValue, Self::Metadata>,
113	) -> Option<Self> {
114		Some(Self::Declarations(group))
115	}
116}
117
118impl<'a> Parse<'a> for NestedGroupRule<'a> {
119	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
120	where
121		I: Iterator<Item = Cursor> + Clone,
122	{
123		Self::parse_rule_variants(p)
124	}
125}
126
127#[cfg(test)]
128mod tests {
129	use super::*;
130	use crate::CssAtomSet;
131	use css_parse::assert_parse;
132
133	#[cfg(feature = "visitable")]
134	use crate::assert_visits;
135
136	#[test]
137	fn test_writes() {
138		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{}");
139		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body,body{}");
140		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{width:1px;}");
141		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{opacity:0;}");
142		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo *{}");
143		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":nth-child(1){opacity:0;}");
144		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{--bar:(baz);}");
145		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{width: calc(1px + (var(--foo)) + 1px);}");
146		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{--bar:1}");
147		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{--custom:{width:0;height:0;};}");
148		// Semicolons are "allowed" in geneirc preludes
149		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{a;b{}}");
150		// Bad Declarations should be parsable.
151		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{$(var)-size: 100%;}");
152		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".md{--:ra( ;86)}");
153		assert_parse!(CssAtomSet::ATOMS, StyleRule, "a{@supports (color:red){color:red;}}");
154		assert_parse!(CssAtomSet::ATOMS, StyleRule, "a{@supports selector(a){color:red;b{color:blue;}}}");
155		assert_parse!(CssAtomSet::ATOMS, StyleRule, "a{@container (width>0){color:red;}}");
156		assert_parse!(CssAtomSet::ATOMS, StyleRule, "a{@layer foo{color:red;}}");
157		assert_parse!(CssAtomSet::ATOMS, StyleRule, "a{@scope(.card) to (img){color:red;}}");
158	}
159
160	#[test]
161	#[cfg(feature = "visitable")]
162	fn test_visits() {
163		assert_visits!(
164			":root{html:has(&[open]){overflow:hidden}}",
165			StyleRule,
166			SelectorList,
167			CompoundSelector,
168			PseudoClass,
169			StyleRule,
170			SelectorList,
171			CompoundSelector,
172			Tag,
173			HtmlTag,
174			HasPseudoFunction,
175			SelectorList,
176			CompoundSelector,
177			Combinator,
178			Attribute,
179			StyleValue,
180			OverflowStyleValue,
181			OverflowBlockStyleValue
182		);
183	}
184}