Skip to main content

css_ast/functions/
if_function.rs

1use super::prelude::*;
2use crate::{CssMetadata, MediaCondition, StyleQuery, StyleValue, SupportsCondition};
3use css_parse::{
4	ComponentValues, Declaration, FeatureConditionList,
5	token_macros::{Colon, Semicolon},
6};
7
8/// A single `<if-test>` inside an [`IfCondition`] boolean expression.
9///
10/// <https://drafts.csswg.org/css-values-5/#typedef-if-test>
11///
12/// ```text,ignore
13/// <if-test> =
14///   supports( [ <ident> : <declaration-value> ] | <supports-condition> ) |
15///   media( <media-feature> | <media-condition> ) |
16///   style( <style-query> )
17/// ```
18#[node]
19#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
21#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
22#[derive(csskit_derives::NodeWithMetadata)]
23pub enum IfTest<'a> {
24	/// `supports( <ident> : <declaration-value> )`
25	SupportsDeclaration(
26		#[cfg_attr(feature = "visitable", visit(skip))] Function,
27		Box<'a, Declaration<'a, StyleValue<'a>, CssMetadata>>,
28		#[cfg_attr(feature = "visitable", visit(skip))]
29		#[semantic_eq(skip)]
30		RightParen,
31	),
32	/// `supports( <supports-condition> )`
33	Supports(
34		#[cfg_attr(feature = "visitable", visit(skip))] Function,
35		SupportsCondition<'a>,
36		#[cfg_attr(feature = "visitable", visit(skip))]
37		#[semantic_eq(skip)]
38		RightParen,
39	),
40	/// `media( <media-feature> | <media-condition> )`
41	Media(
42		#[cfg_attr(feature = "visitable", visit(skip))] Function,
43		MediaCondition<'a>,
44		#[cfg_attr(feature = "visitable", visit(skip))]
45		#[semantic_eq(skip)]
46		RightParen,
47	),
48	/// `style( <style-query> )`
49	Style(
50		#[cfg_attr(feature = "visitable", visit(skip))] Function,
51		StyleQuery<'a>,
52		#[cfg_attr(feature = "visitable", visit(skip))]
53		#[semantic_eq(skip)]
54		RightParen,
55	),
56	/// `( <boolean-expr[ <if-test> ]> )`
57	Group(
58		#[cfg_attr(feature = "visitable", visit(skip))]
59		#[semantic_eq(skip)]
60		LeftParen,
61		Box<'a, IfConditionExpr<'a>>,
62		#[cfg_attr(feature = "visitable", visit(skip))]
63		#[semantic_eq(skip)]
64		RightParen,
65	),
66	/// `<general-enclosed>`: forward-compatible unknown, preserved verbatim.
67	#[cfg_attr(feature = "visitable", visit(skip))]
68	GeneralEnclosed(
69		#[cfg_attr(feature = "visitable", visit(skip))]
70		#[semantic_eq(skip)]
71		Function,
72		ComponentValues<'a>,
73		#[cfg_attr(feature = "visitable", visit(skip))]
74		#[semantic_eq(skip)]
75		RightParen,
76	),
77}
78
79impl<'a> Peek<'a> for IfTest<'a> {
80	const PEEK_KINDSET: KindSet = KindSet::new(&[Kind::LeftParen, Kind::Function]);
81}
82
83impl<'a> Parse<'a> for IfTest<'a> {
84	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
85	where
86		I: Iterator<Item = Cursor> + Clone,
87	{
88		// `( <boolean-expr[ <if-test> ]> )` grouping branch.
89		if p.peek::<LeftParen>() {
90			let open = p.parse::<LeftParen>()?;
91			let expr = p.parse::<IfConditionExpr>()?;
92			let close = p.parse::<RightParen>()?;
93			return Ok(Self::Group(open, Box::new_in(p.alloc(), expr), close));
94		}
95		let function = p.parse::<Function>()?;
96		match p.to_atom::<CssAtomSet>(function.into()) {
97			CssAtomSet::Supports => {
98				// `supports( <ident> : <declaration-value> )` vs `supports( <supports-condition> )`.
99				// A bare declaration is an ident (or dashed-ident) directly followed by a colon; a
100				// `<supports-condition>` always begins with `(`, `not`, or a function.
101				let c = p.peek_n(1);
102				if c == Kind::Ident && p.peek_n(2) == Kind::Colon {
103					let decl = p.parse::<Declaration<'a, StyleValue<'a>, CssMetadata>>()?;
104					let close = p.parse::<RightParen>()?;
105					Ok(Self::SupportsDeclaration(function, Box::new_in(p.alloc(), decl), close))
106				} else {
107					let condition = p.parse::<SupportsCondition>()?;
108					let close = p.parse::<RightParen>()?;
109					Ok(Self::Supports(function, condition, close))
110				}
111			}
112			CssAtomSet::Media => {
113				let condition = p.parse::<MediaCondition>()?;
114				let close = p.parse::<RightParen>()?;
115				Ok(Self::Media(function, condition, close))
116			}
117			CssAtomSet::Style => {
118				let query = p.parse::<StyleQuery>()?;
119				let close = p.parse::<RightParen>()?;
120				Ok(Self::Style(function, query, close))
121			}
122			// `<general-enclosed>`: any other function is preserved verbatim for forward-compat.
123			_ => {
124				let values = p.parse::<ComponentValues>()?;
125				let close = p.parse::<RightParen>()?;
126				Ok(Self::GeneralEnclosed(function, values, close))
127			}
128		}
129	}
130}
131
132/// A `<boolean-expr[ <if-test> ]>`: one or more [`IfTest`]s combined with `not`/`and`/`or`.
133///
134/// <https://drafts.csswg.org/css-values-5/#typedef-boolean-expr>
135#[node]
136#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
138#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
139#[derive(csskit_derives::NodeWithMetadata)]
140pub enum IfConditionExpr<'a> {
141	Is(IfTest<'a>),
142	Not(Ident, IfTest<'a>),
143	#[cfg_attr(feature = "visitable", visit(skip))]
144	And(Vec<'a, (IfTest<'a>, Option<Ident>)>),
145	#[cfg_attr(feature = "visitable", visit(skip))]
146	Or(Vec<'a, (IfTest<'a>, Option<Ident>)>),
147}
148
149impl<'a> FeatureConditionList<'a> for IfConditionExpr<'a> {
150	type FeatureCondition = IfTest<'a>;
151	fn keyword_is_not<I>(p: &Parser<'a, I>, c: Cursor) -> bool
152	where
153		I: Iterator<Item = Cursor> + Clone,
154	{
155		p.equals_atom(c, &CssAtomSet::Not)
156	}
157	fn keyword_is_and<I>(p: &Parser<'a, I>, c: Cursor) -> bool
158	where
159		I: Iterator<Item = Cursor> + Clone,
160	{
161		p.equals_atom(c, &CssAtomSet::And)
162	}
163	fn keyword_is_or<I>(p: &Parser<'a, I>, c: Cursor) -> bool
164	where
165		I: Iterator<Item = Cursor> + Clone,
166	{
167		p.equals_atom(c, &CssAtomSet::Or)
168	}
169	fn build_is(feature: IfTest<'a>) -> Self {
170		Self::Is(feature)
171	}
172	fn build_not(keyword: Ident, feature: IfTest<'a>) -> Self {
173		Self::Not(keyword, feature)
174	}
175	fn build_and(features: Vec<'a, (IfTest<'a>, Option<Ident>)>) -> Self {
176		Self::And(features)
177	}
178	fn build_or(features: Vec<'a, (IfTest<'a>, Option<Ident>)>) -> Self {
179		Self::Or(features)
180	}
181}
182
183impl<'a> Peek<'a> for IfConditionExpr<'a> {
184	const PEEK_KINDSET: KindSet = KindSet::new(&[Kind::LeftParen, Kind::Function, Kind::Ident]);
185}
186
187impl<'a> Parse<'a> for IfConditionExpr<'a> {
188	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
189	where
190		I: Iterator<Item = Cursor> + Clone,
191	{
192		Self::parse_condition(p)
193	}
194}
195
196/// An `<if-condition>`: either a `<boolean-expr[ <if-test> ]>` or the `else` keyword (always true).
197///
198/// <https://drafts.csswg.org/css-values-5/#typedef-if-condition>
199#[node]
200#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
201#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
202#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
203#[derive(csskit_derives::NodeWithMetadata)]
204pub enum IfCondition<'a> {
205	Else(#[atom(CssAtomSet::Else)] Ident),
206	Expr(IfConditionExpr<'a>),
207}
208
209/// A single `<if-branch>`: `<if-condition> : <declaration-value>?`. The branch value `V` is the
210/// enclosing value slot (e.g. `Value<'a, T>`), so substitution functions inside it are preserved.
211///
212/// <https://drafts.csswg.org/css-values-5/#typedef-if-branch>
213#[node]
214#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
216#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
217#[derive(csskit_derives::NodeWithMetadata)]
218pub struct IfBranch<'a, V> {
219	pub condition: IfCondition<'a>,
220	#[semantic_eq(skip)]
221	pub colon: Colon,
222	pub value: Option<V>,
223}
224
225/// if() function: conditional CSS value selection.
226///
227/// <https://drafts.csswg.org/css-values-5/#if-notation>
228///
229/// ```text,ignore
230/// <if()> = if( [ <if-branch> ; ]* <if-branch> ;? )
231/// ```
232#[node]
233#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
234#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
235#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
236#[derive(csskit_derives::NodeWithMetadata)]
237#[metadata(declaration_kinds = Computed)]
238pub struct IfFunction<'a, V> {
239	#[cfg_attr(feature = "visitable", visit(skip))]
240	#[semantic_eq(skip)]
241	pub name: Function,
242	pub branches: Vec<'a, (IfBranch<'a, V>, Option<Semicolon>)>,
243	#[cfg_attr(feature = "visitable", visit(skip))]
244	#[semantic_eq(skip)]
245	pub close: RightParen,
246}
247
248impl<'a, V: Peek<'a>> Peek<'a> for IfFunction<'a, V> {
249	const PEEK_KINDSET: KindSet = KindSet::new(&[Kind::Function]);
250
251	#[inline(always)]
252	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
253	where
254		I: Iterator<Item = Cursor> + Clone,
255	{
256		<Function>::peek(p, c) && p.equals_atom(c, &CssAtomSet::If)
257	}
258}
259
260impl<'a, V: Parse<'a> + Peek<'a>> Parse<'a> for IfFunction<'a, V> {
261	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
262	where
263		I: Iterator<Item = Cursor> + Clone,
264	{
265		let name = p.parse::<Function>()?;
266		let mut branches = Vec::new_in(p.alloc());
267		loop {
268			let branch = p.parse::<IfBranch<'a, V>>()?;
269			let semicolon = p.parse_if_peek::<Semicolon>()?;
270			let had_semicolon = semicolon.is_some();
271			branches.push((branch, semicolon));
272			if !had_semicolon || p.peek::<RightParen>() {
273				break;
274			}
275		}
276		let close = p.parse::<RightParen>()?;
277		Ok(Self { name, branches, close })
278	}
279}
280
281#[cfg(test)]
282mod tests {
283	use super::*;
284	use crate::{CssAtomSet, Length, Value};
285	use css_parse::assert_parse;
286
287	type IfLength<'a> = IfFunction<'a, Value<'a, Length>>;
288
289	#[test]
290	fn test_if_function() {
291		assert_parse!(CssAtomSet::ATOMS, IfLength, "if(style(--x: 1px): 10px; else: 20px)");
292		assert_parse!(CssAtomSet::ATOMS, IfLength, "if(else: 1px)");
293		assert_parse!(CssAtomSet::ATOMS, IfLength, "if(supports(color: red): 1px)");
294		assert_parse!(CssAtomSet::ATOMS, IfLength, "if(media((width: 100px)): 1px)");
295		assert_parse!(CssAtomSet::ATOMS, IfLength, "if(supports(color: red) and style(--x: 1px): 1px)");
296		assert_parse!(CssAtomSet::ATOMS, IfLength, "if(not style(--x: 1px): 1px)");
297		assert_parse!(CssAtomSet::ATOMS, IfLength, "if(else: var(--fallback))");
298	}
299}