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::ContainerRule => Some(AtRuleId::Container),
68			Self::CounterStyleRule => Some(AtRuleId::CounterStyle),
69			Self::DocumentRule => Some(AtRuleId::Document),
70			Self::FontFaceRule => Some(AtRuleId::FontFace),
71			Self::KeyframesRule => Some(AtRuleId::Keyframes),
72			Self::LayerRule => Some(AtRuleId::Layer),
73			Self::MediaRule => Some(AtRuleId::Media),
74			Self::MozDocumentRule => Some(AtRuleId::MozDocument),
75			Self::NamespaceRule => Some(AtRuleId::Namespace),
76			Self::PageRule => Some(AtRuleId::Page),
77			Self::PropertyRule => Some(AtRuleId::Property),
78			Self::StartingStyleRule => Some(AtRuleId::StartingStyle),
79			Self::SupportsRule => Some(AtRuleId::Supports),
80			Self::WebkitKeyframesRule => Some(AtRuleId::WebkitKeyframes),
81			_ => None,
82		}
83	}
84}
85
86#[bitmask(u8)]
87#[bitmask_config(vec_debug)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub enum VendorPrefixes {
90	Moz,
91	WebKit,
92	O,
93	Ms,
94}
95
96impl TryFrom<CssAtomSet> for VendorPrefixes {
97	type Error = ();
98	fn try_from(atom: CssAtomSet) -> Result<Self, Self::Error> {
99		const VENDOR_FLAG: u32 = 0b00000000_10000000_00000000_00000000;
100		const VENDORS: [VendorPrefixes; 4] =
101			[VendorPrefixes::WebKit, VendorPrefixes::Moz, VendorPrefixes::Ms, VendorPrefixes::O];
102
103		let atom_bits = atom as u32;
104		if atom_bits & VENDOR_FLAG == 0 {
105			return Err(());
106		}
107		let index = (atom_bits >> 21) & 0b11;
108		Ok(VENDORS[index as usize])
109	}
110}
111
112#[bitmask(u8)]
113#[bitmask_config(vec_debug)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115pub enum DeclarationKind {
116	/// If a declaration has !important
117	Important,
118	/// If a declaration used a css-wide keyword, e.g. `inherit` or `revert-layer`.
119	CssWideKeywords,
120	/// If a declaration is custom, e.g `--foo`
121	Custom,
122	/// If a declaration is computed-time, e.g. using `calc()` or `var()`
123	Computed,
124	/// If a declaration is shorthand
125	Shorthands,
126	/// If a declaration is longhand
127	Longhands,
128}
129
130/// Categories of nodes present in metadata, used for selector filtering.
131#[bitmask(u16)]
132#[bitmask_config(vec_debug)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub enum NodeKinds {
135	/// Contains unknown nodes
136	Unknown,
137	/// Contains style rules
138	StyleRule,
139	/// Contains at-rules (media, keyframes, etc.)
140	AtRule,
141	/// Contains Declarations
142	Declaration,
143	/// Contains function nodes
144	Function,
145	/// Node has an empty prelude
146	EmptyPrelude,
147	/// Node has an empty block (no declarations, no nested rules)
148	EmptyBlock,
149	/// Node is nested within another node
150	Nested,
151	/// Node is deprecated (non-conforming, obsolete)
152	Deprecated,
153	/// Node is experimental (not yet standardized)
154	Experimental,
155	/// Node is non-standard (vendor-specific, not in spec)
156	NonStandard,
157	/// Node is a dimension value (length, angle, time, flex, etc.)
158	Dimension,
159	/// Node is a custom element or custom property
160	Custom,
161}
162
163/// Queryable properties a node exposes for selector matching.
164/// Used by attribute selectors like `[name]` or `[name=value]`.
165#[bitmask(u8)]
166#[bitmask_config(vec_debug)]
167#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
168pub enum PropertyKind {
169	/// Node has a queryable `name` property (declarations, named at-rules, functions)
170	Name,
171}
172
173/// All PropertyKind variants for iteration.
174pub const PROPERTY_KIND_VARIANTS: &[PropertyKind] = &[PropertyKind::Name];
175
176/// Aggregated metadata computed from declarations within a block.
177/// This allows efficient checking of what types of properties a block contains
178/// without iterating through all declarations.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
181pub struct CssMetadata {
182	/// Bitwise OR of all PropertyGroup values
183	pub property_groups: PropertyGroup,
184	/// Bitwise OR of all AppliesTo values
185	pub applies_to: AppliesTo,
186	/// Bitwise OR of all BoxSide values
187	pub box_sides: BoxSide,
188	/// Bitwise OR of all BoxPortion values
189	pub box_portions: BoxPortion,
190	/// Bitwise OR of all DeclarationKind values
191	pub declaration_kinds: DeclarationKind,
192	/// Bitwise OR of all AtRuleIds in a Node
193	pub used_at_rules: AtRuleId,
194	/// Bitwise OR of all VendorPrefixes in a Node
195	pub vendor_prefixes: VendorPrefixes,
196	/// Bitwise OR of node categories present
197	pub node_kinds: NodeKinds,
198	/// Bitwise OR of queryable properties present
199	pub property_kinds: PropertyKind,
200	/// How unitless zero resolves in this context (Length or Number)
201	pub unitless_zero_resolves: UnitlessZeroResolves,
202	/// Size of vector-based nodes (e.g., number of declarations, selector list length)
203	pub size: u16,
204}
205
206impl Default for CssMetadata {
207	fn default() -> Self {
208		Self {
209			property_groups: PropertyGroup::none(),
210			applies_to: AppliesTo::none(),
211			box_sides: BoxSide::none(),
212			box_portions: BoxPortion::none(),
213			declaration_kinds: DeclarationKind::none(),
214			used_at_rules: AtRuleId::none(),
215			vendor_prefixes: VendorPrefixes::none(),
216			node_kinds: NodeKinds::none(),
217			property_kinds: PropertyKind::none(),
218			unitless_zero_resolves: UnitlessZeroResolves::default(),
219			size: 0,
220		}
221	}
222}
223
224impl CssMetadata {
225	/// Returns true if this metadata is empty (contains no properties or at-rules)
226	#[inline]
227	pub fn is_empty(&self) -> bool {
228		self.property_groups == PropertyGroup::none()
229			&& self.applies_to == AppliesTo::none()
230			&& self.box_sides == BoxSide::none()
231			&& self.box_portions == BoxPortion::none()
232			&& self.declaration_kinds == DeclarationKind::none()
233			&& self.used_at_rules == AtRuleId::none()
234			&& self.vendor_prefixes == VendorPrefixes::none()
235			&& self.node_kinds == NodeKinds::none()
236			&& self.property_kinds == PropertyKind::none()
237			&& self.unitless_zero_resolves == UnitlessZeroResolves::Length
238			&& self.size == 0
239	}
240
241	/// Returns true if this block modifies any positioning-related properties.
242	#[inline]
243	pub fn modifies_box(&self) -> bool {
244		!self.box_portions.is_none()
245	}
246
247	/// Returns true if metadata contains important declarations.
248	#[inline]
249	pub fn has_important(&self) -> bool {
250		self.declaration_kinds.contains(DeclarationKind::Important)
251	}
252
253	/// Returns true if metadata contains custom properties.
254	#[inline]
255	pub fn has_custom_properties(&self) -> bool {
256		self.declaration_kinds.contains(DeclarationKind::Custom)
257	}
258
259	/// Returns true if metadata contains computed values.
260	#[inline]
261	pub fn has_computed(&self) -> bool {
262		self.declaration_kinds.contains(DeclarationKind::Computed)
263	}
264
265	/// Returns true if metadata contains shorthand properties.
266	#[inline]
267	pub fn has_shorthands(&self) -> bool {
268		self.declaration_kinds.contains(DeclarationKind::Shorthands)
269	}
270
271	/// Returns true if metadata contains longhand properties.
272	#[inline]
273	pub fn has_longhands(&self) -> bool {
274		self.declaration_kinds.contains(DeclarationKind::Longhands)
275	}
276
277	/// Returns true if metadata contains unknown nodes.
278	#[inline]
279	pub fn has_unknown(&self) -> bool {
280		self.node_kinds.contains(NodeKinds::Unknown)
281	}
282
283	/// Returns true if metadata contains vendor-prefixed properties.
284	#[inline]
285	pub fn has_vendor_prefixes(&self) -> bool {
286		!self.vendor_prefixes.is_none()
287	}
288
289	/// Returns the vendor prefix if exactly one is present, None otherwise.
290	#[inline]
291	pub fn single_vendor_prefix(&self) -> Option<VendorPrefixes> {
292		if self.vendor_prefixes.is_none() || self.vendor_prefixes.bits().count_ones() != 1 {
293			None
294		} else {
295			Some(self.vendor_prefixes)
296		}
297	}
298
299	/// Returns true if metadata contains any rule nodes.
300	#[inline]
301	pub fn has_rules(&self) -> bool {
302		self.node_kinds.intersects(NodeKinds::StyleRule | NodeKinds::AtRule)
303	}
304
305	/// Returns true if metadata contains style rules.
306	#[inline]
307	pub fn has_style_rules(&self) -> bool {
308		self.node_kinds.contains(NodeKinds::StyleRule)
309	}
310
311	/// Returns true if metadata contains at-rules.
312	#[inline]
313	pub fn has_at_rules(&self) -> bool {
314		self.node_kinds.contains(NodeKinds::AtRule)
315	}
316
317	/// Returns true if metadata contains function nodes.
318	#[inline]
319	pub fn has_functions(&self) -> bool {
320		self.node_kinds.contains(NodeKinds::Function)
321	}
322
323	/// Returns true if metadata contains deprecated nodes.
324	#[inline]
325	pub fn is_deprecated(&self) -> bool {
326		self.node_kinds.contains(NodeKinds::Deprecated)
327	}
328
329	/// Returns true if metadata contains experimental nodes.
330	#[inline]
331	pub fn is_experimental(&self) -> bool {
332		self.node_kinds.contains(NodeKinds::Experimental)
333	}
334
335	/// Returns true if metadata contains non-standard nodes.
336	#[inline]
337	pub fn is_non_standard(&self) -> bool {
338		self.node_kinds.contains(NodeKinds::NonStandard)
339	}
340
341	/// Returns true if metadata contains dimension values.
342	#[inline]
343	pub fn is_dimension(&self) -> bool {
344		self.node_kinds.contains(NodeKinds::Dimension)
345	}
346
347	/// Returns true if metadata contains nodes with the given property kind.
348	#[inline]
349	pub fn has_property_kind(&self, kind: PropertyKind) -> bool {
350		self.property_kinds.contains(kind)
351	}
352
353	/// Returns true if this is an empty container (no declarations, no nested rules).
354	#[inline]
355	pub fn is_empty_container(&self) -> bool {
356		self.node_kinds.contains(NodeKinds::EmptyBlock)
357	}
358
359	/// Returns true if this node can be a container (has StyleRule or AtRule kind).
360	#[inline]
361	pub fn can_be_empty(&self) -> bool {
362		self.node_kinds.intersects(NodeKinds::StyleRule | NodeKinds::AtRule)
363	}
364}
365
366impl NodeMetadata for CssMetadata {
367	#[inline]
368	fn merge(mut self, other: Self) -> Self {
369		self.property_groups |= other.property_groups;
370		self.applies_to |= other.applies_to;
371		self.box_sides |= other.box_sides;
372		self.box_portions |= other.box_portions;
373		self.declaration_kinds |= other.declaration_kinds;
374		self.used_at_rules |= other.used_at_rules;
375		self.vendor_prefixes |= other.vendor_prefixes;
376		self.node_kinds |= other.node_kinds;
377		self.property_kinds |= other.property_kinds;
378		// For unitless_zero_resolves, we keep Number if either side has it (conservative)
379		if other.unitless_zero_resolves == UnitlessZeroResolves::Number {
380			self.unitless_zero_resolves = UnitlessZeroResolves::Number;
381		}
382		self.size = self.size.max(other.size);
383		self
384	}
385
386	#[inline]
387	fn with_size(mut self, size: u16) -> Self {
388		self.size = size;
389		self
390	}
391}
392
393// Metadata is not serialized to tokens but providing these simplifies ToCursors/ToSpan impls
394impl ToCursors for CssMetadata {
395	fn to_cursors(&self, _: &mut impl css_parse::CursorSink) {}
396}
397impl ToSpan for CssMetadata {
398	fn to_span(&self) -> Span {
399		Span::DUMMY
400	}
401}
402
403impl SemanticEq for CssMetadata {
404	fn semantic_eq(&self, other: &Self) -> bool {
405		self == other
406	}
407}
408
409#[cfg(test)]
410mod tests {
411	use super::*;
412	use crate::{CssAtomSet, StyleSheet};
413	use css_lexer::Lexer;
414	use css_parse::{NodeMetadata, NodeWithMetadata, Parser};
415
416	#[test]
417	fn test_block_metadata_merge() {
418		let mut meta1 = CssMetadata::default();
419		meta1.property_groups = PropertyGroup::Color;
420		meta1.declaration_kinds = DeclarationKind::Important;
421
422		let mut meta2 = CssMetadata::default();
423		meta2.property_groups = PropertyGroup::Position;
424		meta2.declaration_kinds = DeclarationKind::Custom;
425
426		let merged = meta1.merge(meta2);
427
428		assert!(merged.property_groups.contains(PropertyGroup::Color));
429		assert!(merged.property_groups.contains(PropertyGroup::Position));
430		assert!(merged.declaration_kinds.contains(DeclarationKind::Important));
431		assert!(merged.declaration_kinds.contains(DeclarationKind::Custom));
432	}
433
434	#[test]
435	fn test_stylesheet_metadata_simple() {
436		let css = "body { color: red; width: 100px; }";
437		let bump = bumpalo::Bump::new();
438		let lexer = Lexer::new(&CssAtomSet::ATOMS, css);
439		let mut parser = Parser::new(&bump, css, lexer);
440		let stylesheet = parser.parse::<StyleSheet>().unwrap();
441
442		let metadata = stylesheet.metadata();
443
444		assert!(metadata.property_groups.contains(PropertyGroup::Color));
445		assert!(metadata.property_groups.contains(PropertyGroup::Sizing));
446		assert!(metadata.modifies_box());
447		assert!(metadata.has_longhands());
448	}
449
450	#[test]
451	fn test_stylesheet_metadata_with_important() {
452		let css = "body { color: red !important; }";
453		let bump = bumpalo::Bump::new();
454		let lexer = Lexer::new(&CssAtomSet::ATOMS, css);
455		let mut parser = Parser::new(&bump, css, lexer);
456		let stylesheet = parser.parse::<StyleSheet>().unwrap();
457
458		let metadata = stylesheet.metadata();
459
460		assert!(metadata.has_important());
461		assert!(metadata.property_groups.contains(PropertyGroup::Color));
462	}
463
464	#[test]
465	fn test_stylesheet_metadata_custom_properties() {
466		let css = "body { --custom: value; }";
467		let bump = bumpalo::Bump::new();
468		let lexer = Lexer::new(&CssAtomSet::ATOMS, css);
469		let mut parser = Parser::new(&bump, css, lexer);
470		let stylesheet = parser.parse::<StyleSheet>().unwrap();
471
472		let metadata = stylesheet.metadata();
473
474		assert!(metadata.has_custom_properties());
475	}
476
477	#[test]
478	fn test_stylesheet_metadata_nested_media() {
479		let css = "@media screen { body { color: red; } }";
480		let bump = bumpalo::Bump::new();
481		let lexer = Lexer::new(&CssAtomSet::ATOMS, css);
482		let mut parser = Parser::new(&bump, css, lexer);
483		let stylesheet = parser.parse::<StyleSheet>().unwrap();
484
485		let metadata = stylesheet.metadata();
486
487		assert!(metadata.property_groups.contains(PropertyGroup::Color));
488		assert!(metadata.used_at_rules.contains(AtRuleId::Media));
489	}
490
491	#[test]
492	fn test_vendor_prefixes_try_from() {
493		// Vendor-prefixed atoms should convert successfully
494		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_WebkitTransform), Ok(VendorPrefixes::WebKit));
495		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_WebkitAnimation), Ok(VendorPrefixes::WebKit));
496		assert_eq!(VendorPrefixes::try_from(CssAtomSet::WebkitLineClamp), Ok(VendorPrefixes::WebKit));
497
498		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_MozAppearance), Ok(VendorPrefixes::Moz));
499		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_MozAny), Ok(VendorPrefixes::Moz));
500
501		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_MsFullscreen), Ok(VendorPrefixes::Ms));
502		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_MsBackdrop), Ok(VendorPrefixes::Ms));
503
504		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_OPlaceholder), Ok(VendorPrefixes::O));
505		assert_eq!(VendorPrefixes::try_from(CssAtomSet::_OScrollbar), Ok(VendorPrefixes::O));
506
507		// Non-vendor atoms should fail
508		assert_eq!(VendorPrefixes::try_from(CssAtomSet::Px), Err(()));
509		assert_eq!(VendorPrefixes::try_from(CssAtomSet::Em), Err(()));
510		assert_eq!(VendorPrefixes::try_from(CssAtomSet::Auto), Err(()));
511		assert_eq!(VendorPrefixes::try_from(CssAtomSet::Transform), Err(()));
512	}
513}