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}