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