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(
49		p: &mut Parser<'a>,
50		atom: &'static dyn DynAtomSet,
51	) -> Result<(T!['('], T![Ident], Option<(T![:], Self::Value)>, T![')'])> {
52		let open = p.parse::<T!['(']>()?;
53		let ident = p.parse::<T![Ident]>()?;
54		let c: Cursor = ident.into();
55		if !p.equals_atom(c, atom) {
56			Err(Diagnostic::new(c, Diagnostic::unexpected_ident))?
57		}
58		if <T![:]>::peek(p, p.peek_n(1)) {
59			let colon = p.parse::<T![:]>()?;
60			let value = p.parse::<Self::Value>()?;
61			let close = p.parse::<T![')']>()?;
62			Ok((open, ident, Some((colon, value)), close))
63		} else {
64			let close = p.parse::<T![')']>()?;
65			Ok((open, ident, None, close))
66		}
67	}
68}
69
70/// This macro expands to define an enum which already implements [Parse][crate::Parse] and [DiscreteFeature], for a
71/// one-liner definition of a [DiscreteFeature].
72///
73/// # Example
74///
75/// ```
76/// use css_parse::*;
77/// use bumpalo::Bump;
78/// use csskit_derives::{ToCursors, ToSpan};
79/// use derive_atom_set::AtomSet;
80///
81/// // Your language atoms:
82/// #[derive(Debug, Default, Copy, Clone, AtomSet, PartialEq)]
83/// pub enum MyLangAtoms {
84///   #[default]
85///   _None,
86///   TestFeature,
87/// }
88/// impl MyLangAtoms {
89///   pub const ATOMS: MyLangAtoms = MyLangAtoms::_None;
90/// }
91///
92/// // Define the Discrete Feature.
93/// discrete_feature! {
94///     /// A discrete media feature: `(test-feature: big)`, `(test-feature: small)`
95///     #[derive(ToCursors, ToSpan, Debug)]
96///     pub enum TestFeature<MyLangAtoms::TestFeature, T![Ident]>
97/// }
98///
99/// // Test!
100/// assert_parse!(MyLangAtoms::ATOMS, TestFeature, "(test-feature)", TestFeature::Bare(_open, _ident, _close));
101/// assert_parse!(MyLangAtoms::ATOMS, TestFeature, "(test-feature:big)", TestFeature::WithValue(_open, _ident, _colon, _feature, _close));
102/// ```
103///
104#[macro_export]
105macro_rules! discrete_feature {
106	($(#[$meta:meta])* $vis:vis enum $feature: ident<$feature_name: path, $value: ty>) => {
107		$(#[$meta])*
108		$vis enum $feature {
109			WithValue($crate::T!['('], $crate::T![Ident], $crate::T![:], $value, $crate::T![')']),
110			Bare($crate::T!['('], $crate::T![Ident], $crate::T![')']),
111		}
112
113		impl<'a> $crate::Parse<'a> for $feature {
114			fn parse(p: &mut $crate::Parser<'a>) -> $crate::Result<Self> {
115				use $crate::DiscreteFeature;
116				let (open, ident, opt, close) = Self::parse_discrete_feature(p, &$feature_name)?;
117				if let Some((colon, value)) = opt {
118					Ok(Self::WithValue(open, ident, colon, value, close))
119				} else {
120					Ok(Self::Bare(open, ident, close))
121				}
122			}
123		}
124
125		impl<'a> $crate::DiscreteFeature<'a> for $feature {
126			type Value = $value;
127		}
128	};
129}