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 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 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 Important,
122 CssWideKeywords,
124 Custom,
126 Computed,
128 Shorthands,
130 Longhands,
132}
133
134#[bitmask(u32)]
136#[bitmask_config(vec_debug)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138pub enum NodeKinds {
139 Unknown,
141 StyleRule,
143 AtRule,
145 Declaration,
147 Function,
149 EmptyPrelude,
151 EmptyBlock,
153 Nested,
155 Deprecated,
157 Experimental,
159 NonStandard,
161 Dimension,
163 Custom,
165 Effective,
168 Inert,
171}
172
173#[bitmask(u8)]
176#[bitmask_config(vec_debug)]
177#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
178pub enum PropertyKind {
179 Name,
181}
182
183pub const PROPERTY_KIND_VARIANTS: &[PropertyKind] = &[PropertyKind::Name];
185
186#[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 pub const ANY: CssTypes = CssTypes { bits: !0 };
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
221#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
222pub struct CssMetadata {
223 pub property_groups: PropertyGroup,
225 pub applies_to: AppliesTo,
227 pub box_sides: BoxSide,
229 pub box_portions: BoxPortion,
231 pub declaration_kinds: DeclarationKind,
233 pub used_at_rules: AtRuleId,
235 pub vendor_prefixes: VendorPrefixes,
237 pub node_kinds: NodeKinds,
239 pub property_kinds: PropertyKind,
241 pub value_kinds: CssTypes,
243 pub unitless_zero_resolves: UnitlessZeroResolves,
245 pub size: u16,
247 pub uses_substitution: bool,
250 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 #[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 #[inline]
298 pub fn modifies_box(&self) -> bool {
299 !self.box_portions.is_none()
300 }
301
302 #[inline]
304 pub fn has_important(&self) -> bool {
305 self.declaration_kinds.contains(DeclarationKind::Important)
306 }
307
308 #[inline]
310 pub fn has_custom_properties(&self) -> bool {
311 self.declaration_kinds.contains(DeclarationKind::Custom)
312 }
313
314 #[inline]
316 pub fn has_computed(&self) -> bool {
317 self.declaration_kinds.contains(DeclarationKind::Computed)
318 }
319
320 #[inline]
322 pub fn has_shorthands(&self) -> bool {
323 self.declaration_kinds.contains(DeclarationKind::Shorthands)
324 }
325
326 #[inline]
328 pub fn has_longhands(&self) -> bool {
329 self.declaration_kinds.contains(DeclarationKind::Longhands)
330 }
331
332 #[inline]
334 pub fn has_unknown(&self) -> bool {
335 self.node_kinds.contains(NodeKinds::Unknown)
336 }
337
338 #[inline]
340 pub fn has_vendor_prefixes(&self) -> bool {
341 !self.vendor_prefixes.is_none()
342 }
343
344 #[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 #[inline]
356 pub fn has_rules(&self) -> bool {
357 self.node_kinds.intersects(NodeKinds::StyleRule | NodeKinds::AtRule)
358 }
359
360 #[inline]
362 pub fn has_style_rules(&self) -> bool {
363 self.node_kinds.contains(NodeKinds::StyleRule)
364 }
365
366 #[inline]
368 pub fn has_at_rules(&self) -> bool {
369 self.node_kinds.contains(NodeKinds::AtRule)
370 }
371
372 #[inline]
374 pub fn has_functions(&self) -> bool {
375 self.node_kinds.contains(NodeKinds::Function)
376 }
377
378 #[inline]
380 pub fn is_deprecated(&self) -> bool {
381 self.node_kinds.contains(NodeKinds::Deprecated)
382 }
383
384 #[inline]
386 pub fn is_experimental(&self) -> bool {
387 self.node_kinds.contains(NodeKinds::Experimental)
388 }
389
390 #[inline]
392 pub fn is_non_standard(&self) -> bool {
393 self.node_kinds.contains(NodeKinds::NonStandard)
394 }
395
396 #[inline]
398 pub fn is_dimension(&self) -> bool {
399 self.node_kinds.contains(NodeKinds::Dimension)
400 }
401
402 #[inline]
404 pub fn has_property_kind(&self, kind: PropertyKind) -> bool {
405 self.property_kinds.contains(kind)
406 }
407
408 #[inline]
410 pub fn has_substitution(&self) -> bool {
411 self.uses_substitution
412 }
413
414 #[inline]
416 pub fn has_value_kinds(&self, kinds: CssTypes) -> bool {
417 self.value_kinds.intersects(kinds)
418 }
419
420 #[inline]
422 pub fn is_empty_container(&self) -> bool {
423 self.node_kinds.contains(NodeKinds::EmptyBlock)
424 }
425
426 #[inline]
428 pub fn has_effect(&self) -> bool {
429 self.node_kinds.contains(NodeKinds::Effective)
430 }
431
432 #[inline]
438 pub fn is_inert(&self) -> bool {
439 self.node_kinds.contains(NodeKinds::Inert)
440 }
441
442 #[inline]
444 pub fn is_nested(&self) -> bool {
445 self.node_kinds.contains(NodeKinds::Nested)
446 }
447
448 #[inline]
451 pub fn has_empty_prelude(&self) -> bool {
452 self.node_kinds.contains(NodeKinds::EmptyPrelude)
453 }
454
455 #[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 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
504impl 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
514impl 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 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 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 let meta = first_rule_metadata("@layer a {}");
890 assert!(meta.has_effect());
891 assert!(!meta.is_inert());
892 }
893
894 #[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 #[derive(csskit_derives::NodeWithMetadata)]
906 #[metadata(node_kinds = Function)]
907 struct StructParent {
908 a: ChildA,
909 b: ChildB,
910 }
911
912 #[derive(csskit_derives::NodeWithMetadata)]
914 struct StructSkippedField {
915 a: ChildA,
916 #[metadata(skip)]
917 #[allow(dead_code)]
918 b: ChildB,
919 }
920
921 #[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 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 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 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 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}