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				// At-rule: flush pending declarations and parse the rule
121				flush_decls!();
122				let rule = p.parse::<R>();
123				p.set_state(old_state);
124				let rule = rule?;
125				meta = meta.merge(rule.metadata());
126				rules.push(rule);
127			} else if let Ok(Some(decl)) = p.try_parse_if_peek::<Declaration<'a, D, M>>() {
128				// https://drafts.csswg.org/css-syntax-3/#consume-a-blocks-contents
129				// Parsing a declaration can result in an error, at which point the parser must be rewound and a Rule parse
130				// must be attempted. The CSS spec allows parsers to discard unknown rules as syntax errors, but this parser
131				// needs to retain them as unknown declarations, which creates some ambiguity as a Declaration may successfully
132				// parse as an unknown. In these cases attempting to parse as a Rule should also be tried so that valid Rules
133				// are not accidentally parsed as Unknown Declarations.
134				//
135				// Only reparse as a rule if:
136				// 1. The declaration is unknown (not recognized property/value)
137				// 2. The declaration name is invalid (not a known CSS property name)
138				// 3. Re-parsing as a rule succeeds AND produces a known rule (not UnknownQualifiedRule/UnknownAtRule)
139				//
140				// This ensures:
141				// - `background: var(--bg);` stays as declaration (valid name, even if unknown value)
142				// - `.foo {...}` becomes a rule (invalid declaration name, parses as known StyleRule)
143				// - `bad-prop: value;` stays as declaration (both unknown, prefer declaration)
144				if decl.is_unknown() && !D::valid_declaration_name(p, decl.name.into()) {
145					p.rewind(checkpoint.clone());
146					if let Ok(rule) = p.parse::<R>()
147						&& !rule.is_unknown()
148					{
149						// Successfully parsed as a known rule, use it instead of the unknown declaration
150						flush_decls!();
151						p.set_state(old_state);
152						meta = meta.merge(rule.metadata());
153						rules.push(rule);
154						continue;
155					}
156					// Failed to parse as rule or rule was also unknown, re-parse as declaration
157					p.rewind(checkpoint);
158					p.parse::<Declaration<'a, D, M>>().ok();
159				}
160				p.set_state(old_state);
161				meta = meta.merge(decl.metadata());
162				declarations.push(decl);
163			} else {
164				// Not an at-rule, not a declaration - try parsing as a qualified rule
165				let result = p.parse::<R>();
166				p.set_state(old_state);
167				match result {
168					Ok(rule) => {
169						flush_decls!();
170						meta = meta.merge(rule.metadata());
171						rules.push(rule);
172					}
173					Err(_) => {
174						// Failed as both declaration and rule - consume as bad declaration for error recovery
175						p.rewind(checkpoint);
176						p.set_state(State::Nested);
177						if let Ok(bad_decl) = p.parse::<BadDeclaration>() {
178							p.set_state(old_state);
179							decls.push(DeclarationOrBad::Bad(bad_decl));
180						}
181					}
182				}
183			}
184		}
185
186		// Flush any remaining declarations to rules
187		flush_decls!();
188		let close_curly = p.parse_if_peek::<T!['}']>()?;
189		Ok(Self { open_curly, declarations, rules, close_curly, meta })
190	}
191}
192
193impl<'a, D, R, M> ToCursors for Block<'a, D, R, M>
194where
195	D: DeclarationValue<'a, M> + ToCursors,
196	R: ToCursors,
197	M: NodeMetadata,
198{
199	fn to_cursors(&self, s: &mut impl CursorSink) {
200		ToCursors::to_cursors(&self.open_curly, s);
201		ToCursors::to_cursors(&self.declarations, s);
202		ToCursors::to_cursors(&self.rules, s);
203		ToCursors::to_cursors(&self.close_curly, s);
204	}
205}
206
207impl<'a, D, R, M> ToSpan for Block<'a, D, R, M>
208where
209	D: DeclarationValue<'a, M> + ToSpan,
210	R: ToSpan,
211	M: NodeMetadata,
212{
213	fn to_span(&self) -> Span {
214		self.open_curly.to_span()
215			+ if self.close_curly.is_some() {
216				self.close_curly.to_span()
217			} else {
218				self.declarations.to_span() + self.rules.to_span() + self.close_curly.to_span()
219			}
220	}
221}
222
223impl<'a, D, R, M> SemanticEq for Block<'a, D, R, M>
224where
225	D: DeclarationValue<'a, M>,
226	R: SemanticEq,
227	M: NodeMetadata,
228{
229	fn semantic_eq(&self, other: &Self) -> bool {
230		self.open_curly.semantic_eq(&other.open_curly)
231			&& self.close_curly.semantic_eq(&other.close_curly)
232			&& self.declarations.semantic_eq(&other.declarations)
233			&& self.rules.semantic_eq(&other.rules)
234	}
235}
236
237#[cfg(test)]
238mod tests {
239	use super::*;
240	use crate::EmptyAtomSet;
241	use crate::{Cursor, test_helpers::*};
242
243	#[derive(Debug)]
244	struct Decl(T![Ident]);
245
246	impl<M: NodeMetadata> NodeWithMetadata<M> for Decl {
247		fn metadata(&self) -> M {
248			M::default()
249		}
250	}
251
252	impl<'a, M: NodeMetadata> DeclarationValue<'a, M> for Decl {
253		fn parse_specified_declaration_value<Iter>(p: &mut Parser<'a, Iter>, _: Cursor) -> Result<Self>
254		where
255			Iter: Iterator<Item = crate::Cursor> + Clone,
256		{
257			p.parse::<T![Ident]>().map(Self)
258		}
259	}
260
261	impl ToCursors for Decl {
262		fn to_cursors(&self, s: &mut impl CursorSink) {
263			ToCursors::to_cursors(&self.0, s);
264		}
265	}
266
267	impl ToSpan for Decl {
268		fn to_span(&self) -> Span {
269			self.0.to_span()
270		}
271	}
272
273	impl SemanticEq for Decl {
274		fn semantic_eq(&self, other: &Self) -> bool {
275			self.0.semantic_eq(&other.0)
276		}
277	}
278
279	impl NodeWithMetadata<()> for T![Ident] {
280		fn metadata(&self) {}
281	}
282
283	#[derive(Debug)]
284	struct Rule(T![Ident]);
285
286	impl<'a> Parse<'a> for Rule {
287		fn parse<I>(p: &mut Parser<'a, I>) -> Result<Self>
288		where
289			I: Iterator<Item = Cursor> + Clone,
290		{
291			Ok(Self(p.parse::<T![Ident]>()?))
292		}
293	}
294
295	impl ToCursors for Rule {
296		fn to_cursors(&self, s: &mut impl CursorSink) {
297			ToCursors::to_cursors(&self.0, s);
298		}
299	}
300
301	impl ToSpan for Rule {
302		fn to_span(&self) -> Span {
303			self.0.to_span()
304		}
305	}
306
307	impl NodeWithMetadata<()> for Rule {
308		fn metadata(&self) {}
309	}
310
311	impl<'a> crate::RuleVariants<'a> for Rule {
312		type DeclarationValue = Decl;
313		type Metadata = ();
314	}
315
316	#[test]
317	fn test_writes() {
318		assert_parse!(EmptyAtomSet::ATOMS, Block<Decl, Rule, ()>, "{color:black}");
319	}
320
321	#[test]
322	fn test_bad_string_in_block_does_not_hang() {
323		let alloc = crate::Arena::new();
324		for src in
325			[":{\".\n", "am:{\"\n", "alm:{\"\n", "alm:{\";.\n", "alm:{\"; }.\n", "alm:s {\n \x16\x00\x00:\";\n }\n"]
326		{
327			let lexer = css_lexer::Lexer::new(&EmptyAtomSet::ATOMS, src);
328			let mut parser = crate::Parser::new(&alloc, src, lexer);
329			let _ = parser.parse::<Block<Decl, Rule, ()>>();
330		}
331	}
332
333	#[test]
334	fn test_trailing_error_kinds_do_not_oom() {
335		let alloc = crate::Arena::new();
336		for src in ["{)))))))))))))", "{))))))))))))))", "{\r)))))))))))))"] {
337			let lexer = css_lexer::Lexer::new(&EmptyAtomSet::ATOMS, src);
338			let mut parser = crate::Parser::new(&alloc, src, lexer);
339			let _ = parser.parse::<Block<Decl, Rule, ()>>();
340		}
341	}
342}