Skip to main content

css_parse/traits/
ranged_feature.rs

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