css_ast/
stylerule.rs

1use crate::{
2	CssAtomSet, CssDiagnostic, CssMetadata, SelectorList, StyleValue, UnknownAtRule, UnknownQualifiedRule, rules,
3};
4use css_parse::{
5	BadDeclaration, Cursor, Diagnostic, NodeWithMetadata, Parse, Parser, QualifiedRule, Result as ParserResult,
6	RuleVariants,
7};
8use csskit_derives::{Parse, Peek, SemanticEq, ToCursors, ToSpan};
9
10/// Represents a "Style Rule", such as `body { width: 100% }`. See also the CSS-OM [CSSStyleRule][1] interface.
11///
12/// The Style Rule is comprised of two child nodes: the [SelectorList] represents the selectors of the rule.
13/// Each [Declaration][css_parse::Declaration] will have a [StyleValue], and each rule will be a [NestedGroupRule].
14///
15/// [1]: https://drafts.csswg.org/cssom-1/#the-cssstylerule-interface
16#[derive(Parse, Peek, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
18#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
19pub struct StyleRule<'a> {
20	pub rule: QualifiedRule<'a, SelectorList<'a>, StyleValue<'a>, NestedGroupRule<'a>, CssMetadata>,
21}
22
23impl<'a> NodeWithMetadata<CssMetadata> for StyleRule<'a> {
24	fn metadata(&self) -> CssMetadata {
25		self.rule.metadata()
26	}
27}
28
29impl<'a> NodeWithMetadata<CssMetadata> for NestedGroupRule<'a> {
30	fn metadata(&self) -> CssMetadata {
31		match self {
32			Self::Container(r) => r.metadata(),
33			Self::Layer(r) => r.metadata(),
34			Self::Media(r) => r.metadata(),
35			Self::Scope(r) => r.metadata(),
36			Self::Supports(r) => r.metadata(),
37			Self::UnknownAt(r) => r.metadata(),
38			Self::Style(r) => r.metadata(),
39			Self::Unknown(r) => r.metadata(),
40			Self::BadDeclaration(r) => r.metadata(),
41		}
42	}
43}
44
45// https://drafts.csswg.org/css-nesting/#conditionals
46macro_rules! apply_rules {
47	($macro: ident) => {
48		$macro! {
49			Container(ContainerRule<'a>): "container",
50			Layer(LayerRule<'a>): "layer",
51			Media(MediaRule<'a>): "media",
52			Scope(ScopeRule): "scope",
53			Supports(SupportsRule<'a>): "supports",
54		}
55	};
56}
57
58macro_rules! nested_group_rule {
59    ( $(
60        $name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
61    )+ ) => {
62		#[allow(clippy::large_enum_variant)] // TODO: Box?
63		// https://drafts.csswg.org/cssom-1/#the-cssrule-interface
64		#[derive(ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
65		#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
66		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
67		pub enum NestedGroupRule<'a> {
68			$(
69				$name(rules::$ty$(<$a>)?),
70			)+
71			UnknownAt(UnknownAtRule<'a>),
72			Style(StyleRule<'a>),
73			Unknown(UnknownQualifiedRule<'a>),
74			BadDeclaration(BadDeclaration<'a>),
75		}
76	}
77}
78apply_rules!(nested_group_rule);
79
80impl<'a> RuleVariants<'a> for NestedGroupRule<'a> {
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 bad_declaration(b: BadDeclaration<'a>) -> Option<Self> {
120		Some(Self::BadDeclaration(b))
121	}
122}
123
124impl<'a> Parse<'a> for NestedGroupRule<'a> {
125	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
126	where
127		I: Iterator<Item = Cursor> + Clone,
128	{
129		Self::parse_rule_variants(p)
130	}
131}
132
133#[cfg(test)]
134mod tests {
135	use super::*;
136	use crate::CssAtomSet;
137	use css_parse::assert_parse;
138
139	#[test]
140	fn size_test() {
141		assert_eq!(std::mem::size_of::<StyleRule>(), 176);
142	}
143
144	#[test]
145	fn test_writes() {
146		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{}");
147		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body,body{}");
148		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{width:1px;}");
149		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{opacity:0;}");
150		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo *{}");
151		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":nth-child(1){opacity:0;}");
152		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{--bar:(baz);}");
153		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{width: calc(1px + (var(--foo)) + 1px);}");
154		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{--bar:1}");
155		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{--custom:{width:0;height:0;};}");
156		// Semicolons are "allowed" in geneirc preludes
157		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{a;b{}}");
158		// Bad Declarations should be parsable.
159		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{$(var)-size: 100%;}");
160	}
161}