css_parse/traits/ranged_feature.rs
1use crate::{Comparison, Parse, Parser, Peek, Result, T, diagnostics};
2
3pub trait RangedFeatureKeyword {
4 fn is_legacy(&self) -> bool {
5 false
6 }
7}
8
9/// This trait provides an implementation for parsing a ["Media Feature" in the "Range" context][1].
10///
11/// [1]: https://drafts.csswg.org/mediaqueries/#range-context
12///
13/// Rather than implementing this trait on an enum, use the [ranged_feature!][crate::ranged_feature] macro which
14/// expands to define the enum and necessary traits ([Parse], this trait, and [ToCursors][crate::ToCursors]) in a
15/// single macro call.
16///
17/// It does not implement [Parse], but provides `parse_ranged_feature(&mut Parser<'a>) -> Result<Self>`, which can make
18/// for a trivial [Parse] implementation. The type [Self::FeatureName] must be defined, and represents the
19/// `<feature-name>` token(s), while [Self::Value] represents the `<value>` token(s). The grammar of both `<value>` and
20/// `<feature-name>` aren't mandated by this spec but are very likely be an `Ident` for the `<feature-name>` and either
21/// a `Dimension` or `Number` for the `<value>` portion. [Self::FeatureName] must also implement the
22/// [crate::RangedFeatureKeyword] trait which provides [RangedFeatureKeyword::is_legacy] to determine if the
23/// `<feature-name>` is unambiguously a legacy "min-" or "max-" prefixed name, for ["legacy" ranged media conditions][2].
24///
25/// [2]: https://drafts.csswg.org/mediaqueries/#mq-min-max
26///
27/// CSS defines the Media Feature in Ranged context as:
28///
29/// ```md
30/// ╭─ "=" ─╮
31/// ├─ "<" ─┤
32/// ├─ "<=" ─┤
33/// ├─ ">" ─┤
34/// │├─ "(" ─╮─ [<feature-name> or <value>] ─╯─ ">=" ─╰─ [<feature-name> or <value>] ─╭─ ")" ─┤│
35/// ├────── <value> ─╮─ "<" ─╭── <feature-name> ─╮─ "<" ─╭── <value> ──────┤
36/// │ ╰─ "<=" ─╯ ╰─ "<=" ─╯ │
37/// ╰────── <value> ─╮─ ">" ─╭── <feature-name> ─╮─ ">" ─╭── <value> ──────╯
38/// ╰─ ">=" ─╯ ╰─ ">=" ─╯
39///
40/// ```
41///
42/// This trait deviates slightly from the CSS spec ever so slightly for a few reasons:
43///
44/// - It uses a `<comparison>` token to represent each of the comparison operators, implemented as [Comparison]. This
45/// makes for much more convenient parsing and subsequent analyses.
46/// - The CSS defined railroad diagram doesn't quite fully convey that `<value> <comparison> <value>` and
47/// `<feature-name> <comparison> <feature-name>` are not valid productions. This trait will fail to parse such
48/// productions, as do all existing implementations of CSS (i.e browsers).
49/// - It does not do the extra validation to ensure a left/right comparison are "directionally equivalent" - in other
50/// words `<value> "<=" <feature-name> "=>" <value>` is a valid production in this trait - this allows for ASTs to
51/// factor in error tolerance. If an AST node wishes to be strict, it can check the comparators inside of
52/// [RangedFeature::new_ranged] and return an [Err] there.
53/// - It supports the "Legacy" modes which are defined for certain ranged media features. These legacy productions use
54/// a colon token and typically have `min` and `max` variants of the [RangedFeature::FeatureName]. For example
55/// `width: 1024px` is equivalent to `width >= 1024px`, while `max-width: 1024px` is equivalent to
56/// `max-width <= 1024px`. If an AST node wishes to _not_ support legacy feature-names, it can return an [Err] in
57/// [RangedFeature::new_legacy].
58///
59/// Given the above differences, the trait `RangedFeature` parses a grammar defined as:
60///
61/// ```md
62/// <comparison>
63/// │├──╮─ "=" ─╭──┤│
64/// ├─ "<" ─┤
65/// ├─ "<=" ─┤
66/// ├─ ">" ─┤
67/// ╰─ ">=" ─╯
68///
69/// <ranged-feature-trait>
70/// │├─ "(" ─╮─ <feature-name> ─ <comparison> ─ <value> ─────────────────────────────────╭─ ")" ─┤│
71/// ├─ <value> ─ <comparison> ─ <ranged-feautre-name> ──────────────────────────┤
72/// ├─ <value> ─ <comparison> ─ <ranged-feature-name> ─ <comparison> ─ <value> ─┤
73/// ╰─ <feature-name> ─ ":" ─ <value> ──────────────────────────────────────────╯
74///
75/// ```
76///
77pub trait RangedFeature<'a>: Sized {
78 type Value: Parse<'a>;
79 type FeatureName: Peek<'a> + Parse<'a> + RangedFeatureKeyword;
80
81 /// Method for constructing a "legacy" media feature. Legacy features always include a colon token.
82 fn new_legacy(
83 open: T!['('],
84 name: Self::FeatureName,
85 colon: T![:],
86 value: Self::Value,
87 close: T![')'],
88 ) -> Result<Self>;
89
90 /// Method for constructing a "left" media feature. This method is called when the parsed tokens encountered
91 /// the `<value>` token before the `<feature-name>`.
92 fn new_left(
93 open: T!['('],
94 name: Self::FeatureName,
95 comparison: Comparison,
96 value: Self::Value,
97 close: T![')'],
98 ) -> Result<Self>;
99
100 /// Method for constructing a "right" media feature. This method is called when the parsed tokens
101 /// encountered the `<feature-name>` token before the `<value>`.
102 fn new_right(
103 open: T!['('],
104 value: Self::Value,
105 comparison: Comparison,
106 name: Self::FeatureName,
107 close: T![')'],
108 ) -> Result<Self>;
109
110 /// Method for constructing a "ranged" media feature. This method is called when the parsed tokens
111 /// encountered the `<value>` token, followed by a `<comparison>`, followed by a `<feature-name>`, followed by a
112 /// `<comparison>` followed lastly by a `<value>`.
113 fn new_ranged(
114 open: T!['('],
115 left: Self::Value,
116 left_comparison: Comparison,
117 name: Self::FeatureName,
118 right_comparison: Comparison,
119 value: Self::Value,
120 close: T![')'],
121 ) -> Result<Self>;
122
123 fn parse_ranged_feature(p: &mut Parser<'a>) -> Result<Self> {
124 let open = p.parse::<T!['(']>()?;
125 let c = p.peek_next();
126 if let Some(name) = p.parse_if_peek::<Self::FeatureName>()? {
127 if p.peek::<T![:]>() {
128 let colon = p.parse::<T![:]>()?;
129 let value = p.parse::<Self::Value>()?;
130 let close = p.parse::<T![')']>()?;
131 return Self::new_legacy(open, name, colon, value, close);
132 } else if name.is_legacy() {
133 let source_cursor = p.to_source_cursor(c);
134 Err(diagnostics::UnexpectedIdent(source_cursor.to_string(), c))?
135 }
136 let comparison = p.parse::<Comparison>()?;
137 let value = p.parse::<Self::Value>()?;
138 let close = p.parse::<T![')']>()?;
139 return Self::new_left(open, name, comparison, value, close);
140 }
141
142 let left = p.parse::<Self::Value>()?;
143 let left_comparison = p.parse::<Comparison>()?;
144 let c = p.peek_next();
145 let name = p.parse::<Self::FeatureName>()?;
146 if name.is_legacy() {
147 Err(diagnostics::Unexpected(c))?
148 }
149 if !p.peek::<T![Delim]>() {
150 let close = p.parse::<T![')']>()?;
151 return Self::new_right(open, left, left_comparison, name, close);
152 }
153 let right_comparison = p.parse::<Comparison>()?;
154 let right = p.parse::<Self::Value>()?;
155 let close = p.parse::<T![')']>()?;
156 Self::new_ranged(open, left, left_comparison, name, right_comparison, right, close)
157 }
158}
159
160/// This macro expands to define an enum which already implements [Parse] and [RangedFeature], for a one-liner
161/// definition of a [RangedFeature].
162///
163/// # Example
164///
165/// ```
166/// use css_parse::*;
167/// use bumpalo::Bump;
168///
169/// // Defined the "FeatureName"
170/// keyword_set!(pub enum TestKeyword { Thing: "thing", MaxThing: "max-thing", MinThing: "min-thing" });
171/// impl RangedFeatureKeyword for TestKeyword {
172/// fn is_legacy(&self) -> bool {
173/// matches!(self, Self::MaxThing(_) | Self::MinThing(_))
174/// }
175/// }
176///
177/// // Define the Ranged Feature.
178/// ranged_feature! {
179/// /// A ranged media feature: (thing: 1), or (1 <= thing < 10)
180/// pub enum TestFeature<TestKeyword, T![Number]>
181/// }
182///
183/// // Test!
184/// assert_parse!(TestFeature, "(thing:2)");
185/// assert_parse!(TestFeature, "(max-thing:2)");
186/// assert_parse!(TestFeature, "(min-thing:2)");
187/// assert_parse!(TestFeature, "(4<=thing>8)");
188/// assert_parse!(TestFeature, "(thing>=2)");
189///
190/// assert_parse_error!(TestFeature, "(max-thing>2)");
191/// assert_parse_error!(TestFeature, "(4<=max-thing<=8)");
192/// ```
193///
194#[macro_export]
195macro_rules! ranged_feature {
196 ($(#[$meta:meta])* $vis:vis enum $feature: ident<$feature_name: ty, $value: ty>) => {
197 $(#[$meta])*
198 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
199 #[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
200 $vis enum $feature {
201 Left($crate::T!['('], $feature_name, $crate::Comparison, $value, $crate::T![')']),
202 Right($crate::T!['('], $value, $crate::Comparison, $feature_name, $crate::T![')']),
203 Range($crate::T!['('], $value, $crate::Comparison, $feature_name, $crate::Comparison, $value, $crate::T![')']),
204 Legacy($crate::T!['('], $feature_name, $crate::T![:], $value, $crate::T![')']),
205 }
206
207 impl $crate::ToCursors for $feature {
208 fn to_cursors(&self, s: &mut impl $crate::CursorSink) {
209 use $crate::ToCursors;
210 match self {
211 Self::Left(a, b, c, d, e) => {
212 ToCursors::to_cursors(a, s);
213 ToCursors::to_cursors(b, s);
214 ToCursors::to_cursors(c, s);
215 ToCursors::to_cursors(d, s);
216 ToCursors::to_cursors(e, s);
217 },
218 Self::Right(a, b, c, d, e) => {
219 ToCursors::to_cursors(a, s);
220 ToCursors::to_cursors(b, s);
221 ToCursors::to_cursors(c, s);
222 ToCursors::to_cursors(d, s);
223 ToCursors::to_cursors(e, s);
224 }
225 Self::Range(a, b, c, d, e, f, g) => {
226 ToCursors::to_cursors(a, s);
227 ToCursors::to_cursors(b, s);
228 ToCursors::to_cursors(c, s);
229 ToCursors::to_cursors(d, s);
230 ToCursors::to_cursors(e, s);
231 ToCursors::to_cursors(f, s);
232 ToCursors::to_cursors(g, s);
233 }
234 Self::Legacy(a, b, c, d, e) => {
235 ToCursors::to_cursors(a, s);
236 ToCursors::to_cursors(b, s);
237 ToCursors::to_cursors(c, s);
238 ToCursors::to_cursors(d, s);
239 ToCursors::to_cursors(e, s);
240 }
241 }
242 }
243 }
244
245 impl $crate::ToSpan for $feature {
246 fn to_span(&self) -> $crate::Span {
247 match self {
248 Self::Left(start, _, _, _, end) => start.to_span() + end.to_span(),
249 Self::Right(start, _, _, _, end) => start.to_span() + end.to_span(),
250 Self::Range(start, _, _, _, _, _, end) => start.to_span() + end.to_span(),
251 Self::Legacy(start, _, _, _, end) => start.to_span() + end.to_span(),
252 }
253 }
254 }
255
256 impl<'a> $crate::Parse<'a> for $feature {
257 fn parse(p: &mut $crate::Parser<'a>) -> $crate::Result<Self> {
258 use $crate::RangedFeature;
259 Self::parse_ranged_feature(p)
260 }
261 }
262
263 impl<'a> $crate::RangedFeature<'a> for $feature {
264 type Value = $value;
265 type FeatureName = $feature_name;
266
267 fn new_legacy(
268 open: $crate::T!['('],
269 ident: Self::FeatureName,
270 colon: $crate::T![:],
271 value: Self::Value,
272 close: $crate::T![')'],
273 ) -> $crate::Result<Self> {
274 Ok(Self::Legacy(open, ident, colon, value, close))
275 }
276
277 fn new_left(
278 open: $crate::T!['('],
279 ident: Self::FeatureName,
280 comparison: $crate::Comparison,
281 value: Self::Value,
282 close: $crate::T![')'],
283 ) -> $crate::Result<Self> {
284 Ok(Self::Left(open, ident, comparison, value, close))
285 }
286
287 fn new_right(
288 open: $crate::T!['('],
289 value: Self::Value,
290 comparison: $crate::Comparison,
291 ident: Self::FeatureName,
292 close: $crate::T![')'],
293 ) -> $crate::Result<Self> {
294 Ok(Self::Right(open, value, comparison, ident, close))
295 }
296
297 fn new_ranged(
298 open: $crate::T!['('],
299 left: Self::Value,
300 left_comparison: $crate::Comparison,
301 ident: Self::FeatureName,
302 right_comparison: $crate::Comparison,
303 value: Self::Value,
304 close: $crate::T![')'],
305 ) -> $crate::Result<Self> {
306 Ok(Self::Range(open, left, left_comparison, ident, right_comparison, value, close))
307 }
308 }
309 };
310}