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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub enum UnitlessZeroResolves {
27 #[default]
29 Length,
30 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 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 Important,
124 CssWideKeywords,
126 Custom,
128 Computed,
130 Shorthands,
132 Longhands,
134}
135
136#[bitmask(u32)]
138#[bitmask_config(vec_debug)]
139#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
140pub enum NodeKinds {
141 Unknown,
143 StyleRule,
145 AtRule,
147 Declaration,
149 Function,
151 EmptyPrelude,
153 EmptyBlock,
155 Nested,
157 Deprecated,
159 Experimental,
161 NonStandard,
163 Dimension,
165 Custom,
167 Effective,
170 Inert,
173}
174
175#[bitmask(u8)]
178#[bitmask_config(vec_debug)]
179#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
180pub enum PropertyKind {
181 Name,
183}
184
185pub const PROPERTY_KIND_VARIANTS: &[PropertyKind] = &[PropertyKind::Name];
187
188#[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 pub const ANY: CssTypes = CssTypes { bits: !0 };
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
223#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
224pub struct CssMetadata {
225 pub property_groups: PropertyGroup,
227 pub applies_to: AppliesTo,
229 pub box_sides: BoxSide,
231 pub box_portions: BoxPortion,
233 pub declaration_kinds: DeclarationKind,
235 pub used_at_rules: AtRuleId,
237 pub vendor_prefixes: VendorPrefixes,
239 pub node_kinds: NodeKinds,
241 pub property_kinds: PropertyKind,
243 pub value_kinds: CssTypes,
245 pub unitless_zero_resolves: UnitlessZeroResolves,
247 pub size: u16,
249 pub uses_substitution: bool,
252 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 #[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 #[inline]
300 pub fn modifies_box(&self) -> bool {
301 !self.box_portions.is_none()
302 }
303
304 #[inline]
306 pub fn has_important(&self) -> bool {
307 self.declaration_kinds.contains(DeclarationKind::Important)
308 }
309
310 #[inline]
312 pub fn has_custom_properties(&self) -> bool {
313 self.declaration_kinds.contains(DeclarationKind::Custom)
314 }
315
316 #[inline]
318 pub fn has_computed(&self) -> bool {
319 self.declaration_kinds.contains(DeclarationKind::Computed)
320 }
321
322 #[inline]
324 pub fn has_shorthands(&self) -> bool {
325 self.declaration_kinds.contains(DeclarationKind::Shorthands)
326 }
327
328 #[inline]
330 pub fn has_longhands(&self) -> bool {
331 self.declaration_kinds.contains(DeclarationKind::Longhands)
332 }
333
334 #[inline]
336 pub fn has_unknown(&self) -> bool {
337 self.node_kinds.contains(NodeKinds::Unknown)
338 }
339
340 #[inline]
342 pub fn has_vendor_prefixes(&self) -> bool {
343 !self.vendor_prefixes.is_none()
344 }
345
346 #[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 #[inline]
358 pub fn has_rules(&self) -> bool {
359 self.node_kinds.intersects(NodeKinds::StyleRule | NodeKinds::AtRule)
360 }
361
362 #[inline]
364 pub fn has_style_rules(&self) -> bool {
365 self.node_kinds.contains(NodeKinds::StyleRule)
366 }
367
368 #[inline]
370 pub fn has_at_rules(&self) -> bool {
371 self.node_kinds.contains(NodeKinds::AtRule)
372 }
373
374 #[inline]
376 pub fn has_functions(&self) -> bool {
377 self.node_kinds.contains(NodeKinds::Function)
378 }
379
380 #[inline]
382 pub fn is_deprecated(&self) -> bool {
383 self.node_kinds.contains(NodeKinds::Deprecated)
384 }
385
386 #[inline]
388 pub fn is_experimental(&self) -> bool {
389 self.node_kinds.contains(NodeKinds::Experimental)
390 }
391
392 #[inline]
394 pub fn is_non_standard(&self) -> bool {
395 self.node_kinds.contains(NodeKinds::NonStandard)
396 }
397
398 #[inline]
400 pub fn is_dimension(&self) -> bool {
401 self.node_kinds.contains(NodeKinds::Dimension)
402 }
403
404 #[inline]
406 pub fn has_property_kind(&self, kind: PropertyKind) -> bool {
407 self.property_kinds.contains(kind)
408 }
409
410 #[inline]
412 pub fn has_substitution(&self) -> bool {
413 self.uses_substitution
414 }
415
416 #[inline]
418 pub fn has_value_kinds(&self, kinds: CssTypes) -> bool {
419 self.value_kinds.intersects(kinds)
420 }
421
422 #[inline]
424 pub fn is_empty_container(&self) -> bool {
425 self.node_kinds.contains(NodeKinds::EmptyBlock)
426 }
427
428 #[inline]
430 pub fn has_effect(&self) -> bool {
431 self.node_kinds.contains(NodeKinds::Effective)
432 }
433
434 #[inline]
440 pub fn is_inert(&self) -> bool {
441 self.node_kinds.contains(NodeKinds::Inert)
442 }
443
444 #[inline]
446 pub fn is_nested(&self) -> bool {
447 self.node_kinds.contains(NodeKinds::Nested)
448 }
449
450 #[inline]
453 pub fn has_empty_prelude(&self) -> bool {
454 self.node_kinds.contains(NodeKinds::EmptyPrelude)
455 }
456
457 #[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 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
506impl 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
516impl 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 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 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 let meta = first_rule_metadata("@layer a {}");
893 assert!(meta.has_effect());
894 assert!(!meta.is_inert());
895 }
896
897 #[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 #[derive(csskit_derives::NodeWithMetadata)]
909 #[metadata(node_kinds = Function)]
910 struct StructParent {
911 a: ChildA,
912 b: ChildB,
913 }
914
915 #[derive(csskit_derives::NodeWithMetadata)]
917 struct StructSkippedField {
918 a: ChildA,
919 #[metadata(skip)]
920 #[allow(dead_code)]
921 b: ChildB,
922 }
923
924 #[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 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 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 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 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}