css_parse/traits/
ranged_feature.rs

1use crate::{AtomSet, Comparison, Diagnostic, Parse, Parser, Peek, Result as ParserResult, T};
2
3/// This trait provides an implementation for parsing a ["Media Feature" in the "Range" context][1].
4///
5/// [1]: https://drafts.csswg.org/mediaqueries/#range-context
6///
7/// Rather than implementing this trait on an enum, use the [ranged_feature!][crate::ranged_feature] macro which
8/// expands to define the enum and necessary traits ([Parse], this trait, and [ToCursors][crate::ToCursors]) in a
9/// single macro call.
10///
11/// It does not implement [Parse], but provides `parse_ranged_feature(&mut Parser<'a>) -> Result<Self>`, which can make
12/// for a trivial [Parse] implementation. The type  [Self::Value] represents the `<value>` token(s). The grammar of both
13/// `<value>` isn't mandated by this spec but is very likely a `Dimension` or `Number`. The `<feature-name>` is
14/// determined by the three given arguments to `parse_ranged_feature` - each must implement AtomSet, so they can be
15/// compared to the given ident atom in that position. Passing the third and fourth arguments for min & max atoms allows
16/// the "legacy" min/max variants to be parsed also.
17///
18/// [2]: https://drafts.csswg.org/mediaqueries/#mq-min-max
19///
20/// CSS defines the Media Feature in Ranged context as:
21///
22/// ```md
23///                                           ╭─ "="  ─╮
24///                                           ├─ "<"  ─┤
25///                                           ├─ "<=" ─┤
26///                                           ├─ ">"  ─┤
27///  │├─ "(" ─╮─ [<feature-name> or <value>] ─╯─ ">=" ─╰─ [<feature-name> or <value>] ─╭─ ")" ─┤│
28///           ├────── <value> ─╮─ "<"  ─╭── <feature-name> ─╮─ "<"  ─╭── <value> ──────┤
29///           │                ╰─ "<=" ─╯                   ╰─ "<=" ─╯                 │
30///           ╰────── <value> ─╮─ ">"  ─╭── <feature-name> ─╮─ ">"  ─╭── <value> ──────╯
31///                            ╰─ ">=" ─╯                   ╰─ ">=" ─╯
32///
33/// ```
34///
35/// This trait deviates slightly from the CSS spec ever so slightly for a few reasons:
36///
37/// - It uses a `<comparison>` token to represent each of the comparison operators, implemented as [Comparison]. This
38///   makes for much more convenient parsing and subsequent analyses.
39/// - The CSS defined railroad diagram doesn't quite fully convey that `<value> <comparison> <value>` and
40///   `<feature-name> <comparison> <feature-name>` are not valid productions. This trait will fail to parse such
41///   productions, as do all existing implementations of CSS (i.e browsers).
42/// - It does not do the extra validation to ensure a left/right comparison are "directionally equivalent" - in other
43///   words `<value> "<=" <feature-name> "=>" <value>` is a valid production in this trait - this allows for ASTs to
44///   factor in error tolerance. If an AST node wishes to be strict, it can check the comparators inside of
45///   [RangedFeature::new_ranged] and return an [Err] there.
46/// - It supports the "Legacy" modes which are defined for certain ranged media features. These legacy productions use
47///   a colon token and typically have `min` and `max` variants. For example `width: 1024px` is equivalent to
48///   `width >= 1024px`, while `max-width: 1024px` is equivalent to `max-width <= 1024px`. If an AST node wishes to
49///   _not_ support legacy feature-names, it can supply `None`s to [RangedFeature::parse_ranged_feature].
50///
51/// Given the above differences, the trait `RangedFeature` parses a grammar defined as:
52///
53/// ```md
54/// <comparison>
55///  │├──╮─ "="  ─╭──┤│
56///      ├─ "<"  ─┤
57///      ├─ "<=" ─┤
58///      ├─ ">"  ─┤
59///      ╰─ ">=" ─╯
60///
61/// <ranged-feature-trait>
62///  │├─ "(" ─╮─ <feature-name> ─ <comparison> ─ <value> ─────────────────────────────────╭─ ")" ─┤│
63///           ├─ <value> ─ <comparison> ─ <ranged-feautre-name> ──────────────────────────┤
64///           ├─ <value> ─ <comparison> ─ <ranged-feature-name> ─ <comparison> ─ <value> ─┤
65///           ╰─ <feature-name> ─ ":" ─ <value> ──────────────────────────────────────────╯
66///
67/// ```
68///
69pub trait RangedFeature<'a>: Sized {
70	type Value: Parse<'a>;
71
72	/// Method for constructing a "legacy max" media feature. Legacy features always include a colon token.
73	fn new_max(
74		_open: T!['('],
75		name: T![Ident],
76		_colon: T![:],
77		_value: Self::Value,
78		_close: T![')'],
79	) -> ParserResult<Self> {
80		Err(Diagnostic::new(name.into(), Diagnostic::unexpected_ident))?
81	}
82
83	/// Method for constructing a "legacy min" media feature. Legacy features always include a colon token.
84	fn new_min(
85		_open: T!['('],
86		name: T![Ident],
87		_colon: T![:],
88		_value: Self::Value,
89		_close: T![')'],
90	) -> ParserResult<Self> {
91		Err(Diagnostic::new(name.into(), Diagnostic::unexpected_ident))?
92	}
93
94	/// Method for constructing a "exact" media feature. Exact features always include a colon token.
95	fn new_exact(
96		open: T!['('],
97		name: T![Ident],
98		colon: T![:],
99		value: Self::Value,
100		close: T![')'],
101	) -> ParserResult<Self>;
102
103	/// Method for constructing a "left" media feature. This method is called when the parsed tokens encountered
104	/// the `<value>` token before the `<feature-name>`.
105	fn new_left(
106		open: T!['('],
107		name: T![Ident],
108		comparison: Comparison,
109		value: Self::Value,
110		close: T![')'],
111	) -> ParserResult<Self>;
112
113	/// Method for constructing a "right" media feature. This method is called when the parsed tokens
114	/// encountered the `<feature-name>` token before the `<value>`.
115	fn new_right(
116		open: T!['('],
117		value: Self::Value,
118		comparison: Comparison,
119		name: T![Ident],
120		close: T![')'],
121	) -> ParserResult<Self>;
122
123	/// Method for constructing a "ranged" media feature. This method is called when the parsed tokens
124	/// encountered the `<value>` token, followed by a `<comparison>`, followed by a `<feature-name>`, followed by a
125	/// `<comparison>` followed lastly by a `<value>`.
126	fn new_ranged(
127		open: T!['('],
128		left: Self::Value,
129		left_comparison: Comparison,
130		name: T![Ident],
131		right_comparison: Comparison,
132		value: Self::Value,
133		close: T![')'],
134	) -> ParserResult<Self>;
135
136	fn parse_ranged_feature<I, A: AtomSet + PartialEq>(
137		p: &mut Parser<'a, I>,
138		name: &A,
139		min: Option<&A>,
140		max: Option<&A>,
141	) -> ParserResult<Self>
142	where
143		I: Iterator<Item = crate::Cursor> + Clone,
144	{
145		let open = p.parse::<T!['(']>()?;
146		let c = p.peek_n(1);
147		if <T![Ident]>::peek(p, c) {
148			let atom = p.to_atom::<A>(c);
149			let ident = p.parse::<T![Ident]>()?;
150			if <T![:]>::peek(p, p.peek_n(1)) {
151				let colon = p.parse::<T![:]>()?;
152				let value = p.parse::<Self::Value>()?;
153				let close = p.parse::<T![')']>()?;
154				if &atom == name {
155					return Self::new_exact(open, ident, colon, value, close);
156				} else if min.is_some_and(|min| &atom == min) {
157					return Self::new_min(open, ident, colon, value, close);
158				} else if max.is_some_and(|max| &atom == max) {
159					return Self::new_max(open, ident, colon, value, close);
160				} else {
161					Err(Diagnostic::new(c, Diagnostic::unexpected_ident))?
162				}
163			}
164			if &atom != name {
165				Err(Diagnostic::new(c, Diagnostic::unexpected_ident))?
166			}
167			let comparison = p.parse::<Comparison>()?;
168			let value = p.parse::<Self::Value>()?;
169			let close = p.parse::<T![')']>()?;
170			return Self::new_left(open, ident, comparison, value, close);
171		}
172
173		let left = p.parse::<Self::Value>()?;
174		let left_comparison = p.parse::<Comparison>()?;
175		let c = p.peek_n(1);
176		let ident = p.parse::<T![Ident]>()?;
177		if &p.to_atom::<A>(ident.into()) != name {
178			Err(Diagnostic::new(c, Diagnostic::unexpected))?
179		}
180		if !<T![Delim]>::peek(p, p.peek_n(1)) {
181			let close = p.parse::<T![')']>()?;
182			return Self::new_right(open, left, left_comparison, ident, close);
183		}
184		let right_comparison = p.parse::<Comparison>()?;
185		let right = p.parse::<Self::Value>()?;
186		let close = p.parse::<T![')']>()?;
187		Self::new_ranged(open, left, left_comparison, ident, right_comparison, right, close)
188	}
189}
190
191/// This macro expands to define an enum which already implements [Parse] and [RangedFeature], for a one-liner
192/// definition of a [RangedFeature].
193///
194/// # Examples
195///
196/// ## No Legacy syntax
197///
198/// ```
199/// use css_parse::*;
200/// use bumpalo::Bump;
201/// use csskit_derives::{ToCursors, ToSpan};
202/// use derive_atom_set::AtomSet;
203///
204/// #[derive(Debug, Default, AtomSet, Copy, Clone, PartialEq)]
205/// pub enum MyAtomSet {
206///   #[default]
207///   _None,
208///   Thing,
209///   MaxThing,
210///   MinThing,
211/// }
212/// impl MyAtomSet {
213///   const ATOMS: MyAtomSet = MyAtomSet::_None;
214/// }
215///
216/// // Define the Ranged Feature.
217/// ranged_feature! {
218///   /// A ranged media feature: (thing: 1), or (1 <= thing < 10)
219///   #[derive(ToCursors, ToSpan, Debug)]
220///   pub enum TestFeature{MyAtomSet::Thing, T![Number]}
221/// }
222///
223/// // Test!
224/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(thing:2)");
225/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(4<=thing>8)");
226/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(thing>=2)");
227///
228/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(max-thing>2)");
229/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(4<=max-thing<=8)");
230/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(max-thing:2)");
231/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(min-thing:2)");
232/// ```
233///
234/// ## With legacy syntax
235///
236/// ```
237/// use css_parse::*;
238/// use csskit_derives::*;
239/// use derive_atom_set::*;
240/// use bumpalo::Bump;
241///
242/// #[derive(Debug, Default, AtomSet, Copy, Clone, PartialEq)]
243/// pub enum MyAtomSet {
244///   #[default]
245///   _None,
246///   Thing,
247///   MaxThing,
248///   MinThing,
249/// }
250/// impl MyAtomSet {
251///   const ATOMS: MyAtomSet = MyAtomSet::_None;
252/// }
253///
254/// // Define the Ranged Feature.
255/// ranged_feature! {
256///   /// A ranged media feature: (thing: 1), or (1 <= thing < 10)
257///   #[derive(Debug, ToCursors, ToSpan)]
258///   pub enum TestFeature{MyAtomSet::Thing | MyAtomSet::MinThing | MyAtomSet::MaxThing, T![Number]}
259/// }
260///
261/// // Test!
262/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(thing:2)");
263/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(4<=thing>8)");
264/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(thing>=2)");
265/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(max-thing:2)");
266/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(min-thing:2)");
267///
268/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(max-thing>2)");
269/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(4<=max-thing<=8)");
270/// ```
271#[macro_export]
272macro_rules! ranged_feature {
273	(@parse_call $p:ident, $feature_name:path) => {
274		Self::parse_ranged_feature($p, &$feature_name, None, None)
275	};
276	(@parse_call $p:ident, $feature_name:path, $min_name:path, $max_name:path) => {
277		Self::parse_ranged_feature($p, &$feature_name, Some(&$min_name), Some(&$max_name))
278	};
279	($(#[$meta:meta])* $vis:vis enum $feature: ident{$feature_name: path $(| $min_name: path | $max_name: path)?, $value: ty}) => {
280		$(#[$meta])*
281		$vis enum $feature {
282			Left($crate::T!['('], T![Ident], $crate::Comparison, $value, $crate::T![')']),
283			Right($crate::T!['('], $value, $crate::Comparison, T![Ident], $crate::T![')']),
284			Range($crate::T!['('], $value, $crate::Comparison, T![Ident], $crate::Comparison, $value, $crate::T![')']),
285			$(
286				#[doc = stringify!($min_name)]
287				Min($crate::T!['('], T![Ident], $crate::T![:], $value, $crate::T![')']),
288				#[doc = stringify!($max_name)]
289				Max($crate::T!['('], T![Ident], $crate::T![:], $value, $crate::T![')']),
290			)?
291			Exact($crate::T!['('], T![Ident], $crate::T![:], $value, $crate::T![')']),
292		}
293
294		impl<'a> $crate::Parse<'a> for $feature {
295			fn parse<I>(p: &mut $crate::Parser<'a, I>) -> $crate::Result<Self>
296			where
297				I: Iterator<Item = $crate::Cursor> + Clone,
298			{
299				use $crate::RangedFeature;
300				$crate::ranged_feature! {@parse_call p, $feature_name $(, $min_name, $max_name)?}
301			}
302		}
303
304		impl<'a> $crate::RangedFeature<'a> for $feature {
305			type Value = $value;
306
307			$(
308				#[doc = stringify!($max_name)]
309				fn new_max(
310					open: $crate::T!['('],
311					ident: T![Ident],
312					colon: $crate::T![:],
313					value: Self::Value,
314					close: $crate::T![')'],
315				) -> $crate::Result<Self> {
316					Ok(Self::Max(open, ident, colon, value, close))
317				}
318
319				#[doc = stringify!($min_name)]
320				fn new_min(
321					open: $crate::T!['('],
322					ident: T![Ident],
323					colon: $crate::T![:],
324					value: Self::Value,
325					close: $crate::T![')'],
326				) -> $crate::Result<Self> {
327					Ok(Self::Min(open, ident, colon, value, close))
328				}
329			)?
330
331			fn new_exact(
332				open: $crate::T!['('],
333				ident: T![Ident],
334				colon: $crate::T![:],
335				value: Self::Value,
336				close: $crate::T![')'],
337			) -> $crate::Result<Self> {
338				Ok(Self::Exact(open, ident, colon, value, close))
339			}
340
341			fn new_left(
342				open: $crate::T!['('],
343				ident: T![Ident],
344				comparison: $crate::Comparison,
345				value: Self::Value,
346				close: $crate::T![')'],
347			) -> $crate::Result<Self> {
348				Ok(Self::Left(open, ident, comparison, value, close))
349			}
350
351			fn new_right(
352				open: $crate::T!['('],
353				value: Self::Value,
354				comparison: $crate::Comparison,
355				ident: T![Ident],
356				close: $crate::T![')'],
357			) -> $crate::Result<Self> {
358				Ok(Self::Right(open, value, comparison, ident, close))
359			}
360
361			fn new_ranged(
362				open: $crate::T!['('],
363				left: Self::Value,
364				left_comparison: $crate::Comparison,
365				ident: T![Ident],
366				right_comparison: $crate::Comparison,
367				value: Self::Value,
368				close: $crate::T![')'],
369			) -> $crate::Result<Self> {
370				Ok(Self::Range(open, left, left_comparison, ident, right_comparison, value, close))
371			}
372		}
373	};
374}