Skip to main content

css_parse/syntax/
block.rs

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