css_parse/traits/
discrete_feature.rs

1use crate::Cursor;
2
3use crate::{Parser, Result, T, diagnostics};
4
5use super::Parse;
6
7/// This trait provides an implementation for parsing a ["Media Feature" that has a discrete keyword][1]. This is
8/// complementary to the other media features: [BooleanFeature][crate::BooleanFeature] and
9/// [DiscreteFeature][crate::DiscreteFeature].
10///
11/// [1]: https://drafts.csswg.org/mediaqueries/#typedef-mf-plain
12///
13/// Rather than implementing this trait on an enum, use the [discrete_feature!][crate::discrete_feature] macro which
14/// expands to define the enum and necessary traits ([Parse], this trait, and [ToCursors][crate::ToCursors]) in a
15/// single macro call.
16///
17/// It does not implement [Parse], but provides `parse_discrere_feature(&mut Parser<'a>, name: &str) -> Result<Self>`,
18/// which can make for a trivial [Parse] implementation. The `name: &str` parameter refers to the `<feature-name>`
19/// token, which will be parsed as an Ident. The [DiscreteFeature::Value] type must be implemented, and defines the
20/// `<value>` portion. Usually [DiscreteFeature::Value] can be easily defined using
21/// [keyword_set!][crate::keyword_set].
22///
23/// CSS defines the Media Feature generally as:
24///
25/// ```md
26///  │├─ "(" ─╮─ <feature-name> ─ ":" ─ <value> ─╭─ ")" ─┤│
27///           ├─ <feature-name> ─────────────────┤
28///           ╰─ <ranged-feature> ───────────────╯
29///
30/// ```
31///
32/// The [RangedFeature][crate::RangedFeature] trait provides algorithms for parsing `<ranged-feature>` productions, but
33/// discrete features use the other two productions.
34///
35/// Given this, this trait parses as:
36///
37/// ```md
38/// <feature-name>
39///  │├─ <ident> ─┤│
40///
41/// <discrete-feature>
42///  │├─ "(" ─╮─ <feature-name> ─ ":" ─ <value> ─╭─ ")" ─┤│
43///           ╰─ <feature-name> ─────────────────╯
44///
45/// ```
46///
47pub trait DiscreteFeature<'a>: Sized {
48	type Value: Parse<'a>;
49
50	#[allow(clippy::type_complexity)] // TODO: simplify types
51	fn parse_descrete_feature(
52		p: &mut Parser<'a>,
53		name: &'static str,
54	) -> Result<(T!['('], T![Ident], Option<(T![:], Self::Value)>, T![')'])> {
55		let open = p.parse::<T!['(']>()?;
56		let ident = p.parse::<T![Ident]>()?;
57		let c: Cursor = ident.into();
58		if !p.eq_ignore_ascii_case(c, name) {
59			Err(diagnostics::ExpectedIdentOf(name, p.parse_str(c).into(), c))?
60		}
61		if p.peek::<T![:]>() {
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///
82/// keyword_set!(
83///     /// A keyword that defines text-feature options
84///     pub enum FeatureKeywords {
85///         Big: "big",
86///         Small: "small",
87///     }
88/// );
89///
90/// // Define the Discrete Feature.
91/// discrete_feature! {
92///     /// A discrete media feature: `(test-feature: big)`, `(test-feature: small)`
93///     pub enum TestFeature<"test-feature", FeatureKeywords>
94/// }
95///
96/// // Test!
97/// let allocator = Bump::new();
98/// let mut p = Parser::new(&allocator, "(test-feature)");
99/// let result = p.parse_entirely::<TestFeature>();
100/// assert!(matches!(result.output, Some(TestFeature::Bare(open, ident, close))));
101///
102/// let mut p = Parser::new(&allocator, "(test-feature: big)");
103/// let result = p.parse_entirely::<TestFeature>();
104/// assert!(matches!(result.output, Some(TestFeature::WithValue(open, ident, colon, any, close))))
105/// ```
106///
107#[macro_export]
108macro_rules! discrete_feature {
109	($(#[$meta:meta])* $vis:vis enum $feature: ident<$feature_name: tt, $value: ty>) => {
110		$(#[$meta])*
111		#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
112		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
113		$vis enum $feature {
114			WithValue($crate::T!['('], $crate::T![Ident], $crate::T![:], $value, $crate::T![')']),
115			Bare($crate::T!['('], $crate::T![Ident], $crate::T![')']),
116		}
117
118		impl $crate::ToCursors for $feature {
119			fn to_cursors(&self, s: &mut impl $crate::CursorSink) {
120			use $crate::ToCursors;
121				match self {
122					Self::WithValue(a, b, c, d, e) => {
123						ToCursors::to_cursors(a, s);
124						ToCursors::to_cursors(b, s);
125						ToCursors::to_cursors(c, s);
126						ToCursors::to_cursors(d, s);
127						ToCursors::to_cursors(e, s);
128					},
129					Self::Bare(a, b, c) => {
130						ToCursors::to_cursors(a, s);
131						ToCursors::to_cursors(b, s);
132						ToCursors::to_cursors(c, s);
133					}
134				}
135			}
136		}
137
138		impl $crate::ToSpan for $feature {
139			fn to_span(&self) -> $crate::Span {
140				match self {
141					Self::WithValue(start, _, _, _, end) => start.to_span() + end.to_span(),
142					Self::Bare(start, _, end) => start.to_span() + end.to_span(),
143				}
144			}
145		}
146
147		impl<'a> $crate::Parse<'a> for $feature {
148			fn parse(p: &mut $crate::Parser<'a>) -> $crate::Result<Self> {
149				use $crate::DiscreteFeature;
150				let (open, ident, opt, close) = Self::parse_descrete_feature(p, $feature_name)?;
151				if let Some((colon, value)) = opt {
152					Ok(Self::WithValue(open, ident, colon, value, close))
153				} else {
154					Ok(Self::Bare(open, ident, close))
155				}
156			}
157		}
158
159		impl<'a> $crate::DiscreteFeature<'a> for $feature {
160			type Value = $value;
161		}
162	};
163}