css_parse/traits/
boolean_feature.rs

1use crate::Cursor;
2
3use crate::{Parser, Result, T, diagnostics};
4
5/// This trait provides an implementation for parsing a ["Media Feature" in the "Boolean" context][1]. This is
6/// complementary to the other media features: [RangedFeature][crate::RangedFeature] and
7/// [DiscreteFeature][crate::DiscreteFeature].
8///
9/// [1]: https://drafts.csswg.org/mediaqueries/#boolean-context
10///
11/// Rather than implementing this trait on an enum, use the [boolean_feature!][crate::boolean_feature] macro which
12/// expands to define the enum and necessary traits ([Parse][crate::Parse], this trait, and
13/// [ToCursors][crate::ToCursors]) in a single macro call.
14///
15/// It does not implement [Parse][crate::Parse], but provides
16/// `parse_boolean_feature(&mut Parser<'a>, name: &str) -> Result<Self>`, which can make for a trivial
17/// [Parse][crate::Parse] implementation. The `name: &str` parameter refers to the `<feature-name>` token, which will
18/// be parsed as an Ident.
19///
20/// CSS defines the Media Feature generally as:
21///
22/// ```md
23///  │├─ "(" ─╮─ <feature-name> ─ ":" ─ <value> ─╭─ ")" ─┤│
24///           ├─ <feature-name> ─────────────────┤
25///           ╰─ <ranged-feature> ───────────────╯
26///
27/// ```
28///
29/// The [RangedFeature][crate::RangedFeature] trait provides algorithms for parsing `<ranged-feature>` productions, but
30/// boolean features use the other two productions, with some rules around the `<value>`.
31///
32/// A boolean media query:
33///
34/// - Can omit the the `:` and `<value>`.
35/// - Must allow any token as the `<value>`, but the `<dimension>` of `0`, `<number>` of `0` and `<ident>` of `none`
36///   will mean the query evaluates to false.
37///
38/// Given these, this trait parses as:
39///
40/// ```md
41/// <boolean-feature>
42///  │├─ "(" ─╮─ <feature-name> ─ ":" ─ <any> ─╭─ ")" ─┤│
43///           ╰─ <feature-name> ───────────────╯
44///
45/// ```
46///
47pub trait BooleanFeature<'a>: Sized {
48	#[allow(clippy::type_complexity)] // TODO: simplify types
49	fn parse_boolean_feature(
50		p: &mut Parser<'a>,
51		name: &'static str,
52	) -> Result<(T!['('], T![Ident], Option<(T![:], T![Any])>, T![')'])> {
53		let open = p.parse::<T!['(']>()?;
54		let ident = p.parse::<T![Ident]>()?;
55		let c: Cursor = ident.into();
56		if !p.eq_ignore_ascii_case(c, name) {
57			Err(diagnostics::ExpectedIdentOf(name, p.parse_str(c).into(), c))?
58		}
59		if p.peek::<T![:]>() {
60			let colon = p.parse::<T![:]>()?;
61			let value = p.parse::<T![Any]>()?;
62			let close = p.parse::<T![')']>()?;
63			Ok((open, ident, Some((colon, value)), close))
64		} else {
65			let close = p.parse::<T![')']>()?;
66			Ok((open, ident, None, close))
67		}
68	}
69}
70
71/// This macro expands to define an enum which already implements [Parse][crate::Parse] and [BooleanFeature], for a
72/// one-liner definition of a [BooleanFeature].
73///
74/// # Example
75///
76/// ```
77/// use css_parse::*;
78/// use bumpalo::Bump;
79///
80/// // Define the Boolean Feature.
81/// boolean_feature! {
82///     /// A boolean media feature: `(test-feature)`
83///     pub enum TestFeature<"test-feature">
84/// }
85///
86/// // Test!
87/// let allocator = Bump::new();
88/// let mut p = Parser::new(&allocator, "(test-feature)");
89/// let result = p.parse_entirely::<TestFeature>();
90/// assert!(matches!(result.output, Some(TestFeature::Bare(open, ident, close))));
91///
92/// let mut p = Parser::new(&allocator, "(test-feature: none)");
93/// let result = p.parse_entirely::<TestFeature>();
94/// assert!(matches!(result.output, Some(TestFeature::WithValue(open, ident, colon, any, close))));
95/// ```
96///
97#[macro_export]
98macro_rules! boolean_feature {
99	($(#[$meta:meta])* $vis:vis enum $feature: ident<$feature_name: tt>) => {
100		$(#[$meta])*
101		#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
102		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
103		$vis enum $feature {
104			WithValue($crate::T!['('], $crate::T![Ident], $crate::T![:], $crate::T![Any], $crate::T![')']),
105			Bare($crate::T!['('], $crate::T![Ident], $crate::T![')']),
106		}
107
108		impl $crate::ToCursors for $feature {
109			fn to_cursors(&self, s: &mut impl $crate::CursorSink) {
110			use $crate::ToCursors;
111				match self {
112					Self::WithValue(a, b, c, d, e) => {
113						ToCursors::to_cursors(a, s);
114						ToCursors::to_cursors(b, s);
115						ToCursors::to_cursors(c, s);
116						ToCursors::to_cursors(d, s);
117						ToCursors::to_cursors(e, s);
118					},
119					Self::Bare(a, b, c) => {
120						ToCursors::to_cursors(a, s);
121						ToCursors::to_cursors(b, s);
122						ToCursors::to_cursors(c, s);
123					}
124				}
125			}
126		}
127
128		impl $crate::ToSpan for $feature {
129			fn to_span(&self) -> $crate::Span {
130				match self {
131					Self::WithValue(start, _, _, _, end) => start.to_span() + end.to_span(),
132					Self::Bare(start, _, end) => start.to_span() + end.to_span(),
133				}
134			}
135		}
136
137		impl<'a> $crate::Parse<'a> for $feature {
138			fn parse(p: &mut $crate::Parser<'a>) -> $crate::Result<Self> {
139				use $crate::BooleanFeature;
140				let (open, ident, opt, close) = Self::parse_boolean_feature(p, $feature_name)?;
141				if let Some((colon, number)) = opt {
142					Ok(Self::WithValue(open, ident, colon, number, close))
143				} else {
144					Ok(Self::Bare(open, ident, close))
145				}
146			}
147		}
148
149		impl<'a> $crate::BooleanFeature<'a> for $feature {}
150	};
151}