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}