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