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}