css_ast/
stylerule.rs

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