Skip to main content

css_ast/
metadata.rs

1#[cfg(feature = "visitable")]
2use crate::visit::NodeId;
3use crate::{
4	CssAtomSet,
5	traits::{AppliesTo, BoxPortion, BoxSide, PropertyGroup},
6};
7use bitmask_enum::bitmask;
8use css_lexer::{Span, ToSpan};
9use css_parse::{NodeMetadata, SemanticEq, ToCursors};
10
11/// How unitless zero (0 without a unit) resolves in a given context.
12///
13/// For most Style Values, a `0` can be a drop-in replacement for `0px`, but
14/// certain style values will provide discrete syntax for `0px` and `0`, meaning
15/// they resolve to different things. For properties that accept both `<number>`
16/// and `<length>`, unitless zero may resolve to a _different value_. Using a
17/// piece of metadata to describe this can be helpful for linting/minifying -
18/// avoiding a reduction in semantic meaning.
19///
20/// Examples:
21/// - `width: 0px` == `width: 0` (unitless zero resolves to length)
22/// - `line-height: 0px` != `line-height: 0` (unitless zero resolves to number = 0x multiplier)
23/// - `tab-size: 0px` != `tab-size: 0` (unitless zero resolves to number = 0 tab characters)
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub enum UnitlessZeroResolves {
27	/// Unitless zero resolves to a length (0 = 0px).
28	#[default]
29	Length,
30	/// Unitless zero resolves to a number or percentage. NOT safe to reduce.
31	Number,
32}
33
34#[bitmask(u32)]
35#[bitmask_config(vec_debug)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub enum AtRuleId {
38	Charset,
39	ColorProfile,
40	Container,
41	CounterStyle,
42	CustomMedia,
43	FontFace,
44	FontFeatureValues,
45	FontPaletteValues,
46	Import,
47	Keyframes,
48	Layer,
49	Media,
50	Namespace,
51	Page,
52	Property,
53	Scope,
54	StartingStyle,
55	Supports,
56	Document,
57	WebkitKeyframes,
58	MozDocument,
59}
60
61#[cfg(feature = "visitable")]
62impl NodeId {
63	/// Converts a NodeId to an AtRuleId if the node is an at-rule type.
64	/// Returns `None` for non-at-rule nodes like StyleRule, Declaration, etc.
65	pub fn to_at_rule_id(self) -> Option<AtRuleId> {
66		match self {
67			Self::CharsetRule => Some(AtRuleId::Charset),
68			Self::ColorProfileRule => Some(AtRuleId::ColorProfile),
69			Self::ContainerRule => Some(AtRuleId::Container),
70			Self::CounterStyleRule => Some(AtRuleId::CounterStyle),
71			Self::CustomMediaRule => Some(AtRuleId::CustomMedia),
72			Self::DocumentRule => Some(AtRuleId::Document),
73			Self::FontFaceRule => Some(AtRuleId::FontFace),
74			Self::FontFeatureValuesRule => Some(AtRuleId::FontFeatureValues),
75			Self::FontPaletteValuesRule => Some(AtRuleId::FontPaletteValues),
76			Self::KeyframesRule => Some(AtRuleId::Keyframes),
77			Self::LayerRule => Some(AtRuleId::Layer),
78			Self::MediaRule => Some(AtRuleId::Media),
79			Self::MozDocumentRule => Some(AtRuleId::MozDocument),
80			Self::NamespaceRule => Some(AtRuleId::Namespace),
81			Self::PageRule => Some(AtRuleId::Page),
82			Self::PropertyRule => Some(AtRuleId::Property),
83			Self::ScopeRule => Some(AtRuleId::Scope),
84			Self::StartingStyleRule => Some(AtRuleId::StartingStyle),
85			Self::SupportsRule => Some(AtRuleId::Supports),
86			Self::WebkitKeyframesRule => Some(AtRuleId::WebkitKeyframes),
87			_ => None,
88		}
89	}
90}
91
92#[bitmask(u8)]
93#[bitmask_config(vec_debug)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95pub enum VendorPrefixes {
96	Moz,
97	WebKit,
98	O,
99	Ms,
100}
101
102impl TryFrom<CssAtomSet> for VendorPrefixes {
103	type Error = ();
104	fn try_from(atom: CssAtomSet) -> Result<Self, Self::Error> {
105		const VENDOR_FLAG: u32 = 0b00000000_10000000_00000000_00000000;
106		const VENDORS: [VendorPrefixes; 4] =
107			[VendorPrefixes::WebKit, VendorPrefixes::Moz, VendorPrefixes::Ms, VendorPrefixes::O];
108
109		let atom_bits = atom as u32;
110		if atom_bits & VENDOR_FLAG == 0 {
111			return Err(());
112		}
113		let index = (atom_bits >> 21) & 0b11;
114		Ok(VENDORS[index as usize])
115	}
116}
117
118#[bitmask(u8)]
119#[bitmask_config(vec_debug)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121pub enum DeclarationKind {
122	/// If a declaration has !important
123	Important,
124	/// If a declaration used a css-wide keyword, e.g. `inherit` or `revert-layer`.
125	CssWideKeywords,
126	/// If a declaration is custom, e.g `--foo`
127	Custom,
128	/// If a declaration is computed-time, e.g. using `calc()` or `var()`
129	Computed,
130	/// If a declaration is shorthand
131	Shorthands,
132	/// If a declaration is longhand
133	Longhands,
134}
135
136/// Categories of nodes present in metadata, used for selector filtering.
137#[bitmask(u32)]
138#[bitmask_config(vec_debug)]
139#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
140pub enum NodeKinds {
141	/// Contains unknown nodes
142	Unknown,
143	/// Contains style rules
144	StyleRule,
145	/// Contains at-rules (media, keyframes, etc.)
146	AtRule,
147	/// Contains Declarations
148	Declaration,
149	/// Contains function nodes
150	Function,
151	/// Node has an empty prelude
152	EmptyPrelude,
153	/// Node has a block which contains no declarations and no rules
154	EmptyBlock,
155	/// Node is nested within another node
156	Nested,
157	/// Node is deprecated (non-conforming, obsolete)
158	Deprecated,
159	/// Node is experimental (not yet standardized)
160	Experimental,
161	/// Node is non-standard (vendor-specific, not in spec)
162	NonStandard,
163	/// Node is a dimension value (length, angle, time, flex, etc.)
164	Dimension,
165	/// Node is a custom element or custom property
166	Custom,
167	/// Node has an effect on rendering: a declaration, or a rule which is not inert. Rules without
168	/// a block (`@import`, `@layer a;`) always have an effect.
169	Effective,
170	/// Node has no effect on rendering: a rule whose block holds no declarations, and no rules
171	/// other than inert ones. An inert node can be removed without changing what the sheet does.
172	Inert,
173}
174
175/// Queryable properties a node exposes for selector matching.
176/// Used by attribute selectors like `[name]` or `[name=value]`.
177#[bitmask(u8)]
178#[bitmask_config(vec_debug)]
179#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
180pub enum PropertyKind {
181	/// Node has a queryable `name` property (declarations, named at-rules, functions)
182	Name,
183}
184
185/// All PropertyKind variants for iteration.
186pub const PROPERTY_KIND_VARIANTS: &[PropertyKind] = &[PropertyKind::Name];
187
188/// OR-composable bitflag recording the set of CSS value types a substitution position accepts.
189///
190/// Used by [`Unresolved`](crate::Unresolved) to carry grammar-type knowledge at positions where a
191/// substitution function appears but the slot cannot be fully typed at parse time.
192///
193/// `ANY` (all bits set) is used for `Custom` declaration bodies and substitution-function
194/// internals where no type constraint applies.
195#[bitmask(u32)]
196#[bitmask_config(vec_debug)]
197#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
198pub enum CssTypes {
199	Length,
200	Percentage,
201	Number,
202	Integer,
203	Angle,
204	Time,
205	Frequency,
206	Flex,
207	Color,
208	Keyword,
209	Image,
210	Url,
211	String,
212}
213
214impl CssTypes {
215	/// All bits set - use for untyped contexts (custom declarations, substitution internals).
216	pub const ANY: CssTypes = CssTypes { bits: !0 };
217}
218
219/// Aggregated metadata computed from declarations within a block.
220/// This allows efficient checking of what types of properties a block contains
221/// without iterating through all declarations.
222#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
223#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
224pub struct CssMetadata {
225	/// Bitwise OR of all PropertyGroup values
226	pub property_groups: PropertyGroup,
227	/// Bitwise OR of all AppliesTo values
228	pub applies_to: AppliesTo,
229	/// Bitwise OR of all BoxSide values
230	pub box_sides: BoxSide,
231	/// Bitwise OR of all BoxPortion values
232	pub box_portions: BoxPortion,
233	/// Bitwise OR of all DeclarationKind values
234	pub declaration_kinds: DeclarationKind,
235	/// Bitwise OR of all AtRuleIds in a Node
236	pub used_at_rules: AtRuleId,
237	/// Bitwise OR of all VendorPrefixes in a Node
238	pub vendor_prefixes: VendorPrefixes,
239	/// Bitwise OR of node categories present
240	pub node_kinds: NodeKinds,
241	/// Bitwise OR of queryable properties present
242	pub property_kinds: PropertyKind,
243	/// Bitwise OR of literal value kinds present in this node and its subtree
244	pub value_kinds: CssTypes,
245	/// How unitless zero resolves in this context (Length or Number)
246	pub unitless_zero_resolves: UnitlessZeroResolves,
247	/// Size of vector-based nodes (e.g., number of declarations, selector list length)
248	pub size: u16,
249	/// True if any substitution function (var(), env(), attr(), etc.) or Unresolved node is present.
250	/// Enables subtree-skip optimisations in visitors and the minifier.
251	pub uses_substitution: bool,
252	/// OR-union of all CssTypes bits for substitution positions in this node.
253	/// Accumulates upward through CssMetadata::merge for type inference.
254	pub expected_value_kinds: CssTypes,
255}
256
257impl Default for CssMetadata {
258	fn default() -> Self {
259		Self {
260			property_groups: PropertyGroup::none(),
261			applies_to: AppliesTo::none(),
262			box_sides: BoxSide::none(),
263			box_portions: BoxPortion::none(),
264			declaration_kinds: DeclarationKind::none(),
265			used_at_rules: AtRuleId::none(),
266			vendor_prefixes: VendorPrefixes::none(),
267			node_kinds: NodeKinds::none(),
268			property_kinds: PropertyKind::none(),
269			value_kinds: CssTypes::none(),
270			unitless_zero_resolves: UnitlessZeroResolves::default(),
271			size: 0,
272			uses_substitution: false,
273			expected_value_kinds: CssTypes::none(),
274		}
275	}
276}
277
278impl CssMetadata {
279	/// Returns true if this metadata is empty (contains no properties or at-rules)
280	#[inline]
281	pub fn is_empty(&self) -> bool {
282		self.property_groups == PropertyGroup::none()
283			&& self.applies_to == AppliesTo::none()
284			&& self.box_sides == BoxSide::none()
285			&& self.box_portions == BoxPortion::none()
286			&& self.declaration_kinds == DeclarationKind::none()
287			&& self.used_at_rules == AtRuleId::none()
288			&& self.vendor_prefixes == VendorPrefixes::none()
289			&& self.node_kinds == NodeKinds::none()
290			&& self.property_kinds == PropertyKind::none()
291			&& self.unitless_zero_resolves == UnitlessZeroResolves::Length
292			&& self.size == 0
293			&& !self.uses_substitution
294			&& self.expected_value_kinds == CssTypes::none()
295			&& self.value_kinds == CssTypes::none()
296	}
297
298	/// Returns true if this block modifies any positioning-related properties.
299	#[inline]
300	pub fn modifies_box(&self) -> bool {
301		!self.box_portions.is_none()
302	}
303
304	/// Returns true if metadata contains important declarations.
305	#[inline]
306	pub fn has_important(&self) -> bool {
307		self.declaration_kinds.contains(DeclarationKind::Important)
308	}
309
310	/// Returns true if metadata contains custom properties.
311	#[inline]
312	pub fn has_custom_properties(&self) -> bool {
313		self.declaration_kinds.contains(DeclarationKind::Custom)
314	}
315
316	/// Returns true if metadata contains computed values.
317	#[inline]
318	pub fn has_computed(&self) -> bool {
319		self.declaration_kinds.contains(DeclarationKind::Computed)
320	}
321
322	/// Returns true if metadata contains shorthand properties.
323	#[inline]
324	pub fn has_shorthands(&self) -> bool {
325		self.declaration_kinds.contains(DeclarationKind::Shorthands)
326	}
327
328	/// Returns true if metadata contains longhand properties.
329	#[inline]
330	pub fn has_longhands(&self) -> bool {
331		self.declaration_kinds.contains(DeclarationKind::Longhands)
332	}
333
334	/// Returns true if metadata contains unknown nodes.
335	#[inline]
336	pub fn has_unknown(&self) -> bool {
337		self.node_kinds.contains(NodeKinds::Unknown)
338	}
339
340	/// Returns true if metadata contains vendor-prefixed properties.
341	#[inline]
342	pub fn has_vendor_prefixes(&self) -> bool {
343		!self.vendor_prefixes.is_none()
344	}
345
346	/// Returns the vendor prefix if exactly one is present, None otherwise.
347	#[inline]
348	pub fn single_vendor_prefix(&self) -> Option<VendorPrefixes> {
349		if self.vendor_prefixes.is_none() || self.vendor_prefixes.bits().count_ones() != 1 {
350			None
351		} else {
352			Some(self.vendor_prefixes)
353		}
354	}
355
356	/// Returns true if metadata contains any rule nodes.
357	#[inline]
358	pub fn has_rules(&self) -> bool {
359		self.node_kinds.intersects(NodeKinds::StyleRule | NodeKinds::AtRule)
360	}
361
362	/// Returns true if metadata contains style rules.
363	#[inline]
364	pub fn has_style_rules(&self) -> bool {
365		self.node_kinds.contains(NodeKinds::StyleRule)
366	}
367
368	/// Returns true if metadata contains at-rules.
369	#[inline]
370	pub fn has_at_rules(&self) -> bool {
371		self.node_kinds.contains(NodeKinds::AtRule)
372	}
373
374	/// Returns true if metadata contains function nodes.
375	#[inline]
376	pub fn has_functions(&self) -> bool {
377		self.node_kinds.contains(NodeKinds::Function)
378	}
379
380	/// Returns true if metadata contains deprecated nodes.
381	#[inline]
382	pub fn is_deprecated(&self) -> bool {
383		self.node_kinds.contains(NodeKinds::Deprecated)
384	}
385
386	/// Returns true if metadata contains experimental nodes.
387	#[inline]
388	pub fn is_experimental(&self) -> bool {
389		self.node_kinds.contains(NodeKinds::Experimental)
390	}
391
392	/// Returns true if metadata contains non-standard nodes.
393	#[inline]
394	pub fn is_non_standard(&self) -> bool {
395		self.node_kinds.contains(NodeKinds::NonStandard)
396	}
397
398	/// Returns true if metadata contains dimension values.
399	#[inline]
400	pub fn is_dimension(&self) -> bool {
401		self.node_kinds.contains(NodeKinds::Dimension)
402	}
403
404	/// Returns true if metadata contains nodes with the given property kind.
405	#[inline]
406	pub fn has_property_kind(&self, kind: PropertyKind) -> bool {
407		self.property_kinds.contains(kind)
408	}
409
410	/// Returns true if any substitution function or Unresolved node is present in this subtree.
411	#[inline]
412	pub fn has_substitution(&self) -> bool {
413		self.uses_substitution
414	}
415
416	/// Returns true if this node or its subtree contains any of the given value kinds.
417	#[inline]
418	pub fn has_value_kinds(&self, kinds: CssTypes) -> bool {
419		self.value_kinds.intersects(kinds)
420	}
421
422	/// Returns true if this is an empty container (no declarations, no nested rules).
423	#[inline]
424	pub fn is_empty_container(&self) -> bool {
425		self.node_kinds.contains(NodeKinds::EmptyBlock)
426	}
427
428	/// Returns true if anything in this node or its subtree has an effect on rendering.
429	#[inline]
430	pub fn has_effect(&self) -> bool {
431		self.node_kinds.contains(NodeKinds::Effective)
432	}
433
434	/// Returns true if this node has no effect on rendering, so it can be removed.
435	///
436	/// For [self metadata](css_parse::NodeWithMetadata::self_metadata) this describes the node
437	/// itself. Node kinds aggregate upwards, so for a subtree it only says that the subtree holds
438	/// an inert node somewhere.
439	#[inline]
440	pub fn is_inert(&self) -> bool {
441		self.node_kinds.contains(NodeKinds::Inert)
442	}
443
444	/// Returns true if this node, or a node in its subtree, is nested inside another node.
445	#[inline]
446	pub fn is_nested(&self) -> bool {
447		self.node_kinds.contains(NodeKinds::Nested)
448	}
449
450	/// Returns true if this node has a prelude which covers no source text, such as the omitted
451	/// selector of `@page {}` or the anonymous layer of `@layer {}`.
452	#[inline]
453	pub fn has_empty_prelude(&self) -> bool {
454		self.node_kinds.contains(NodeKinds::EmptyPrelude)
455	}
456
457	/// Returns true if this node can be a container (has StyleRule or AtRule kind).
458	#[inline]
459	pub fn can_be_empty(&self) -> bool {
460		self.node_kinds.intersects(NodeKinds::StyleRule | NodeKinds::AtRule)
461	}
462}
463
464impl NodeMetadata for CssMetadata {
465	#[inline]
466	fn merge(mut self, other: Self) -> Self {
467		self.property_groups |= other.property_groups;
468		self.applies_to |= other.applies_to;
469		self.box_sides |= other.box_sides;
470		self.box_portions |= other.box_portions;
471		self.declaration_kinds |= other.declaration_kinds;
472		self.used_at_rules |= other.used_at_rules;
473		self.vendor_prefixes |= other.vendor_prefixes;
474		self.node_kinds |= other.node_kinds;
475		self.property_kinds |= other.property_kinds;
476		self.value_kinds |= other.value_kinds;
477		// For unitless_zero_resolves, we keep Number if either side has it (conservative)
478		if other.unitless_zero_resolves == UnitlessZeroResolves::Number {
479			self.unitless_zero_resolves = UnitlessZeroResolves::Number;
480		}
481		self.size = self.size.max(other.size);
482		self.uses_substitution |= other.uses_substitution;
483		self.expected_value_kinds |= other.expected_value_kinds;
484		self
485	}
486
487	#[inline]
488	fn with_size(mut self, size: u16) -> Self {
489		self.size = size;
490		self
491	}
492
493	#[inline]
494	fn with_declaration(mut self) -> Self {
495		self.node_kinds |= NodeKinds::Declaration | NodeKinds::Effective;
496		self
497	}
498
499	#[inline]
500	fn with_nested(mut self) -> Self {
501		self.node_kinds |= NodeKinds::Nested;
502		self
503	}
504}
505
506// Metadata is not serialized to tokens but providing these simplifies ToCursors/ToSpan impls
507impl ToCursors for CssMetadata {
508	fn to_cursors(&self, _: &mut impl css_parse::CursorSink) {}
509}
510impl ToSpan for CssMetadata {
511	fn to_span(&self) -> Span {
512		Span::DUMMY
513	}
514}
515
516// CssTypes is not serialized to tokens; these no-op impls let it sit as a
517// non-node field on Unresolved under derive(ToCursors)/derive(ToSpan).
518impl ToCursors for CssTypes {
519	fn to_cursors(&self, _: &mut impl css_parse::CursorSink) {}
520}
521impl ToSpan for CssTypes {
522	fn to_span(&self) -> Span {
523		Span::DUMMY
524	}
525}
526impl SemanticEq for CssTypes {
527	fn semantic_eq(&self, other: &Self, _source_text: &str) -> bool {
528		self == other
529	}
530}
531
532impl SemanticEq for CssMetadata {
533	fn semantic_eq(&self, other: &Self, _source_text: &str) -> bool {
534		self == other
535	}
536}
537
538macro_rules! impl_token_metadata {
539	($($token:tt),* $(,)?) => {
540		$(
541			impl css_parse::NodeWithMetadata<CssMetadata> for css_parse::T![$token] {
542				fn metadata(&self) -> CssMetadata {
543					CssMetadata::default()
544				}
545			}
546		)*
547	};
548}
549
550impl_token_metadata!(
551	Ident,
552	Number,
553	Dimension,
554	Hash,
555	AtKeyword,
556	String,
557	Function,
558	Url,
559	Delim,
560	Colon,
561	Semicolon,
562	Comma,
563	LeftCurly,
564	RightCurly,
565	LeftSquare,
566	RightSquare,
567	LeftParen
568);
569
570macro_rules! impl_leaf_metadata {
571	($($t:ty),* $(,)?) => {
572		$(
573			impl css_parse::NodeWithMetadata<CssMetadata> for $t {
574				fn metadata(&self) -> CssMetadata {
575					CssMetadata::default()
576				}
577			}
578		)*
579	};
580}
581impl_leaf_metadata!(
582	css_parse::token_macros::delim::Slash,
583	css_parse::token_macros::delim::Or,
584	css_parse::token_macros::delim::Plus,
585	css_parse::token_macros::delim::Tilde,
586	css_parse::token_macros::delim::Star,
587	css_parse::token_macros::delim::Question,
588	css_parse::token_macros::delim::Underscore,
589	css_parse::token_macros::delim::Eq,
590	css_parse::token_macros::delim::Gt,
591	css_parse::token_macros::delim::Lt,
592	css_parse::token_macros::delim::Dot,
593	css_parse::token_macros::delim::And,
594	css_parse::token_macros::delim::At,
595	css_parse::token_macros::delim::Caret,
596	css_parse::token_macros::delim::Dash,
597	css_parse::token_macros::delim::Dollar,
598	css_parse::token_macros::delim::Bang,
599	css_parse::token_macros::delim::Percent,
600	css_parse::token_macros::delim::Hash,
601	css_parse::token_macros::delim::Backtick,
602	css_parse::token_macros::double::ColonColon,
603	css_parse::token_macros::double::PipePipe,
604	css_parse::token_macros::double::EqualEqual,
605	css_parse::token_macros::double::BangEqual,
606	css_parse::token_macros::double::TildeEqual,
607	css_parse::token_macros::double::PipeEqual,
608	css_parse::token_macros::double::CaretEqual,
609	css_parse::token_macros::double::DollarEqual,
610	css_parse::token_macros::double::StarEqual,
611	css_parse::token_macros::Any,
612	css_parse::token_macros::DashedIdent,
613	css_parse::token_macros::Whitespace,
614	css_parse::token_macros::RightParen,
615	crate::HexColor,
616	css_parse::Comparison,
617);
618
619impl<'a, T: css_parse::NodeWithMetadata<CssMetadata>> css_parse::NodeWithMetadata<CssMetadata>
620	for css_parse::Vec<'a, T>
621{
622	fn metadata(&self) -> CssMetadata {
623		self.iter().fold(CssMetadata::default(), |acc, item| NodeMetadata::merge(acc, item.metadata()))
624	}
625}
626
627impl<'a, T: css_parse::NodeWithMetadata<CssMetadata>, const MIN: usize> css_parse::NodeWithMetadata<CssMetadata>
628	for css_parse::CommaSeparated<'a, T, MIN>
629{
630	fn metadata(&self) -> CssMetadata {
631		self.into_iter().fold(CssMetadata::default(), |acc, (item, _comma)| NodeMetadata::merge(acc, item.metadata()))
632	}
633}
634
635macro_rules! impl_optionals_metadata {
636	($name:ident, $($T:ident => $v:ident),+) => {
637		impl<$($T: css_parse::NodeWithMetadata<CssMetadata>),+>
638			css_parse::NodeWithMetadata<CssMetadata> for css_parse::$name<$($T),+>
639		{
640			fn metadata(&self) -> CssMetadata {
641				let css_parse::$name($($v),+) = self;
642				let mut meta = CssMetadata::default();
643				$(
644					if let Some(val) = $v {
645						meta = NodeMetadata::merge(meta, val.metadata());
646					}
647				)+
648				meta
649			}
650		}
651	};
652}
653
654impl_optionals_metadata!(Optionals2, A => a, B => b);
655impl_optionals_metadata!(Optionals3, A => a, B => b, C => c);
656impl_optionals_metadata!(Optionals4, A => a, B => b, C => c, D => d);
657impl_optionals_metadata!(Optionals5, A => a, B => b, C => c, D => d, E => e);
658
659macro_rules! impl_tuple_metadata {
660	($($T:ident),+) => {
661		impl<$($T: css_parse::NodeWithMetadata<CssMetadata>),+>
662			css_parse::NodeWithMetadata<CssMetadata> for ($($T,)+)
663		{
664			#[allow(non_snake_case)]
665			fn metadata(&self) -> CssMetadata {
666				let ($($T,)+) = self;
667				let mut meta = CssMetadata::default();
668				$(
669					meta = NodeMetadata::merge(meta, $T.metadata());
670				)+
671				meta
672			}
673		}
674	};
675}
676
677impl_tuple_metadata!(A, B);
678impl_tuple_metadata!(A, B, C);
679impl_tuple_metadata!(A, B, C, D);
680impl_tuple_metadata!(A, B, C, D, E);
681impl_tuple_metadata!(A, B, C, D, E, F);
682impl_tuple_metadata!(A, B, C, D, E, F, G);
683impl_tuple_metadata!(A, B, C, D, E, F, G, H);
684
685#[cfg(test)]
686mod tests {
687	use super::*;
688	use crate::{CssAtomSet, StyleSheet};
689	use css_lexer::Lexer;
690	use css_parse::{Arena, NodeMetadata, NodeWithMetadata, Parser};
691
692	#[test]
693	fn test_block_metadata_merge() {
694		let meta1 = CssMetadata {
695			property_groups: PropertyGroup::Color,
696			declaration_kinds: DeclarationKind::Important,
697			..Default::default()
698		};
699
700		let meta2 = CssMetadata {
701			property_groups: PropertyGroup::Position,
702			declaration_kinds: DeclarationKind::Custom,
703			..Default::default()
704		};
705
706		let merged = meta1.merge(meta2);
707
708		assert!(merged.property_groups.contains(PropertyGroup::Color));
709		assert!(merged.property_groups.contains(PropertyGroup::Position));
710		assert!(merged.declaration_kinds.contains(DeclarationKind::Important));
711		assert!(merged.declaration_kinds.contains(DeclarationKind::Custom));
712	}
713
714	#[test]
715	fn test_stylesheet_metadata_simple() {
716		let css = "body { color: red; width: 100px; }";
717		let alloc = Arena::new();
718		let lexer = Lexer::new(&CssAtomSet::ATOMS, css);
719		let mut parser = Parser::new(&alloc, css, lexer);
720		let stylesheet = parser.parse::<StyleSheet>().unwrap();
721
722		let metadata = stylesheet.metadata();
723
724		assert!(metadata.property_groups.contains(PropertyGroup::Color));
725		assert!(metadata.property_groups.contains(PropertyGroup::Sizing));
726		assert!(metadata.modifies_box());
727		assert!(metadata.has_longhands());
728	}
729
730	#[test]
731	fn test_stylesheet_metadata_with_important() {
732		let css = "body { color: red !important; }";
733		let alloc = Arena::new();
734		let lexer = Lexer::new(&CssAtomSet::ATOMS, css);
735		let mut parser = Parser::new(&alloc, css, lexer);
736		let stylesheet = parser.parse::<StyleSheet>().unwrap();
737
738		let metadata = stylesheet.metadata();
739
740		assert!(metadata.has_important());
741		assert!(metadata.property_groups.contains(PropertyGroup::Color));
742	}
743
744	#[test]
745	fn test_stylesheet_metadata_custom_properties() {
746		let css = "body { --custom: value; }";
747		let alloc = Arena::new();
748		let lexer = Lexer::new(&CssAtomSet::ATOMS, css);
749		let mut parser = Parser::new(&alloc, css, lexer);
750		let stylesheet = parser.parse::<StyleSheet>().unwrap();
751
752		let metadata = stylesheet.metadata();
753
754		assert!(metadata.has_custom_properties());
755	}
756
757	#[test]
758	fn test_stylesheet_metadata_nested_media() {
759		let css = "@media screen { body { color: red; } }";
760		let alloc = Arena::new();
761		let lexer = Lexer::new(&CssAtomSet::ATOMS, css);
762		let mut parser = Parser::new(&alloc, css, lexer);
763		let stylesheet = parser.parse::<StyleSheet>().unwrap();
764
765		let metadata = stylesheet.metadata();
766
767		assert!(metadata.property_groups.contains(PropertyGroup::Color));
768		assert!(metadata.used_at_rules.contains(AtRuleId::Media));
769	}
770
771	fn first_rule_metadata(css: &str) -> CssMetadata {
772		let alloc = Arena::new();
773		let lexer = Lexer::new(&CssAtomSet::ATOMS, css);
774		let mut parser = Parser::new(&alloc, css, lexer);
775		let stylesheet = parser.parse::<StyleSheet>().unwrap();
776		match stylesheet.rules.first().expect("stylesheet has no rules") {
777			crate::Rule::Style(rule) => rule.self_metadata(),
778			crate::Rule::Media(rule) => rule.self_metadata(),
779			crate::Rule::FontFace(rule) => rule.self_metadata(),
780			crate::Rule::Keyframes(rule) => rule.self_metadata(),
781			crate::Rule::Layer(rule) => rule.self_metadata(),
782			crate::Rule::Page(rule) => rule.self_metadata(),
783			crate::Rule::Scope(rule) => rule.self_metadata(),
784			rule => panic!("unexpected rule kind {rule:?}"),
785		}
786	}
787
788	fn stylesheet_metadata(css: &str) -> CssMetadata {
789		let alloc = Arena::new();
790		let lexer = Lexer::new(&CssAtomSet::ATOMS, css);
791		let mut parser = Parser::new(&alloc, css, lexer);
792		parser.parse::<StyleSheet>().unwrap().metadata()
793	}
794
795	#[test]
796	fn nested_rules_are_marked_nested() {
797		assert!(!stylesheet_metadata("a { color: red }").is_nested());
798		assert!(!stylesheet_metadata("@media screen { color: red }").is_nested());
799		assert!(stylesheet_metadata("a { b { color: red } }").is_nested());
800		assert!(stylesheet_metadata("@media screen { a { color: red } }").is_nested());
801	}
802
803	#[test]
804	fn omitted_preludes_are_marked_empty() {
805		assert!(first_rule_metadata("@page {}").has_empty_prelude());
806		assert!(first_rule_metadata("@layer { a { color: red } }").has_empty_prelude());
807		assert!(first_rule_metadata("@scope { a { color: red } }").has_empty_prelude());
808	}
809
810	#[test]
811	fn written_preludes_are_not_marked_empty() {
812		assert!(!first_rule_metadata("@page :left {}").has_empty_prelude());
813		assert!(!first_rule_metadata("@layer base { a { color: red } }").has_empty_prelude());
814		assert!(!first_rule_metadata("@scope (.card) { a { color: red } }").has_empty_prelude());
815	}
816
817	#[test]
818	fn rules_with_an_empty_block_are_marked_empty() {
819		assert!(first_rule_metadata("a {}").is_empty_container());
820		assert!(first_rule_metadata("@media screen {}").is_empty_container());
821		assert!(first_rule_metadata("@page {}").is_empty_container());
822		assert!(first_rule_metadata("@keyframes fade {}").is_empty_container());
823	}
824
825	#[test]
826	fn rules_with_a_filled_block_are_not_marked_empty() {
827		assert!(!first_rule_metadata("a { color: red }").is_empty_container());
828		assert!(!first_rule_metadata("nav { a {} }").is_empty_container());
829		assert!(!first_rule_metadata("@media screen { a { color: red } }").is_empty_container());
830		assert!(!first_rule_metadata("@font-face { font-display: swap }").is_empty_container());
831	}
832
833	#[test]
834	fn a_rule_is_either_inert_or_effective_never_both() {
835		for (css, inert) in [("a {}", true), ("a { color: red }", false), ("nav { a {} }", true)] {
836			let meta = first_rule_metadata(css);
837			assert_eq!(meta.is_inert(), inert, "{css}");
838			assert_eq!(meta.has_effect(), !inert, "{css}");
839		}
840	}
841
842	#[test]
843	fn a_subtree_can_hold_both_inert_and_effective_nodes() {
844		// Node kinds merge with OR, so for anything but a node's own metadata the two bits answer
845		// separate existential questions and are not complements: this sheet holds one of each.
846		// Deciding a rule is inert needs "holds nothing effective", which the `Inert` bit alone
847		// cannot answer.
848		let meta = stylesheet_metadata("a {}\nb { color: red }");
849		assert!(meta.is_inert());
850		assert!(meta.has_effect());
851	}
852
853	#[test]
854	fn rule_holding_only_empty_rules_is_inert_but_not_empty() {
855		let meta = first_rule_metadata("nav { a {} }");
856		assert!(meta.is_inert());
857		// The block holds a rule, so it is not an empty container in the literal sense `:empty`
858		// matches on.
859		assert!(!meta.is_empty_container());
860	}
861
862	#[test]
863	fn rule_with_unknown_value_has_effect() {
864		let meta = first_rule_metadata("a { color: fnord }");
865		assert!(meta.has_effect());
866		assert!(!meta.is_inert());
867	}
868
869	#[test]
870	fn at_rule_descriptors_have_effect() {
871		let meta = first_rule_metadata("@font-face { font-display: swap }");
872		assert!(meta.has_effect());
873		assert!(!meta.is_inert());
874	}
875
876	#[test]
877	fn keyframes_with_only_empty_keyframes_is_inert() {
878		let meta = first_rule_metadata("@keyframes fade { 0% {} 100% {} }");
879		assert!(meta.is_inert());
880	}
881
882	#[test]
883	fn statement_at_rule_keeps_containing_rule_effective() {
884		let meta = first_rule_metadata("@media screen { @layer a; }");
885		assert!(meta.has_effect());
886		assert!(!meta.is_inert());
887	}
888
889	#[test]
890	fn unmarked_block_rules_are_never_inert() {
891		// `@layer a {}` declares layer order, so it must survive even when empty.
892		let meta = first_rule_metadata("@layer a {}");
893		assert!(meta.has_effect());
894		assert!(!meta.is_inert());
895	}
896
897	// Child leaf types carrying distinct node_kinds bits, used to verify delegation
898	// propagates and merges children's metadata upward.
899	#[derive(csskit_derives::NodeWithMetadata)]
900	#[metadata(node_kinds = StyleRule)]
901	struct ChildA;
902
903	#[derive(csskit_derives::NodeWithMetadata)]
904	#[metadata(node_kinds = AtRule)]
905	struct ChildB;
906
907	// Structs merge every field's metadata into self_metadata.
908	#[derive(csskit_derives::NodeWithMetadata)]
909	#[metadata(node_kinds = Function)]
910	struct StructParent {
911		a: ChildA,
912		b: ChildB,
913	}
914
915	// Fields marked #[metadata(skip)] contribute nothing.
916	#[derive(csskit_derives::NodeWithMetadata)]
917	struct StructSkippedField {
918		a: ChildA,
919		#[metadata(skip)]
920		#[allow(dead_code)]
921		b: ChildB,
922	}
923
924	// Enums merge the active variant's fields into self_metadata.
925	#[derive(csskit_derives::NodeWithMetadata)]
926	enum EnumParent {
927		Named { a: ChildA, b: ChildB },
928		Tuple(ChildA),
929		Empty,
930	}
931
932	#[test]
933	fn test_struct_merges_all_fields() {
934		let node = StructParent { a: ChildA, b: ChildB };
935		let meta = node.metadata();
936		// self_metadata bit plus both children.
937		assert!(meta.node_kinds.contains(NodeKinds::Function));
938		assert!(meta.node_kinds.contains(NodeKinds::StyleRule));
939		assert!(meta.node_kinds.contains(NodeKinds::AtRule));
940	}
941
942	#[test]
943	fn test_struct_skips_marked_field() {
944		let meta = StructSkippedField { a: ChildA, b: ChildB }.metadata();
945		assert!(meta.node_kinds.contains(NodeKinds::StyleRule));
946		assert!(!meta.node_kinds.contains(NodeKinds::AtRule));
947	}
948
949	#[test]
950	fn test_enum_named_variant() {
951		let meta = EnumParent::Named { a: ChildA, b: ChildB }.metadata();
952		assert!(meta.node_kinds.contains(NodeKinds::StyleRule));
953		assert!(meta.node_kinds.contains(NodeKinds::AtRule));
954		assert!(!meta.node_kinds.contains(NodeKinds::Function));
955	}
956
957	#[test]
958	fn test_enum_tuple_variant() {
959		let meta = EnumParent::Tuple(ChildA).metadata();
960		assert!(meta.node_kinds.contains(NodeKinds::StyleRule));
961		assert!(!meta.node_kinds.contains(NodeKinds::AtRule));
962	}
963
964	#[test]
965	fn test_enum_empty_variant() {
966		let meta = EnumParent::Empty.metadata();
967		assert!(meta.is_empty());
968	}
969
970	#[test]
971	fn test_vendor_prefixes_try_from() {
972		// Vendor-prefixed atoms should convert successfully
973		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_WebkitTransform), Ok(VendorPrefixes::WebKit));
974		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_WebkitAnimation), Ok(VendorPrefixes::WebKit));
975		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_WebkitLineClamp), Ok(VendorPrefixes::WebKit));
976
977		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_MozAppearance), Ok(VendorPrefixes::Moz));
978		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_MozAny), Ok(VendorPrefixes::Moz));
979
980		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_MsFullscreen), Ok(VendorPrefixes::Ms));
981		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_MsBackdrop), Ok(VendorPrefixes::Ms));
982
983		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_OPlaceholder), Ok(VendorPrefixes::O));
984		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_OScrollbar), Ok(VendorPrefixes::O));
985
986		// Non-vendor atoms should fail
987		assert_eq!(VendorPrefixes::try_from(CssAtomSet::Px), Err(()));
988		assert_eq!(VendorPrefixes::try_from(CssAtomSet::Em), Err(()));
989		assert_eq!(VendorPrefixes::try_from(CssAtomSet::Auto), Err(()));
990		assert_eq!(VendorPrefixes::try_from(CssAtomSet::Transform), Err(()));
991	}
992
993	#[test]
994	fn size_baseline_css_metadata() {
995		// `property_groups` is a u128 bitmask, so CssMetadata is align 16 and rounds up to 48 bytes.
996		// The payload is well under that, leaving spare bytes for new flag fields at no cost.
997		assert_eq!(std::mem::size_of::<CssMetadata>(), 48);
998	}
999
1000	#[test]
1001	fn test_substitution_fields_default() {
1002		let meta = CssMetadata::default();
1003		assert!(!meta.uses_substitution);
1004		assert_eq!(meta.expected_value_kinds, CssTypes::none());
1005		assert!(meta.is_empty());
1006	}
1007
1008	#[test]
1009	fn test_substitution_fields_merge() {
1010		let meta1 = CssMetadata {
1011			uses_substitution: true,
1012			expected_value_kinds: CssTypes::Length | CssTypes::Percentage,
1013			..Default::default()
1014		};
1015
1016		let meta2 = CssMetadata { expected_value_kinds: CssTypes::Color, ..Default::default() };
1017
1018		let merged = NodeMetadata::merge(meta1, meta2);
1019		assert!(merged.uses_substitution);
1020		assert!(merged.expected_value_kinds.contains(CssTypes::Length));
1021		assert!(merged.expected_value_kinds.contains(CssTypes::Percentage));
1022		assert!(merged.expected_value_kinds.contains(CssTypes::Color));
1023	}
1024
1025	#[test]
1026	fn test_has_substitution() {
1027		let mut meta = CssMetadata::default();
1028		assert!(!meta.has_substitution());
1029		meta.uses_substitution = true;
1030		assert!(meta.has_substitution());
1031	}
1032
1033	#[test]
1034	fn test_is_empty_with_substitution() {
1035		let mut meta = CssMetadata::default();
1036		assert!(meta.is_empty());
1037		meta.uses_substitution = true;
1038		assert!(!meta.is_empty());
1039		let meta2 = CssMetadata { expected_value_kinds: CssTypes::Number, ..Default::default() };
1040		assert!(!meta2.is_empty());
1041	}
1042}