Skip to main content

css_parse/traits/
boolean_feature.rs

1use super::prelude::*;
2use atom_set::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 atom_set::AtomSet;
81/// use css_parse::*;
82/// use csskit_derives::*;
83/// use derive_atom_set::*;
84///
85/// #[derive(Debug, Default, AtomSet, Copy, Clone, PartialEq)]
86/// pub enum MyAtomSet {
87///   #[default]
88///   _None,
89///   TestFeature,
90/// }
91/// impl MyAtomSet {
92///   const ATOMS: MyAtomSet = MyAtomSet::_None;
93/// }
94///
95/// // Define the Boolean Feature.
96/// boolean_feature! {
97///     /// A boolean media feature: `(test-feature)`
98///     #[derive(ToCursors, ToSpan, Debug)]
99///     pub enum TestFeature{MyAtomSet::TestFeature}
100/// }
101///
102/// // Test!
103/// let allocator = Arena::new();
104/// let source_text = "(test-feature)";
105/// let lexer = Lexer::new( &MyAtomSet::ATOMS, &source_text);
106/// let mut p = Parser::new(&allocator, &source_text, lexer);
107/// let result = p.parse_entirely::<TestFeature>();
108/// assert!(matches!(result.output, Some(TestFeature::Bare(open, ident, close))));
109///
110/// let source_text = "(test-feature: none)";
111/// let lexer = Lexer::new(&MyAtomSet::ATOMS, &source_text);
112/// let mut p = Parser::new(&allocator, &source_text, lexer);
113/// let result = p.parse_entirely::<TestFeature>();
114/// assert!(matches!(result.output, Some(TestFeature::WithValue(open, ident, colon, any, close))));
115/// ```
116///
117#[macro_export]
118macro_rules! boolean_feature {
119	($(#[$meta:meta])* $vis:vis enum $feature: ident{$feature_name: path}) => {
120		$(#[$meta])*
121		$vis enum $feature {
122			WithValue($crate::T!['('], $crate::T![Ident], $crate::T![:], $crate::T![Any], $crate::T![')']),
123			Bare($crate::T!['('], $crate::T![Ident], $crate::T![')']),
124		}
125
126		impl<'a> $crate::Peek<'a> for $feature {
127			fn peek<Iter>(p: &$crate::Parser<'a, Iter>, c: $crate::Cursor) -> bool
128			where
129				Iter: Iterator<Item = $crate::Cursor> + Clone,
130			{
131				c == $crate::Kind::LeftParen && p.peek_n(2) == $crate::Kind::Ident
132			}
133		}
134
135		impl<'a> $crate::Parse<'a> for $feature {
136			fn parse<I>(p: &mut $crate::Parser<'a, I>) -> $crate::Result<Self>
137			where
138				I: Iterator<Item = $crate::Cursor> + Clone,
139			{
140				use $crate::BooleanFeature;
141				let (open, ident, opt, close) = Self::parse_boolean_feature(p, &$feature_name)?;
142				if let Some((colon, number)) = opt {
143					Ok(Self::WithValue(open, ident, colon, number, close))
144				} else {
145					Ok(Self::Bare(open, ident, close))
146				}
147			}
148		}
149
150		impl<'a> $crate::BooleanFeature<'a> for $feature {}
151	};
152}