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}