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/// This macro expands to define an enum which already implements [Parse] and [RangedFeature], for a one-liner
175/// definition of a [RangedFeature].
176///
177/// # Examples
178///
179/// ## No Legacy syntax
180///
181/// ```
182/// use css_parse::*;
183/// use csskit_derives::{ToCursors, ToSpan};
184/// use derive_atom_set::AtomSet;
185///
186/// #[derive(Debug, Default, AtomSet, Copy, Clone, PartialEq)]
187/// pub enum MyAtomSet {
188///   #[default]
189///   _None,
190///   Thing,
191///   MaxThing,
192///   MinThing,
193/// }
194/// impl MyAtomSet {
195///   const ATOMS: MyAtomSet = MyAtomSet::_None;
196/// }
197///
198/// // Define the Ranged Feature.
199/// ranged_feature! {
200///   /// A ranged media feature: (thing: 1), or (1 <= thing < 10)
201///   #[derive(ToCursors, ToSpan, Debug)]
202///   pub enum TestFeature{MyAtomSet::Thing, T![Number]}
203/// }
204///
205/// // Test!
206/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(thing:2)");
207/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(4<=thing>8)");
208/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(thing>=2)");
209///
210/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(max-thing>2)");
211/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(4<=max-thing<=8)");
212/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(max-thing:2)");
213/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(min-thing:2)");
214/// ```
215///
216/// ## With legacy syntax
217///
218/// ```
219/// use css_parse::*;
220/// use csskit_derives::*;
221/// use derive_atom_set::*;
222///
223/// #[derive(Debug, Default, AtomSet, Copy, Clone, PartialEq)]
224/// pub enum MyAtomSet {
225///   #[default]
226///   _None,
227///   Thing,
228///   MaxThing,
229///   MinThing,
230/// }
231/// impl MyAtomSet {
232///   const ATOMS: MyAtomSet = MyAtomSet::_None;
233/// }
234///
235/// // Define the Ranged Feature.
236/// ranged_feature! {
237///   /// A ranged media feature: (thing: 1), or (1 <= thing < 10)
238///   #[derive(Debug, ToCursors, ToSpan)]
239///   pub enum TestFeature{MyAtomSet::Thing | MyAtomSet::MinThing | MyAtomSet::MaxThing, T![Number]}
240/// }
241///
242/// // Test!
243/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(thing:2)");
244/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(4<=thing>8)");
245/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(thing>=2)");
246/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(max-thing:2)");
247/// assert_parse!(MyAtomSet::ATOMS, TestFeature, "(min-thing:2)");
248///
249/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(max-thing>2)");
250/// assert_parse_error!(MyAtomSet::ATOMS, TestFeature, "(4<=max-thing<=8)");
251/// ```
252#[macro_export]
253macro_rules! ranged_feature {
254	(@parse_call $p:ident, $feature_name:path) => {
255		Self::parse_ranged_feature($p, &$feature_name, None, None)
256	};
257	(@parse_call $p:ident, $feature_name:path, $min_name:path, $max_name:path) => {
258		Self::parse_ranged_feature($p, &$feature_name, Some(&$min_name), Some(&$max_name))
259	};
260	($(#[$meta:meta])* $vis:vis enum $feature: ident $(<$lt:lifetime>)? {$feature_name: path $(| $min_name: path | $max_name: path)?, $value: ty}) => {
261		$(#[$meta])*
262		$vis enum $feature $(<$lt>)? {
263			Left($crate::T!['('], T![Ident], $crate::Comparison, $value, $crate::T![')']),
264			Right($crate::T!['('], $value, $crate::Comparison, T![Ident], $crate::T![')']),
265			Range($crate::T!['('], $value, $crate::Comparison, T![Ident], $crate::Comparison, $value, $crate::T![')']),
266			$(
267				#[doc = stringify!($min_name)]
268				Min($crate::T!['('], T![Ident], $crate::T![:], $value, $crate::T![')']),
269				#[doc = stringify!($max_name)]
270				Max($crate::T!['('], T![Ident], $crate::T![:], $value, $crate::T![')']),
271			)?
272			Exact($crate::T!['('], T![Ident], $crate::T![:], $value, $crate::T![')']),
273		}
274
275		impl<'a> $crate::Peek<'a> for $feature $(<$lt>)? {
276			fn peek<Iter>(p: &$crate::Parser<'a, Iter>, c: $crate::Cursor) -> bool
277			where
278				Iter: Iterator<Item = $crate::Cursor> + Clone,
279			{
280				c == $crate::Kind::LeftParen && p.peek_n(2) == $crate::KindSet::new(&[$crate::Kind::Ident, $crate::Kind::Number, $crate::Kind::Dimension])
281			}
282		}
283
284		impl<'a> $crate::Parse<'a> for $feature $(<$lt>)? {
285			fn parse<I>(p: &mut $crate::Parser<'a, I>) -> $crate::Result<Self>
286			where
287				I: Iterator<Item = $crate::Cursor> + Clone,
288			{
289				use $crate::RangedFeature;
290				$crate::ranged_feature! {@parse_call p, $feature_name $(, $min_name, $max_name)?}
291			}
292		}
293
294		impl<'a> $crate::RangedFeature<'a> for $feature $(<$lt>)? {
295			type Value = $value;
296
297			$(
298				#[doc = stringify!($max_name)]
299				fn new_max(
300					open: $crate::T!['('],
301					ident: T![Ident],
302					colon: $crate::T![:],
303					value: Self::Value,
304					close: $crate::T![')'],
305				) -> $crate::Result<Self> {
306					Ok(Self::Max(open, ident, colon, value, close))
307				}
308
309				#[doc = stringify!($min_name)]
310				fn new_min(
311					open: $crate::T!['('],
312					ident: T![Ident],
313					colon: $crate::T![:],
314					value: Self::Value,
315					close: $crate::T![')'],
316				) -> $crate::Result<Self> {
317					Ok(Self::Min(open, ident, colon, value, close))
318				}
319			)?
320
321			fn new_exact(
322				open: $crate::T!['('],
323				ident: T![Ident],
324				colon: $crate::T![:],
325				value: Self::Value,
326				close: $crate::T![')'],
327			) -> $crate::Result<Self> {
328				Ok(Self::Exact(open, ident, colon, value, close))
329			}
330
331			fn new_left(
332				open: $crate::T!['('],
333				ident: T![Ident],
334				comparison: $crate::Comparison,
335				value: Self::Value,
336				close: $crate::T![')'],
337			) -> $crate::Result<Self> {
338				Ok(Self::Left(open, ident, comparison, value, close))
339			}
340
341			fn new_right(
342				open: $crate::T!['('],
343				value: Self::Value,
344				comparison: $crate::Comparison,
345				ident: T![Ident],
346				close: $crate::T![')'],
347			) -> $crate::Result<Self> {
348				Ok(Self::Right(open, value, comparison, ident, close))
349			}
350
351			fn new_ranged(
352				open: $crate::T!['('],
353				left: Self::Value,
354				left_comparison: $crate::Comparison,
355				ident: T![Ident],
356				right_comparison: $crate::Comparison,
357				value: Self::Value,
358				close: $crate::T![')'],
359			) -> $crate::Result<Self> {
360				Ok(Self::Range(open, left, left_comparison, ident, right_comparison, value, close))
361			}
362		}
363	};
364}