Skip to main content

css_parse/syntax/
block.rs

1use crate::{
2	BadDeclaration, CursorSink, Declaration, DeclarationGroup, DeclarationOrBad, DeclarationValue, Kind, KindSet,
3	NodeMetadata, NodeWithMetadata, Parse, Parser, Peek, Result, RuleVariants, SemanticEq, Span, State, T, ToCursors,
4	ToSpan, Vec, token_macros,
5};
6use csskit_proc_macro::node;
7
8/// This trait provides an implementation for ["consuming a blocks contents"][1].
9///
10/// ```md
11/// <block>
12///
13///  │├─ "{" ─╭──╮─╭─ <ws-*> ─╮─╭─╮─╭─ ";" ─╮─╭─╮─ <R> ─╭─╮─ "}" ─┤│
14///           │  │ ╰──────────╯ │ │ ╰───────╯ │ ├─ <D> ─┤ │
15///           │  ╰──────────────╯ ╰───────────╯ ╰───────╯ │
16///           ╰───────────────────────────────────────────╯
17/// ```
18///
19/// [1]: https://drafts.csswg.org/css-syntax-3/#consume-block-contents
20#[node]
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize))]
23#[cfg_attr(feature = "serde", serde(bound(serialize = "D: serde::Serialize, R: serde::Serialize")))]
24pub struct Block<'a, D, R, M>
25where
26	D: DeclarationValue<'a, M>,
27	M: NodeMetadata,
28{
29	pub open_curly: token_macros::LeftCurly,
30	pub declarations: Vec<'a, Declaration<'a, D, M>>,
31	pub rules: Vec<'a, R>,
32	pub close_curly: Option<token_macros::RightCurly>,
33	#[cfg_attr(feature = "serde", serde(skip))]
34	pub meta: M,
35}
36
37impl<'a, D, R, M> NodeWithMetadata<M> for Block<'a, D, R, M>
38where
39	D: DeclarationValue<'a, M>,
40	M: NodeMetadata,
41{
42	fn metadata(&self) -> M {
43		self.meta
44	}
45}
46
47impl<'a, D, R, M> Peek<'a> for Block<'a, D, R, M>
48where
49	D: DeclarationValue<'a, M>,
50	M: NodeMetadata,
51{
52	const PEEK_KINDSET: KindSet = KindSet::new(&[Kind::LeftCurly]);
53}
54
55impl<'a, D, R, M> Parse<'a> for Block<'a, D, R, M>
56where
57	D: DeclarationValue<'a, M>,
58	R: Parse<'a> + NodeWithMetadata<M> + RuleVariants<'a, DeclarationValue = D, Metadata = M>,
59	M: NodeMetadata,
60{
61	fn parse<Iter>(p: &mut Parser<'a, Iter>) -> Result<Self>
62	where
63		Iter: Iterator<Item = crate::Cursor> + Clone,
64	{
65		let open_curly = p.parse::<T!['{']>()?;
66		let mut declarations = Vec::new_in(p.alloc());
67		let mut rules = Vec::new_in(p.alloc());
68		let mut meta = M::default();
69
70		// Per CSS Syntax spec: maintain a buffer of declarations to flush when we encounter rules.
71		// This enables proper interleaving of declarations and rules.
72		let mut decls: Vec<'a, DeclarationOrBad<'a, D, M>> = Vec::new_in(p.alloc());
73
74		// Flush the decls buffer into the rules list as a DeclarationGroup.
75		// Per spec: "If decls is not empty, append it to rules, and set decls to a fresh empty list"
76		macro_rules! flush_decls {
77			() => {
78				if !decls.is_empty() {
79					let group =
80						DeclarationGroup { declarations: std::mem::replace(&mut decls, Vec::new_in(p.alloc())) };
81					if let Some(rule) = R::from_declaration_group(group) {
82						meta = meta.merge(rule.metadata());
83						rules.push(rule);
84					}
85				}
86			};
87		}
88
89		loop {
90			// While by default the parser will skip whitespace, the Declaration or Rule type may be a whitespace sensitive
91			// node, for example `ComponentValues`. As such whitespace needs to be consumed here, before Declarations and
92			// Rules are parsed.
93			// Additional tokens, such as CDC (`-->`) and CDO (`<!--`), Semicolon, RightParen, RightSquare are not valid
94			// component values and are not consumed by the declaration/rule/bad-declaration recovery paths below. Left
95			// unconsumed they cause a zero-progress loop and unbounded allocation. Per CSS Syntax they are only meaningful
96			// at the stylesheet top level, so discard them here, mirroring the stylesheet top-level loop.
97			p.consume_trivia_as_leading();
98			const ERROR_KINDS: KindSet = KindSet::new(&[
99				Kind::CdcOrCdo,
100				Kind::Semicolon,
101				Kind::RightParen,
102				Kind::RightSquare,
103				Kind::BadString,
104				Kind::BadUrl,
105			]);
106			let c = p.peek_n(1);
107			if c == ERROR_KINDS {
108				let old_skip = p.set_skip(ERROR_KINDS);
109				p.consume_trivia_as_leading();
110				p.set_skip(old_skip);
111				continue;
112			}
113			if p.at_end() {
114				break;
115			}
116			let c = p.peek_n(1);
117			if <T!['}']>::peek(p, c) {
118				break;
119			}
120			let old_state = p.set_state(State::Nested);
121			let checkpoint = p.checkpoint();
122			if <T![AtKeyword]>::peek(p, c) {
123				// At-rule: flush pending declarations and parse the rule
124				flush_decls!();
125				let rule = p.parse::<R>();
126				p.set_state(old_state);
127				let rule = rule?;
128				meta = meta.merge(rule.metadata());
129				rules.push(rule);
130			} else if let Ok(Some(decl)) = p.try_parse_if_peek::<Declaration<'a, D, M>>() {
131				// https://drafts.csswg.org/css-syntax-3/#consume-a-blocks-contents
132				// Parsing a declaration can result in an error, at which point the parser must be rewound and a Rule parse
133				// must be attempted. The CSS spec allows parsers to discard unknown rules as syntax errors, but this parser
134				// needs to retain them as unknown declarations, which creates some ambiguity as a Declaration may successfully
135				// parse as an unknown. In these cases attempting to parse as a Rule should also be tried so that valid Rules
136				// are not accidentally parsed as Unknown Declarations.
137				//
138				// Only reparse as a rule if:
139				// 1. The declaration is unknown (not recognized property/value)
140				// 2. The declaration name is invalid (not a known CSS property name)
141				// 3. Re-parsing as a rule succeeds AND produces a known rule (not UnknownQualifiedRule/UnknownAtRule)
142				//
143				// This ensures:
144				// - `background: var(--bg);` stays as declaration (valid name, even if unknown value)
145				// - `.foo {...}` becomes a rule (invalid declaration name, parses as known StyleRule)
146				// - `bad-prop: value;` stays as declaration (both unknown, prefer declaration)
147				if decl.is_unknown() && !D::valid_declaration_name(p, decl.name.into()) {
148					p.rewind(checkpoint.clone());
149					if let Ok(rule) = p.parse::<R>()
150						&& !rule.is_unknown()
151					{
152						// Successfully parsed as a known rule, use it instead of the unknown declaration
153						flush_decls!();
154						p.set_state(old_state);
155						meta = meta.merge(rule.metadata());
156						rules.push(rule);
157						continue;
158					}
159					// Failed to parse as rule or rule was also unknown, re-parse as declaration
160					p.rewind(checkpoint);
161					p.parse::<Declaration<'a, D, M>>().ok();
162				}
163				p.set_state(old_state);
164				meta = meta.merge(decl.metadata());
165				declarations.push(decl);
166			} else {
167				// Not an at-rule, not a declaration - try parsing as a qualified rule
168				let result = p.parse::<R>();
169				p.set_state(old_state);
170				match result {
171					Ok(rule) => {
172						flush_decls!();
173						meta = meta.merge(rule.metadata());
174						rules.push(rule);
175					}
176					Err(_) => {
177						// Failed as both declaration and rule - consume as bad declaration for error recovery
178						p.rewind(checkpoint);
179						p.set_state(State::Nested);
180						if let Ok(bad_decl) = p.parse::<BadDeclaration>() {
181							p.set_state(old_state);
182							decls.push(DeclarationOrBad::Bad(bad_decl));
183						}
184					}
185				}
186			}
187		}
188
189		// Flush any remaining declarations to rules
190		flush_decls!();
191		let close_curly = p.parse_if_peek::<T!['}']>()?;
192		Ok(Self { open_curly, declarations, rules, close_curly, meta })
193	}
194}
195
196impl<'a, D, R, M> ToCursors for Block<'a, D, R, M>
197where
198	D: DeclarationValue<'a, M> + ToCursors,
199	R: ToCursors,
200	M: NodeMetadata,
201{
202	fn to_cursors(&self, s: &mut impl CursorSink) {
203		ToCursors::to_cursors(&self.open_curly, s);
204		ToCursors::to_cursors(&self.declarations, s);
205		ToCursors::to_cursors(&self.rules, s);
206		ToCursors::to_cursors(&self.close_curly, s);
207	}
208}
209
210impl<'a, D, R, M> ToSpan for Block<'a, D, R, M>
211where
212	D: DeclarationValue<'a, M> + ToSpan,
213	R: ToSpan,
214	M: NodeMetadata,
215{
216	fn to_span(&self) -> Span {
217		self.open_curly.to_span()
218			+ if self.close_curly.is_some() {
219				self.close_curly.to_span()
220			} else {
221				self.declarations.to_span() + self.rules.to_span() + self.close_curly.to_span()
222			}
223	}
224}
225
226impl<'a, D, R, M> SemanticEq for Block<'a, D, R, M>
227where
228	D: DeclarationValue<'a, M>,
229	R: SemanticEq,
230	M: NodeMetadata,
231{
232	fn semantic_eq(&self, other: &Self) -> bool {
233		self.open_curly.semantic_eq(&other.open_curly)
234			&& self.close_curly.semantic_eq(&other.close_curly)
235			&& self.declarations.semantic_eq(&other.declarations)
236			&& self.rules.semantic_eq(&other.rules)
237	}
238}
239
240#[cfg(test)]
241mod tests {
242	use super::*;
243	use crate::EmptyAtomSet;
244	use crate::{Cursor, test_helpers::*};
245
246	#[derive(Debug)]
247	struct Decl(T![Ident]);
248
249	impl<M: NodeMetadata> NodeWithMetadata<M> for Decl {
250		fn metadata(&self) -> M {
251			M::default()
252		}
253	}
254
255	impl<'a, M: NodeMetadata> DeclarationValue<'a, M> for Decl {
256		fn is_initial(&self) -> bool {
257			false
258		}
259
260		fn is_inherit(&self) -> bool {
261			false
262		}
263
264		fn is_unset(&self) -> bool {
265			false
266		}
267
268		fn is_revert(&self) -> bool {
269			false
270		}
271
272		fn is_revert_layer(&self) -> bool {
273			false
274		}
275
276		fn needs_computing(&self) -> bool {
277			false
278		}
279
280		fn parse_specified_declaration_value<Iter>(p: &mut Parser<'a, Iter>, _: Cursor) -> Result<Self>
281		where
282			Iter: Iterator<Item = crate::Cursor> + Clone,
283		{
284			p.parse::<T![Ident]>().map(Self)
285		}
286	}
287
288	impl ToCursors for Decl {
289		fn to_cursors(&self, s: &mut impl CursorSink) {
290			ToCursors::to_cursors(&self.0, s);
291		}
292	}
293
294	impl ToSpan for Decl {
295		fn to_span(&self) -> Span {
296			self.0.to_span()
297		}
298	}
299
300	impl SemanticEq for Decl {
301		fn semantic_eq(&self, other: &Self) -> bool {
302			self.0.semantic_eq(&other.0)
303		}
304	}
305
306	impl NodeWithMetadata<()> for T![Ident] {
307		fn metadata(&self) {}
308	}
309
310	#[derive(Debug)]
311	struct Rule(T![Ident]);
312
313	impl<'a> Parse<'a> for Rule {
314		fn parse<I>(p: &mut Parser<'a, I>) -> Result<Self>
315		where
316			I: Iterator<Item = Cursor> + Clone,
317		{
318			Ok(Self(p.parse::<T![Ident]>()?))
319		}
320	}
321
322	impl ToCursors for Rule {
323		fn to_cursors(&self, s: &mut impl CursorSink) {
324			ToCursors::to_cursors(&self.0, s);
325		}
326	}
327
328	impl ToSpan for Rule {
329		fn to_span(&self) -> Span {
330			self.0.to_span()
331		}
332	}
333
334	impl NodeWithMetadata<()> for Rule {
335		fn metadata(&self) {}
336	}
337
338	impl<'a> crate::RuleVariants<'a> for Rule {
339		type DeclarationValue = Decl;
340		type Metadata = ();
341	}
342
343	#[test]
344	fn test_writes() {
345		assert_parse!(EmptyAtomSet::ATOMS, Block<Decl, Rule, ()>, "{color:black}");
346	}
347
348	#[test]
349	fn test_bad_string_in_block_does_not_hang() {
350		let alloc = crate::Arena::new();
351		for src in
352			[":{\".\n", "am:{\"\n", "alm:{\"\n", "alm:{\";.\n", "alm:{\"; }.\n", "alm:s {\n \x16\x00\x00:\";\n }\n"]
353		{
354			let lexer = css_lexer::Lexer::new(&EmptyAtomSet::ATOMS, src);
355			let mut parser = crate::Parser::new(&alloc, src, lexer);
356			let _ = parser.parse::<Block<Decl, Rule, ()>>();
357		}
358	}
359
360	#[test]
361	fn test_trailing_error_kinds_do_not_oom() {
362		let alloc = crate::Arena::new();
363		for src in ["{)))))))))))))", "{))))))))))))))", "{\r)))))))))))))"] {
364			let lexer = css_lexer::Lexer::new(&EmptyAtomSet::ATOMS, src);
365			let mut parser = crate::Parser::new(&alloc, src, lexer);
366			let _ = parser.parse::<Block<Decl, Rule, ()>>();
367		}
368	}
369}