Skip to main content

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}