Skip to main content

css_ast/rules/
supports.rs

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