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, SimpleBlock, 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			CustomMedia(CustomMediaRule<'a>): CssAtomSet::CustomMedia,
66			FontFace(FontFaceRule<'a>): CssAtomSet::FontFace,
67			FontFeatureValues(FontFeatureValuesRule<'a>): CssAtomSet::FontFeatureValues,
68			FontPaletteValues(FontPaletteValuesRule<'a>): CssAtomSet::FontPaletteValues,
69			Keyframes(KeyframesRule<'a>): CssAtomSet::Keyframes,
70			Layer(LayerRule<'a>): CssAtomSet::Layer,
71			Media(MediaRule<'a>): CssAtomSet::Media,
72			Namespace(NamespaceRule): CssAtomSet::Namespace,
73			Page(PageRule<'a>): CssAtomSet::Page,
74			Property(PropertyRule<'a>): CssAtomSet::Property,
75			Scope(ScopeRule<'a>): CssAtomSet::Scope,
76			StartingStyle(StartingStyleRule<'a>): CssAtomSet::StartingStyle,
77
78			// Deprecated Rules
79			Document(DocumentRule<'a>): CssAtomSet::Document,
80
81			// Vendor Prefixed
82			WebkitKeyframes(WebkitKeyframesRule<'a>): CssAtomSet::_WebkitKeyframes,
83
84			// https://developer.mozilla.org/en-US/docs/Web/CSS/Mozilla_Extensions#at-rules
85			MozDocument(MozDocumentRule<'a>): CssAtomSet::_MozDocument,
86		}
87	};
88}
89
90#[node]
91#[derive(Parse, Peek, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
93#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self), queryable(skip))]
94#[derive(csskit_derives::NodeWithMetadata)]
95#[metadata(node_kinds = Unknown | Effective, property_kinds = Name)]
96pub struct UnknownAtRule<'a> {
97	name: T![AtKeyword],
98	prelude: UnknownAtRulePrelude<'a>,
99	block: Option<SimpleBlock<'a>>,
100	#[semantic_eq(skip)]
101	semicolon: Option<T![;]>,
102}
103
104#[node]
105#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
107#[parse(stop = KindSet::LEFT_CURLY_OR_SEMICOLON)]
108#[derive(csskit_derives::NodeWithMetadata)]
109pub struct UnknownAtRulePrelude<'a>(ComponentValues<'a>);
110
111#[cfg(feature = "visitable")]
112impl<'a> QueryableNode for UnknownAtRule<'a> {
113	const NODE_ID: NodeId = NodeId::UnknownAtRule;
114
115	fn get_property(&self, kind: PropertyKind) -> Option<Cursor> {
116		match kind {
117			PropertyKind::Name => Some(self.name.into()),
118			_ => None,
119		}
120	}
121}
122
123#[node]
124#[derive(Parse, Peek, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
126#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
127#[derive(csskit_derives::NodeWithMetadata)]
128#[metadata(node_kinds = Unknown | Effective)]
129pub struct UnknownQualifiedRule<'a>(
130	QualifiedRule<
131		'a,
132		UnknownRuleBlock<'a>,
133		StyleValue<'a>,
134		UnknownRuleBlock<'a, StyleValue<'a>, CssMetadata>,
135		CssMetadata,
136	>,
137);
138
139macro_rules! rule {
140    ( $(
141        $name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
142    )+ ) => {
143		/// <https://drafts.csswg.org/cssom-1/#the-cssrule-interface>
144		#[node]
145		#[derive(ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
146		#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
147		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
148		#[derive(csskit_derives::NodeWithMetadata)]
149		pub enum Rule<'a> {
150			$(
151				$name(Box<'a, rules::$ty$(<$a>)?>),
152			)+
153			Import(Box<'a, rules::ImportRule<'a>>),
154			Supports(Box<'a, rules::SupportsRule<'a>>),
155
156			UnknownAt(Box<'a, UnknownAtRule<'a>>),
157			Style(Box<'a, StyleRule<'a>>),
158			Unknown(Box<'a, UnknownQualifiedRule<'a>>)
159		}
160	}
161}
162
163apply_rules!(rule);
164
165impl<'a> RuleVariants<'a> for Rule<'a> {
166	type DeclarationValue = StyleValue<'a>;
167	type Metadata = CssMetadata;
168
169	fn parse_at_rule<I>(p: &mut Parser<'a, I>, c: Cursor) -> ParserResult<Self>
170	where
171		I: Iterator<Item = Cursor> + Clone,
172	{
173		macro_rules! parse_rule {
174			( $(
175				$name: ident($ty: ident$(<$a: lifetime>)?): $atoms: pat,
176			)+ ) => {
177				match p.to_atom::<CssAtomSet>(c) {
178					$($atoms => p.parse::<Box<rules::$ty>>().map(Self::$name),)+
179					CssAtomSet::Import => p.parse::<rules::ImportRule>().map(|r| Self::Import(Box::new_in(p.alloc(), r))),
180					CssAtomSet::Supports => p.parse::<rules::SupportsRule>().map(|r| Self::Supports(Box::new_in(p.alloc(), r))),
181					_ => Err(Diagnostic::new(p.next(), Diagnostic::unexpected))?,
182				}
183			}
184		}
185		apply_rules!(parse_rule)
186	}
187
188	fn parse_unknown_at_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
189	where
190		I: Iterator<Item = Cursor> + Clone,
191	{
192		p.parse::<Box<UnknownAtRule>>().map(Self::UnknownAt)
193	}
194
195	fn parse_qualified_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
196	where
197		I: Iterator<Item = Cursor> + Clone,
198	{
199		p.parse::<Box<StyleRule>>().map(Self::Style)
200	}
201
202	fn parse_unknown_qualified_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
203	where
204		I: Iterator<Item = Cursor> + Clone,
205	{
206		p.parse::<Box<UnknownQualifiedRule>>().map(Self::Unknown)
207	}
208}
209
210impl<'a> Peek<'a> for Rule<'a> {
211	const PEEK_KINDSET: KindSet = KindSet::ANY;
212}
213
214impl<'a> Parse<'a> for Rule<'a> {
215	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
216	where
217		I: Iterator<Item = Cursor> + Clone,
218	{
219		Self::parse_rule_variants(p)
220	}
221}
222
223#[cfg(test)]
224mod tests {
225	use super::*;
226	use crate::CssAtomSet;
227	use css_parse::assert_parse;
228
229	#[test]
230	fn test_writes() {
231		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "body{}");
232		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "body{color:red;}");
233		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "body,tr:nth-child(n-1){}");
234		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "body{width:1px;}");
235		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "body{width:1px;}.a{width:2px;}");
236		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "one:1;a{two:2}");
237		assert_parse!(CssAtomSet::ATOMS, Rule, "@media screen{}", Rule::Media(_));
238		assert_parse!(CssAtomSet::ATOMS, Rule, "@layer foo{}", Rule::Layer(_));
239	}
240
241	#[test]
242	fn cdc_in_unknown_rule_block() {
243		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "={-->}");
244		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "={-->");
245	}
246
247	#[test]
248	fn stray_close_bracket_in_block() {
249		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "%{)");
250		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "t% {90)");
251		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "{--ty-2% {90)");
252		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "%{]");
253	}
254
255	#[test]
256	fn unknown_at_rule_ends_at_semicolon_or_block() {
257		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "@foo bar;@media screen{}", |sheet| {
258			assert!(matches!(sheet.rules[..], [Rule::UnknownAt(_), Rule::Media(_)]))
259		});
260		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "@foo bar{baz}@media screen{}", |sheet| {
261			assert!(matches!(sheet.rules[..], [Rule::UnknownAt(_), Rule::Media(_)]))
262		});
263		assert_parse!(CssAtomSet::ATOMS, StyleSheet, "a{@foo bar;color:red}", |sheet| {
264			assert!(matches!(sheet.rules[..], [Rule::Style(_)]))
265		});
266	}
267}