css_parse/traits/boolean_feature.rs
1use crate::Cursor;
2use crate::{Diagnostic, Parser, Peek, Result, T};
3use css_lexer::DynAtomSet;
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<I>(
50 p: &mut Parser<'a, I>,
51 name: &'static dyn DynAtomSet,
52 ) -> Result<(T!['('], T![Ident], Option<(T![:], T![Any])>, T![')'])>
53 where
54 I: Iterator<Item = Cursor> + Clone,
55 {
56 let open = p.parse::<T!['(']>()?;
57 let ident = p.parse::<T![Ident]>()?;
58 let c: Cursor = ident.into();
59 if !p.equals_atom(c, name) {
60 Err(Diagnostic::new(c, Diagnostic::unexpected_ident))?
61 }
62 if <T![:]>::peek(p, p.peek_n(1)) {
63 let colon = p.parse::<T![:]>()?;
64 let value = p.parse::<T![Any]>()?;
65 let close = p.parse::<T![')']>()?;
66 Ok((open, ident, Some((colon, value)), close))
67 } else {
68 let close = p.parse::<T![')']>()?;
69 Ok((open, ident, None, close))
70 }
71 }
72}
73
74/// This macro expands to define an enum which already implements [Parse][crate::Parse] and [BooleanFeature], for a
75/// one-liner definition of a [BooleanFeature].
76///
77/// # Example
78///
79/// ```
80/// use css_lexer::*;
81/// use css_parse::*;
82/// use csskit_derives::*;
83/// use derive_atom_set::*;
84/// use bumpalo::Bump;
85///
86/// #[derive(Debug, Default, AtomSet, Copy, Clone, PartialEq)]
87/// pub enum MyAtomSet {
88/// #[default]
89/// _None,
90/// TestFeature,
91/// }
92/// impl MyAtomSet {
93/// const ATOMS: MyAtomSet = MyAtomSet::_None;
94/// }
95///
96/// // Define the Boolean Feature.
97/// boolean_feature! {
98/// /// A boolean media feature: `(test-feature)`
99/// #[derive(ToCursors, ToSpan, Debug)]
100/// pub enum TestFeature{MyAtomSet::TestFeature}
101/// }
102///
103/// // Test!
104/// let allocator = Bump::new();
105/// let source_text = "(test-feature)";
106/// let lexer = Lexer::new( &MyAtomSet::ATOMS, &source_text);
107/// let mut p = Parser::new(&allocator, &source_text, lexer);
108/// let result = p.parse_entirely::<TestFeature>();
109/// assert!(matches!(result.output, Some(TestFeature::Bare(open, ident, close))));
110///
111/// let source_text = "(test-feature: none)";
112/// let lexer = Lexer::new(&MyAtomSet::ATOMS, &source_text);
113/// let mut p = Parser::new(&allocator, &source_text, lexer);
114/// let result = p.parse_entirely::<TestFeature>();
115/// assert!(matches!(result.output, Some(TestFeature::WithValue(open, ident, colon, any, close))));
116/// ```
117///
118#[macro_export]
119macro_rules! boolean_feature {
120 ($(#[$meta:meta])* $vis:vis enum $feature: ident{$feature_name: path}) => {
121 $(#[$meta])*
122 $vis enum $feature {
123 WithValue($crate::T!['('], $crate::T![Ident], $crate::T![:], $crate::T![Any], $crate::T![')']),
124 Bare($crate::T!['('], $crate::T![Ident], $crate::T![')']),
125 }
126
127 impl<'a> $crate::Parse<'a> for $feature {
128 fn parse<I>(p: &mut $crate::Parser<'a, I>) -> $crate::Result<Self>
129 where
130 I: Iterator<Item = $crate::Cursor> + Clone,
131 {
132 use $crate::BooleanFeature;
133 let (open, ident, opt, close) = Self::parse_boolean_feature(p, &$feature_name)?;
134 if let Some((colon, number)) = opt {
135 Ok(Self::WithValue(open, ident, colon, number, close))
136 } else {
137 Ok(Self::Bare(open, ident, close))
138 }
139 }
140 }
141
142 impl<'a> $crate::BooleanFeature<'a> for $feature {}
143 };
144}