Skip to main content

css_parse/syntax/
comma_separated.rs

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