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}