Skip to main content

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}