css_ast/rules/
supports.rs

1use crate::{
2	StyleValue, Visit, VisitMut, Visitable as VisitableTrait, VisitableMut, selector::ComplexSelector, stylesheet::Rule,
3};
4use bumpalo::collections::Vec;
5use css_parse::{
6	AtRule, Build, ComponentValues, ConditionKeyword, Cursor, Declaration, FeatureConditionList, Parse, Parser,
7	Result as ParserResult, RuleList, T, atkeyword_set, diagnostics, function_set,
8};
9use csskit_derives::{Parse, Peek, ToCursors, ToSpan, Visitable};
10
11atkeyword_set!(pub struct AtSupportsKeyword "supports");
12
13///
14/// ```md
15/// <general-enclosed>
16///  │├─╮─ <function-token> ─╭─╮─ <any-value> ─╭─ ")" ─┤│
17///     ╰─ "(" ──────────────╯ ╰───────────────╯
18///
19///
20/// <supports-in-parens>
21///  │├─╮─ "(" ─ <supports-condition> ─ ")" ─╭──┤│
22///     ├─────── <supports-feature> ─────────┤
23///     ╰─────── <general-enclosed> ─────────╯
24///
25/// <supports-feature>
26///  │├─ <supports-decl> ──┤│
27///
28/// <supports-feature>
29///  │├─ "(" ─ <declaration> ─ ")" ─┤│
30///
31///
32/// <container-condition> = [ <container-name>? <container-query>? ]!
33/// <container-name> = <custom-ident>
34/// <container-query> = not <query-in-parens>
35///                   | <query-in-parens> [ [ and <query-in-parens> ]* | [ or <query-in-parens> ]* ]
36/// <query-in-parens> = ( <container-query> )
37///                   | ( <size-feature> )
38///                   | style( <style-query> )
39///                   | scroll-state( <scroll-state-query> )
40///                   | <general-enclosed>
41///
42/// <https://drafts.csswg.org/css-conditional-3/#at-supports>
43/// <https://drafts.csswg.org/css-conditional-3/#at-ruledef-supports>
44#[derive(Parse, Peek, ToSpan, ToCursors, Visitable, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
46#[cfg_attr(feature = "css_feature_data", derive(::csskit_derives::ToCSSFeature), css_feature("css.at-rules.property"))]
47#[visit]
48pub struct SupportsRule<'a>(pub AtRule<AtSupportsKeyword, SupportsCondition<'a>, SupportsRuleBlock<'a>>);
49
50#[derive(Parse, Peek, ToSpan, ToCursors, Visitable, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
52pub struct SupportsRuleBlock<'a>(RuleList<'a, Rule<'a>>);
53
54#[derive(ToSpan, ToCursors, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(tag = "type", content = "value"))]
56pub enum SupportsCondition<'a> {
57	Is(SupportsFeature<'a>),
58	Not(ConditionKeyword, SupportsFeature<'a>),
59	And(Vec<'a, (SupportsFeature<'a>, Option<ConditionKeyword>)>),
60	Or(Vec<'a, (SupportsFeature<'a>, Option<ConditionKeyword>)>),
61}
62
63impl<'a> FeatureConditionList<'a> for SupportsCondition<'a> {
64	type FeatureCondition = SupportsFeature<'a>;
65	fn build_is(feature: SupportsFeature<'a>) -> Self {
66		Self::Is(feature)
67	}
68	fn build_not(keyword: ConditionKeyword, feature: SupportsFeature<'a>) -> Self {
69		Self::Not(keyword, feature)
70	}
71	fn build_and(feature: Vec<'a, (SupportsFeature<'a>, Option<ConditionKeyword>)>) -> Self {
72		Self::And(feature)
73	}
74	fn build_or(feature: Vec<'a, (SupportsFeature<'a>, Option<ConditionKeyword>)>) -> Self {
75		Self::Or(feature)
76	}
77}
78
79impl<'a> Parse<'a> for SupportsCondition<'a> {
80	fn parse(p: &mut Parser<'a>) -> ParserResult<Self> {
81		if p.peek::<T![Function]>() || p.peek::<T!['(']>() {
82			return Ok(Self::Is(p.parse::<SupportsFeature>()?));
83		}
84		Self::parse_condition(p)
85	}
86}
87
88impl<'a> VisitableTrait for SupportsCondition<'a> {
89	fn accept<V: Visit>(&self, v: &mut V) {
90		match self {
91			Self::Is(feature) => feature.accept(v),
92			Self::Not(_, feature) => feature.accept(v),
93			Self::And(features) => {
94				for (feature, _) in features {
95					feature.accept(v);
96				}
97			}
98			Self::Or(features) => {
99				for (feature, _) in features {
100					feature.accept(v);
101				}
102			}
103		}
104	}
105}
106
107impl<'a> VisitableMut for SupportsCondition<'a> {
108	fn accept_mut<V: VisitMut>(&mut self, v: &mut V) {
109		match self {
110			Self::Is(feature) => feature.accept_mut(v),
111			Self::Not(_, feature) => feature.accept_mut(v),
112			Self::And(features) => {
113				for (feature, _) in features {
114					feature.accept_mut(v);
115				}
116			}
117			Self::Or(features) => {
118				for (feature, _) in features {
119					feature.accept_mut(v);
120				}
121			}
122		}
123	}
124}
125
126#[allow(clippy::large_enum_variant)] // TODO: Box?
127#[derive(ToCursors, ToSpan, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
129pub enum SupportsFeature<'a> {
130	FontTech(Option<T!['(']>, T![Function], ComponentValues<'a>, T![')'], Option<T![')']>),
131	FontFormat(Option<T!['(']>, T![Function], ComponentValues<'a>, T![')'], Option<T![')']>),
132	Selector(Option<T!['(']>, T![Function], ComplexSelector<'a>, T![')'], Option<T![')']>),
133	Property(T!['('], Declaration<'a, StyleValue<'a>>, Option<T![')']>),
134}
135
136function_set!(
137	pub enum SupportsFeatureKeyword {
138		FontTech: "font-tech",
139		FontFormat: "font-format",
140		Selector: "selector"
141	}
142);
143
144impl<'a> Parse<'a> for SupportsFeature<'a> {
145	fn parse(p: &mut Parser<'a>) -> ParserResult<Self> {
146		let open = p.parse_if_peek::<T!['(']>()?;
147		if p.peek::<T![Function]>() {
148			let keyword = p.parse::<SupportsFeatureKeyword>()?;
149			let c: Cursor = keyword.into();
150			let function = <T![Function]>::build(p, c);
151			match keyword {
152				SupportsFeatureKeyword::Selector(_) => {
153					let selector = p.parse::<ComplexSelector>()?;
154					// End function
155					let close = p.parse::<T![')']>()?;
156					let open_close = if open.is_some() { Some(p.parse::<T![')']>()?) } else { None };
157					Ok(Self::Selector(open, function, selector, close, open_close))
158				}
159				SupportsFeatureKeyword::FontTech(_) => {
160					todo!();
161				}
162				SupportsFeatureKeyword::FontFormat(_) => {
163					todo!();
164				}
165			}
166		} else if let Some(open) = open {
167			let property = p.parse::<Declaration<'a, StyleValue<'a>>>()?;
168			let close = p.parse_if_peek::<T![')']>()?;
169			Ok(Self::Property(open, property, close))
170		} else {
171			Err(diagnostics::Unexpected(p.next()))?
172		}
173	}
174}
175
176impl<'a> VisitableTrait for SupportsFeature<'a> {
177	fn accept<V: Visit>(&self, v: &mut V) {
178		match self {
179			Self::FontTech(_, _, _, _, _) => todo!(),
180			Self::FontFormat(_, _, _, _, _) => todo!(),
181			Self::Selector(_, _, selector, _, _) => selector.accept(v),
182			Self::Property(_, property, _) => property.accept(v),
183		}
184	}
185}
186
187impl<'a> VisitableMut for SupportsFeature<'a> {
188	fn accept_mut<V: VisitMut>(&mut self, v: &mut V) {
189		match self {
190			Self::FontTech(_, _, _, _, _) => todo!(),
191			Self::FontFormat(_, _, _, _, _) => todo!(),
192			Self::Selector(_, _, selector, _, _) => selector.accept_mut(v),
193			Self::Property(_, property, _) => property.accept_mut(v),
194		}
195	}
196}
197
198#[cfg(test)]
199mod tests {
200	use super::*;
201	use css_parse::assert_parse;
202
203	#[test]
204	fn size_test() {
205		assert_eq!(std::mem::size_of::<SupportsRule>(), 512);
206		assert_eq!(std::mem::size_of::<SupportsCondition>(), 416);
207		assert_eq!(std::mem::size_of::<SupportsRuleBlock>(), 64);
208	}
209
210	#[test]
211	fn test_writes() {
212		assert_parse!(SupportsRule, "@supports(color:black){}");
213		assert_parse!(SupportsRule, "@supports(width:1px){body{width:1px}}");
214		// assert_parse!(SupportsRule, "@supports not (width:1--foo){}");
215		// assert_parse!(SupportsRule, "@supports(width: 1--foo) or (width: 1foo) {\n\n}");
216		// assert_parse!(SupportsRule, "@supports(width: 1--foo) and (width: 1foo) {\n\n}");
217		// assert_parse!(SupportsRule, "@supports(width: 100vw) {\n\tbody {\n\t\twidth: 100vw;\n\t}\n}");
218		// assert_parse!(SupportsRule, "@supports not ((text-align-last: justify) or (-moz-text-align-last: justify)) {\n\n}");
219		// assert_parse!(SupportsRule, "@supports((position:-webkit-sticky)or (position:sticky)) {}");
220		// assert_parse!(SupportsRule, "@supports selector(h2 > p) {\n\n}");
221		// assert_parse!(SupportsRule, "@supports(selector(h2 > p)) {}", "@supports selector(h2 > p) {\n\n}");
222		// assert_parse!(SupportsRule, "@supports not selector(h2 > p) {\n\n}");
223		// assert_parse!(SupportsRule, "@supports not (selector(h2 > p)) {}", "@supports not selector(h2 > p) {\n\n}");
224	}
225}