Skip to main content

css_parse/syntax/
comma_separated.rs

1use crate::{
2	Arena, Cursor, CursorSink, IntoIter, KindSet, Parse, Parser, Peek, Result as ParserResult, SemanticEq, Span,
3	ToCursors, ToSpan, Vec, token_macros::Comma,
4};
5use csskit_proc_macro::node;
6use std::{
7	ops::{Index, IndexMut},
8	slice::{Iter, IterMut},
9};
10
11/// This is a generic type that can be used for AST nodes representing multiple multiple items separated with commas.
12///
13/// This can be used for any grammar which defines a Comma Separated group (`[]#`).
14///
15/// The given `<T>` will be parsed first, followed by a comma. Parsing completes if the comma isn't found.
16///
17/// As `<T>` is parsed first, it can have any number of interior commas, however if T should ideally not consume
18/// trailing commas, as doing so would likely mean only a single T in this struct.
19///
20/// The effective grammar for this struct is:
21///
22/// ```md
23/// <comma-separated>
24///  │├─╭─ <T> ─╮─ "," ─╭─┤│
25///     │       ╰───────╯
26///     ╰───────╯
27/// ```
28///
29/// [1]: https://drafts.csswg.org/css-syntax-3/#typedef-at-rule-list
30#[node]
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
33pub struct CommaSeparated<'a, T, const MIN: usize = 1> {
34	items: Vec<'a, (T, Option<Comma>)>,
35}
36
37impl<'a, T, const MIN: usize> CommaSeparated<'a, T, MIN> {
38	pub fn new_in(alloc: &'a Arena) -> Self {
39		Self { items: Vec::new_in(alloc) }
40	}
41
42	pub fn is_empty(&self) -> bool {
43		self.items.is_empty()
44	}
45
46	pub fn len(&self) -> usize {
47		self.items.len()
48	}
49}
50
51impl<'a, T: Peek<'a>, const MIN: usize> Peek<'a> for CommaSeparated<'a, T, MIN> {
52	const PEEK_KINDSET: KindSet = T::PEEK_KINDSET;
53
54	#[inline(always)]
55	fn peek<Iter>(p: &Parser<'a, Iter>, c: Cursor) -> bool
56	where
57		Iter: Iterator<Item = crate::Cursor> + Clone,
58	{
59		T::peek(p, c)
60	}
61}
62
63impl<'a, T: Parse<'a> + Peek<'a>, const MIN: usize> Parse<'a> for CommaSeparated<'a, T, MIN> {
64	fn parse<Iter>(p: &mut Parser<'a, Iter>) -> ParserResult<Self>
65	where
66		Iter: Iterator<Item = crate::Cursor> + Clone,
67	{
68		let mut items = Self::new_in(p.alloc());
69		if MIN == 0 && !<T>::peek(p, p.peek_n(1)) {
70			return Ok(items);
71		}
72		loop {
73			let item = p.parse::<T>()?;
74			if p.peek::<Comma>() {
75				let checkpoint = p.checkpoint();
76				let comma = p.parse::<Comma>()?;
77				if !<T>::peek(p, p.peek_n(1)) {
78					p.rewind(checkpoint);
79					items.items.push((item, None));
80					break;
81				}
82				items.items.push((item, Some(comma)));
83			} else {
84				items.items.push((item, None));
85				break;
86			}
87		}
88		if MIN > items.len() {
89			p.parse::<Comma>()?;
90		}
91		Ok(items)
92	}
93}
94
95impl<'a, T: ToCursors, const MIN: usize> ToCursors for CommaSeparated<'a, T, MIN> {
96	fn to_cursors(&self, s: &mut impl CursorSink) {
97		ToCursors::to_cursors(&self.items, s);
98	}
99}
100
101impl<'a, T: ToSpan, const MIN: usize> ToSpan for CommaSeparated<'a, T, MIN> {
102	fn to_span(&self) -> Span {
103		let first = self.items[0].to_span();
104		first + self.items.last().map(|t| t.to_span()).unwrap_or(first)
105	}
106}
107
108impl<'a, T: SemanticEq, const MIN: usize> SemanticEq for CommaSeparated<'a, T, MIN> {
109	fn semantic_eq(&self, other: &Self) -> bool {
110		self.items.semantic_eq(&other.items)
111	}
112}
113
114impl<'a, T: 'a, const MIN: usize> IntoIterator for CommaSeparated<'a, T, MIN> {
115	type Item = (T, Option<Comma>);
116	type IntoIter = IntoIter<'a, Self::Item>;
117
118	fn into_iter(self) -> Self::IntoIter {
119		self.items.into_iter()
120	}
121}
122
123impl<'a, 'b, T, const MIN: usize> IntoIterator for &'b CommaSeparated<'a, T, MIN> {
124	type Item = &'b (T, Option<Comma>);
125	type IntoIter = Iter<'b, (T, Option<Comma>)>;
126
127	fn into_iter(self) -> Self::IntoIter {
128		self.items.iter()
129	}
130}
131
132impl<'a, 'b, T, const MIN: usize> IntoIterator for &'b mut CommaSeparated<'a, T, MIN> {
133	type Item = &'b mut (T, Option<Comma>);
134	type IntoIter = IterMut<'b, (T, Option<Comma>)>;
135
136	fn into_iter(self) -> Self::IntoIter {
137		self.items.iter_mut()
138	}
139}
140
141impl<'a, T, I, const MIN: usize> Index<I> for CommaSeparated<'a, T, MIN>
142where
143	I: ::core::slice::SliceIndex<[(T, Option<Comma>)]>,
144{
145	type Output = I::Output;
146
147	#[inline]
148	fn index(&self, index: I) -> &Self::Output {
149		Index::index(&self.items, index)
150	}
151}
152
153impl<'a, T, I, const MIN: usize> IndexMut<I> for CommaSeparated<'a, T, MIN>
154where
155	I: ::core::slice::SliceIndex<[(T, Option<Comma>)]>,
156{
157	#[inline]
158	fn index_mut(&mut self, index: I) -> &mut Self::Output {
159		IndexMut::index_mut(&mut self.items, index)
160	}
161}
162
163#[cfg(test)]
164mod tests {
165	use super::*;
166	use crate::{EmptyAtomSet, T, test_helpers::*};
167
168	#[test]
169	fn test_writes() {
170		assert_parse!(EmptyAtomSet::ATOMS, CommaSeparated<T![Ident]>, "foo");
171		assert_parse!(EmptyAtomSet::ATOMS, CommaSeparated<T![Ident]>, "one,two");
172		assert_parse!(EmptyAtomSet::ATOMS, CommaSeparated<T![Ident]>, "one,two,three");
173		assert_parse!(EmptyAtomSet::ATOMS, CommaSeparated<(T![Number], CommaSeparated<T![Ident]>)>, "1 foo, 2 bar");
174	}
175
176	#[test]
177	fn test_spans() {
178		assert_parse_span!(
179			EmptyAtomSet::ATOMS,
180			CommaSeparated<T![Ident]>,
181			r#"
182			foo bar
183			^^^
184		"#
185		);
186		assert_parse_span!(
187			EmptyAtomSet::ATOMS,
188			CommaSeparated<T![Ident]>,
189			r#"
190			foo, bar, baz 1
191			^^^^^^^^^^^^^
192		"#
193		);
194	}
195
196	#[test]
197	fn test_peek() {
198		assert_peek_false!(EmptyAtomSet::ATOMS, CommaSeparated<T![Ident]>, "");
199		assert_peek_false!(EmptyAtomSet::ATOMS, CommaSeparated<T![Ident]>, ",");
200	}
201
202	#[test]
203	fn test_errors() {
204		assert_parse_error!(EmptyAtomSet::ATOMS, CommaSeparated<T![Ident]>, "one,two,three,");
205		assert_parse_error!(EmptyAtomSet::ATOMS, CommaSeparated<T![Ident]>, "one two");
206		assert_parse_error!(EmptyAtomSet::ATOMS, CommaSeparated<T![Ident], 2>, "one");
207		assert_parse_error!(EmptyAtomSet::ATOMS, CommaSeparated<T![Ident], 3>, "one, two");
208	}
209}