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