Skip to main content

css_parse/syntax/
comma_separated.rs

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