css_ast/rules/
supports.rs

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