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}