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