Skip to main content

css_ast/rules/
supports.rs

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