css_ast/
stylesheet.rs

1use bumpalo::collections::Vec;
2use css_parse::{
3	AtRule, Build, ComponentValues, Cursor, Parse, Parser, Peek, QualifiedRule, Result as ParserResult, RuleVariants,
4	StyleSheet as StyleSheetTrait, T, atkeyword_set, diagnostics,
5};
6use csskit_derives::{Parse, Peek, ToCursors, ToSpan, Visitable};
7
8use crate::{StyleValue, rules, stylerule::StyleRule};
9
10// https://drafts.csswg.org/cssom-1/#the-cssstylesheet-interface
11#[derive(ToSpan, ToCursors, Visitable, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(tag = "type", rename = "stylesheet"))]
13#[visit]
14pub struct StyleSheet<'a> {
15	pub rules: Vec<'a, Rule<'a>>,
16}
17
18// A StyleSheet represents the root node of a CSS-like language.
19// The StyleSheet trait represents an abstraction of this, which allows for
20// alternate implementations such as SCSS.
21// AtRules vs QualifiedRules are differentiated by two different functions.
22impl<'a> Parse<'a> for StyleSheet<'a> {
23	fn parse(p: &mut Parser<'a>) -> ParserResult<Self> {
24		Ok(Self { rules: Self::parse_stylesheet(p)? })
25	}
26}
27
28impl<'a> StyleSheetTrait<'a> for StyleSheet<'a> {
29	type Rule = Rule<'a>;
30}
31
32macro_rules! apply_rules {
33	($macro: ident) => {
34		$macro! {
35			Charset(CharsetRule): "charset",
36			ColorProfile(ColorProfileRule): "color-profile",
37			Container(ContainerRule<'a>): "container",
38			CounterStyle(CounterStyleRule): "counter-style",
39			FontFace(FontFaceRule<'a>): "font-face",
40			FontFeatureValues(FontFeatureValuesRule): "font-feature-values",
41			FontPaletteValues(FontPaletteValuesRule): "font-palette-values",
42			Import(ImportRule): "import",
43			Keyframes(KeyframesRule<'a>): "keyframes",
44			Layer(LayerRule<'a>): "layer",
45			Media(MediaRule<'a>): "media",
46			Namespace(NamespaceRule): "namespace",
47			Page(PageRule<'a>): "page",
48			Property(PropertyRule<'a>): "property",
49			Scope(ScopeRule): "scope",
50			StartingStyle(StartingStyleRule): "starting-style",
51			Supports(SupportsRule<'a>): "supports",
52
53			// Deprecated Rules
54			Document(DocumentRule<'a>): "document",
55
56			// Vendor Prefixed
57			WebkitKeyframes(WebkitKeyframesRule<'a>): "-webkit-keyframes",
58
59			// https://developer.mozilla.org/en-US/docs/Web/CSS/Mozilla_Extensions#at-rules
60			MozDocument(MozDocumentRule<'a>): "-moz-document",
61		}
62	};
63}
64
65#[derive(Parse, Peek, ToSpan, ToCursors, Visitable, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
67#[visit(self)]
68pub struct UnknownAtRule<'a>(AtRule<T![AtKeyword], ComponentValues<'a>, ComponentValues<'a>>);
69
70#[derive(Parse, Peek, ToSpan, ToCursors, Visitable, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
72#[visit(self)]
73pub struct UnknownQualifiedRule<'a>(QualifiedRule<'a, ComponentValues<'a>, StyleValue<'a>, ComponentValues<'a>>);
74
75macro_rules! rule {
76    ( $(
77        $name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
78    )+ ) => {
79		#[allow(clippy::large_enum_variant)] // TODO: Box?
80		// https://drafts.csswg.org/cssom-1/#the-cssrule-interface
81		#[derive(ToSpan, ToCursors, Visitable, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
82		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
83		pub enum Rule<'a> {
84			$(
85				$name(rules::$ty$(<$a>)?),
86			)+
87			UnknownAt(UnknownAtRule<'a>),
88			Style(StyleRule<'a>),
89			Unknown(UnknownQualifiedRule<'a>)
90		}
91	}
92}
93
94apply_rules!(rule);
95
96macro_rules! define_atkeyword_set {
97	( $(
98		$name:ident($ty:ty): $str:tt,
99	)+ ) => {
100		atkeyword_set!(
101			enum AtRuleKeywords {
102				$($name: $str),+
103			}
104		);
105	}
106}
107
108apply_rules!(define_atkeyword_set);
109
110impl<'a> RuleVariants<'a> for Rule<'a> {
111	fn parse_at_rule(p: &mut Parser<'a>, c: Cursor) -> ParserResult<Self> {
112		if !AtRuleKeywords::peek(p, c) {
113			Err(diagnostics::Unexpected(p.next()))?;
114		}
115		let kw = AtRuleKeywords::build(p, c);
116		macro_rules! parse_rule {
117			( $(
118				$name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
119			)+ ) => {
120				match kw {
121					$(AtRuleKeywords::$name(_) => p.parse::<rules::$ty>().map(Self::$name),)+
122				}
123			}
124		}
125		apply_rules!(parse_rule)
126	}
127
128	fn parse_unknown_at_rule(p: &mut Parser<'a>, _name: Cursor) -> ParserResult<Self> {
129		p.parse::<UnknownAtRule>().map(Self::UnknownAt)
130	}
131
132	fn parse_qualified_rule(p: &mut Parser<'a>, _name: Cursor) -> ParserResult<Self> {
133		p.parse::<StyleRule>().map(Self::Style)
134	}
135
136	fn parse_unknown_qualified_rule(p: &mut Parser<'a>, _name: Cursor) -> ParserResult<Self> {
137		p.parse::<UnknownQualifiedRule>().map(Self::Unknown)
138	}
139}
140
141impl<'a> Parse<'a> for Rule<'a> {
142	fn parse(p: &mut Parser<'a>) -> ParserResult<Self> {
143		Self::parse_rule_variants(p)
144	}
145}
146
147#[cfg(test)]
148mod tests {
149	use super::*;
150	use css_parse::assert_parse;
151
152	#[test]
153	fn size_test() {
154		assert_eq!(std::mem::size_of::<StyleSheet>(), 32);
155		assert_eq!(std::mem::size_of::<Rule>(), 512);
156	}
157
158	#[test]
159	fn test_writes() {
160		assert_parse!(StyleSheet, "body{}");
161		assert_parse!(StyleSheet, "body{color:red;}");
162		assert_parse!(StyleSheet, "body,tr:nth-child(n-1){}");
163		assert_parse!(StyleSheet, "body{width:1px;}");
164		assert_parse!(StyleSheet, "body{width:1px;}.a{width:2px;}");
165		assert_parse!(StyleSheet, "one:1;a{two:2}");
166		assert_parse!(Rule, "@media screen{}", Rule::Media(_));
167		assert_parse!(Rule, "@layer foo{}", Rule::Layer(_));
168	}
169}