css_parse/traits/
discrete_feature.rs

1use css_lexer::DynAtomSet;
2
3use crate::{Cursor, Diagnostic, Parse, Parser, Peek, Result, T};
4
5/// This trait provides an implementation for parsing a ["Media Feature" that has a discrete keyword][1]. This is
6/// complementary to the other media features: [BooleanFeature][crate::BooleanFeature] and
7/// [DiscreteFeature][crate::DiscreteFeature].
8///
9/// [1]: https://drafts.csswg.org/mediaqueries/#typedef-mf-plain
10///
11/// Rather than implementing this trait on an enum, use the [discrete_feature!][crate::discrete_feature] macro which
12/// expands to define the enum and necessary traits ([Parse], this trait, and [ToCursors][crate::ToCursors]) in a
13/// single macro call.
14///
15/// It does not implement [Parse], but provides `parse_discrete_feature(&mut Parser<'a>, name: &str) -> Result<Self>`,
16/// which can make for a trivial [Parse] implementation. The `name: &str` parameter refers to the `<feature-name>`
17/// token, which will be parsed as an Ident. The [DiscreteFeature::Value] type must be implemented, and defines the
18/// `<value>` portion.
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/// discrete features use the other two productions.
31///
32/// Given this, this trait parses as:
33///
34/// ```md
35/// <feature-name>
36///  │├─ <ident> ─┤│
37///
38/// <discrete-feature>
39///  │├─ "(" ─╮─ <feature-name> ─ ":" ─ <value> ─╭─ ")" ─┤│
40///           ╰─ <feature-name> ─────────────────╯
41///
42/// ```
43///
44pub trait DiscreteFeature<'a>: Sized {
45	type Value: Parse<'a>;
46
47	#[allow(clippy::type_complexity)]
48	fn parse_discrete_feature<I>(
49		p: &mut Parser<'a, I>,
50		atom: &'static dyn DynAtomSet,
51	) -> Result<(T!['('], T![Ident], Option<(T![:], Self::Value)>, 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, atom) {
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::<Self::Value>()?;
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 [DiscreteFeature], for a
74/// one-liner definition of a [DiscreteFeature].
75///
76/// # Example
77///
78/// ```
79/// use css_parse::*;
80/// use bumpalo::Bump;
81/// use csskit_derives::{ToCursors, ToSpan};
82/// use derive_atom_set::AtomSet;
83///
84/// // Your language atoms:
85/// #[derive(Debug, Default, Copy, Clone, AtomSet, PartialEq)]
86/// pub enum MyLangAtoms {
87///   #[default]
88///   _None,
89///   TestFeature,
90/// }
91/// impl MyLangAtoms {
92///   pub const ATOMS: MyLangAtoms = MyLangAtoms::_None;
93/// }
94///
95/// // Define the Discrete Feature.
96/// discrete_feature! {
97///     /// A discrete media feature: `(test-feature: big)`, `(test-feature: small)`
98///     #[derive(ToCursors, ToSpan, Debug)]
99///     pub enum TestFeature{MyLangAtoms::TestFeature, T![Ident]}
100/// }
101///
102/// // Test!
103/// assert_parse!(MyLangAtoms::ATOMS, TestFeature, "(test-feature)", TestFeature::Bare(_open, _ident, _close));
104/// assert_parse!(MyLangAtoms::ATOMS, TestFeature, "(test-feature:big)", TestFeature::WithValue(_open, _ident, _colon, _feature, _close));
105/// ```
106///
107#[macro_export]
108macro_rules! discrete_feature {
109	($(#[$meta:meta])* $vis:vis enum $feature: ident{$feature_name: path, $value: ty}) => {
110		$(#[$meta])*
111		$vis enum $feature {
112			WithValue($crate::T!['('], $crate::T![Ident], $crate::T![:], $value, $crate::T![')']),
113			Bare($crate::T!['('], $crate::T![Ident], $crate::T![')']),
114		}
115
116		impl<'a> $crate::Parse<'a> for $feature {
117			fn parse<I>(p: &mut $crate::Parser<'a, I>) -> $crate::Result<Self>
118			where
119				I: Iterator<Item = $crate::Cursor> + Clone,
120			{
121				use $crate::DiscreteFeature;
122				let (open, ident, opt, close) = Self::parse_discrete_feature(p, &$feature_name)?;
123				if let Some((colon, value)) = opt {
124					Ok(Self::WithValue(open, ident, colon, value, close))
125				} else {
126					Ok(Self::Bare(open, ident, close))
127				}
128			}
129		}
130
131		impl<'a> $crate::DiscreteFeature<'a> for $feature {
132			type Value = $value;
133		}
134	};
135}