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}