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