Skip to main content

css_ast/
stylesheet.rs

1use crate::{CssAtomSet, CssMetadata, StyleValue, rules, stylerule::StyleRule};
2use css_lexer::KindSet;
3use css_parse::Vec;
4use css_parse::{
5	Box, ComponentValues, Cursor, Diagnostic, NodeWithMetadata, Parse, Parser, Peek, QualifiedRule,
6	Result as ParserResult, RuleVariants, StyleSheet as StyleSheetTrait, T, UnknownRuleBlock,
7};
8use csskit_derives::*;
9use csskit_proc_macro::node;
10
11#[cfg(feature = "visitable")]
12use crate::{
13	PropertyKind,
14	visit::{NodeId, QueryableNode},
15};
16
17/// <https://drafts.csswg.org/cssom-1/#the-cssstylesheet-interface>
18#[node]
19#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
21#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
22pub struct StyleSheet<'a> {
23	pub rules: Vec<'a, Rule<'a>>,
24	#[to_cursors(skip)]
25	#[cfg_attr(feature = "serde", serde(skip))]
26	#[cfg_attr(feature = "visitable", visit(skip))]
27	meta: CssMetadata,
28}
29
30impl<'a> NodeWithMetadata<CssMetadata> for StyleSheet<'a> {
31	fn metadata(&self) -> CssMetadata {
32		self.meta
33	}
34}
35
36impl<'a> Peek<'a> for StyleSheet<'a> {
37	const PEEK_KINDSET: KindSet = KindSet::ANY;
38}
39
40// A StyleSheet represents the root node of a CSS-like language.
41// The StyleSheet trait represents an abstraction of this, which allows for
42// alternate implementations such as SCSS.
43// AtRules vs QualifiedRules are differentiated by two different functions.
44impl<'a> Parse<'a> for StyleSheet<'a> {
45	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
46	where
47		I: Iterator<Item = Cursor> + Clone,
48	{
49		let (rules, meta) = Self::parse_stylesheet(p)?;
50		Ok(Self { rules, meta })
51	}
52}
53
54impl<'a> StyleSheetTrait<'a, CssMetadata> for StyleSheet<'a> {
55	type Rule = Rule<'a>;
56}
57
58macro_rules! apply_rules {
59	($macro: ident) => {
60		$macro! {
61			Charset(CharsetRule): CssAtomSet::Charset,
62			ColorProfile(ColorProfileRule<'a>): CssAtomSet::ColorProfile,
63			Container(ContainerRule<'a>): CssAtomSet::Container,
64			CounterStyle(CounterStyleRule<'a>): CssAtomSet::CounterStyle,
65			FontFace(FontFaceRule<'a>): CssAtomSet::FontFace,
66			FontFeatureValues(FontFeatureValuesRule<'a>): CssAtomSet::FontFeatureValues,
67			FontPaletteValues(FontPaletteValuesRule<'a>): CssAtomSet::FontPaletteValues,
68			Keyframes(KeyframesRule<'a>): CssAtomSet::Keyframes,
69			Layer(LayerRule<'a>): CssAtomSet::Layer,
70			Media(MediaRule<'a>): CssAtomSet::Media,
71			Namespace(NamespaceRule): CssAtomSet::Namespace,
72			Page(PageRule<'a>): CssAtomSet::Page,
73			Property(PropertyRule<'a>): CssAtomSet::Property,
74			Scope(ScopeRule<'a>): CssAtomSet::Scope,
75			StartingStyle(StartingStyleRule<'a>): CssAtomSet::StartingStyle,
76
77			// Deprecated Rules
78			Document(DocumentRule<'a>): CssAtomSet::Document,
79
80			// Vendor Prefixed
81			WebkitKeyframes(WebkitKeyframesRule<'a>): CssAtomSet::_WebkitKeyframes,
82
83			// https://developer.mozilla.org/en-US/docs/Web/CSS/Mozilla_Extensions#at-rules
84			MozDocument(MozDocumentRule<'a>): CssAtomSet::_MozDocument,
85		}
86	};
87}
88
89#[node]
90#[derive(Parse, Peek, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
92#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self), queryable(skip))]
93#[derive(csskit_derives::NodeWithMetadata)]
94#[metadata(node_kinds = Unknown | Effective, property_kinds = Name)]
95pub struct UnknownAtRule<'a> {
96	name: T![AtKeyword],
97	prelude: ComponentValues<'a>,
98	block: ComponentValues<'a>,
99}
100
101#[cfg(feature = "visitable")]
102impl<'a> QueryableNode for UnknownAtRule<'a> {
103	const NODE_ID: NodeId = NodeId::UnknownAtRule;
104
105	fn get_property(&self, kind: PropertyKind) -> Option<Cursor> {
106		match kind {
107			PropertyKind::Name => Some(self.name.into()),
108			_ => None,
109		}
110	}
111}
112
113#[node]
114#[derive(Parse, Peek, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
116#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
117#[derive(csskit_derives::NodeWithMetadata)]
118#[metadata(node_kinds = Unknown | Effective)]
119pub struct UnknownQualifiedRule<'a>(
120	QualifiedRule<
121		'a,
122		UnknownRuleBlock<'a>,
123		StyleValue<'a>,
124		UnknownRuleBlock<'a, StyleValue<'a>, CssMetadata>,
125		CssMetadata,
126	>,
127);
128
129macro_rules! rule {
130    ( $(
131        $name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
132    )+ ) => {
133		/// <https://drafts.csswg.org/cssom-1/#the-cssrule-interface>
134		#[node]
135		#[derive(ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
136		#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
137		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
138		#[derive(csskit_derives::NodeWithMetadata)]
139		pub enum Rule<'a> {
140			$(
141				$name(rules::$ty$(<$a>)?),
142			)+
143			// Boxed variants for rarely used rules
144			Import(Box<'a, rules::ImportRule<'a>>),
145			Supports(Box<'a, rules::SupportsRule<'a>>),
146
147			UnknownAt(UnknownAtRule<'a>),
148			Style(StyleRule<'a>),
149			Unknown(UnknownQualifiedRule<'a>)
150		}
151	}
152}
153
154apply_rules!(rule);
155
156impl<'a> RuleVariants<'a> for Rule<'a> {
157	type DeclarationValue = StyleValue<'a>;
158	type Metadata = CssMetadata;
159
160	fn parse_at_rule<I>(p: &mut Parser<'a, I>, c: Cursor) -> ParserResult<Self>
161	where
162		I: Iterator<Item = Cursor> + Clone,
163	{
164		macro_rules! parse_rule {
165			( $(
166				$name: ident($ty: ident$(<$a: lifetime>)?): $atoms: pat,
167			)+ ) => {
168				match p.to_atom::<CssAtomSet>(c) {
169					$($atoms => p.parse::<rules::$ty>().map(Self::$name),)+
170					CssAtomSet::Import => p.parse::<rules::ImportRule>().map(|r| Self::Import(Box::new_in(p.alloc(), r))),
171					CssAtomSet::Supports => p.parse::<rules::SupportsRule>().map(|r| Self::Supports(Box::new_in(p.alloc(), r))),
172					_ => Err(Diagnostic::new(p.next(), Diagnostic::unexpected))?,
173				}
174			}
175		}
176		apply_rules!(parse_rule)
177	}
178
179	fn parse_unknown_at_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
180	where
181		I: Iterator<Item = Cursor> + Clone,
182	{
183		p.parse::<UnknownAtRule>().map(Self::UnknownAt)
184	}
185
186	fn parse_qualified_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
187	where
188		I: Iterator<Item = Cursor> + Clone,
189	{
190		p.parse::<StyleRule>().map(Self::Style)
191	}
192
193	fn parse_unknown_qualified_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
194	where
195		I: Iterator<Item = Cursor> + Clone,
196	{
197		p.parse::<UnknownQualifiedRule>().map(Self::Unknown)
198	}
199}
200
201impl<'a> Peek<'a> for Rule<'a> {
202	const PEEK_KINDSET: KindSet = KindSet::ANY;
203}
204
205impl<'a> Parse<'a> for Rule<'a> {
206	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
207	where
208		I: Iterator<Item = Cursor> + Clone,
209	{
210		Self::parse_rule_variants(p)
211	}
212}
213
214#[cfg(test)]
215mod tests {
216	use super::*;
217	use crate::CssAtomSet;
218	use css_parse::assert_parse;
219
220	#[test]
221	fn test_writes() {
222		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "body{}");
223		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "body{color:red;}");
224		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "body,tr:nth-child(n-1){}");
225		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "body{width:1px;}");
226		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "body{width:1px;}.a{width:2px;}");
227		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "one:1;a{two:2}");
228		assert_parse!(CssAtomSet::ATOMS, Rule, "@media screen{}", Rule::Media(_));
229		assert_parse!(CssAtomSet::ATOMS, Rule, "@layer foo{}", Rule::Layer(_));
230	}
231
232	#[test]
233	fn cdc_in_unknown_rule_block() {
234		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "={-->}");
235		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "={-->");
236	}
237
238	#[test]
239	fn stray_close_bracket_in_block() {
240		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "%{)");
241		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "t% {90)");
242		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "{--ty-2% {90)");
243		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "%{]");
244	}
245}