css_ast/
stylerule.rs

1use crate::{CssAtomSet, CssDiagnostic, SelectorList, StyleValue, UnknownAtRule, UnknownQualifiedRule, rules};
2use css_parse::{
3	BadDeclaration, Cursor, Diagnostic, Parse, Parser, QualifiedRule, Result as ParserResult, RuleVariants,
4};
5use csskit_derives::{Parse, Peek, ToCursors, ToSpan};
6
7/// Represents a "Style Rule", such as `body { width: 100% }`. See also the CSS-OM [CSSStyleRule][1] interface.
8///
9/// The Style Rule is comprised of two child nodes: the [SelectorList] represents the selectors of the rule.
10/// Each [Declaration][css_parse::Declaration] will have a [StyleValue], and each rule will be a [NestedGroupRule].
11///
12/// [1]: https://drafts.csswg.org/cssom-1/#the-cssstylerule-interface
13#[derive(Parse, Peek, ToSpan, ToCursors, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
15#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
16pub struct StyleRule<'a>(pub QualifiedRule<'a, SelectorList<'a>, StyleValue<'a>, NestedGroupRule<'a>>);
17
18// https://drafts.csswg.org/css-nesting/#conditionals
19macro_rules! apply_rules {
20	($macro: ident) => {
21		$macro! {
22			Container(ContainerRule<'a>): "container",
23			Layer(LayerRule<'a>): "layer",
24			Media(MediaRule<'a>): "media",
25			Scope(ScopeRule): "scope",
26			Supports(SupportsRule<'a>): "supports",
27		}
28	};
29}
30
31macro_rules! nested_group_rule {
32    ( $(
33        $name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
34    )+ ) => {
35		#[allow(clippy::large_enum_variant)] // TODO: Box?
36		// https://drafts.csswg.org/cssom-1/#the-cssrule-interface
37		#[derive(ToSpan, ToCursors, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38		#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
39		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
40		pub enum NestedGroupRule<'a> {
41			$(
42				$name(rules::$ty$(<$a>)?),
43			)+
44			UnknownAt(UnknownAtRule<'a>),
45			Style(StyleRule<'a>),
46			Unknown(UnknownQualifiedRule<'a>),
47			BadDeclaration(BadDeclaration<'a>),
48		}
49	}
50}
51apply_rules!(nested_group_rule);
52
53impl<'a> RuleVariants<'a> for NestedGroupRule<'a> {
54	fn parse_at_rule<I>(p: &mut Parser<'a, I>, name: Cursor) -> ParserResult<Self>
55	where
56		I: Iterator<Item = Cursor> + Clone,
57	{
58		macro_rules! parse_rule {
59			( $(
60				$name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
61			)+ ) => {
62				match p.to_atom::<CssAtomSet>(name) {
63					$(CssAtomSet::$name => p.parse::<rules::$ty>().map(Self::$name),)+
64					_ => Err(Diagnostic::new(name.into(), Diagnostic::unexpected_at_rule))?,
65				}
66			}
67		}
68		apply_rules!(parse_rule)
69	}
70
71	fn parse_unknown_at_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
72	where
73		I: Iterator<Item = Cursor> + Clone,
74	{
75		p.parse::<UnknownAtRule>().map(Self::UnknownAt)
76	}
77
78	fn parse_qualified_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
79	where
80		I: Iterator<Item = Cursor> + Clone,
81	{
82		p.parse::<StyleRule>().map(Self::Style)
83	}
84
85	fn parse_unknown_qualified_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
86	where
87		I: Iterator<Item = Cursor> + Clone,
88	{
89		p.parse::<UnknownQualifiedRule>().map(Self::Unknown)
90	}
91
92	fn bad_declaration(b: BadDeclaration<'a>) -> Option<Self> {
93		Some(Self::BadDeclaration(b))
94	}
95}
96
97impl<'a> Parse<'a> for NestedGroupRule<'a> {
98	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
99	where
100		I: Iterator<Item = Cursor> + Clone,
101	{
102		Self::parse_rule_variants(p)
103	}
104}
105
106#[cfg(test)]
107mod tests {
108	use super::*;
109	use crate::CssAtomSet;
110	use css_parse::assert_parse;
111
112	#[test]
113	fn size_test() {
114		assert_eq!(std::mem::size_of::<StyleRule>(), 128);
115	}
116
117	#[test]
118	fn test_writes() {
119		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{}");
120		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body,body{}");
121		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{width:1px;}");
122		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{opacity:0;}");
123		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo *{}");
124		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":nth-child(1){opacity:0;}");
125		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{--bar:(baz);}");
126		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{width: calc(1px + (var(--foo)) + 1px);}");
127		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{--bar:1}");
128		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{--custom:{width:0;height:0;};}");
129		// Semicolons are "allowed" in geneirc preludes
130		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{a;b{}}");
131		// Bad Declarations should be parsable.
132		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{$(var)-size: 100%;}");
133	}
134}