1use alloc::{string::String, vec::Vec};
12use core::fmt;
13
14use crate::{
15 corety::OptionString,
16 dynamic_selector::DynamicSelectorVec,
17 props::property::{CssProperty, CssPropertyType},
18 AzString,
19};
20
21#[derive(Debug, Default, PartialEq, Clone)]
29#[repr(C)]
30pub struct Css {
31 pub rules: CssRuleBlockVec,
34 pub keyframes: KeyframesVec,
39}
40
41#[derive(Debug, Default, PartialEq, Eq, Clone)]
47#[repr(C)]
48pub struct KeyframeStop {
49 pub permille: u16,
51 pub props: crate::props::property::CssPropertyVec,
53}
54
55#[derive(Debug, Default, PartialEq, Eq, Clone)]
57#[repr(C)]
58pub struct Keyframes {
59 pub name: AzString,
60 pub stops: KeyframeStopVec,
62}
63
64crate::impl_vec!(
65 KeyframeStop,
66 KeyframeStopVec,
67 KeyframeStopVecDestructor,
68 KeyframeStopVecDestructorType,
69 KeyframeStopVecSlice,
70 OptionKeyframeStop
71);
72crate::impl_vec_mut!(KeyframeStop, KeyframeStopVec);
73crate::impl_vec_debug!(KeyframeStop, KeyframeStopVec);
74crate::impl_vec_clone!(KeyframeStop, KeyframeStopVec, KeyframeStopVecDestructor);
75crate::impl_vec_partialeq!(KeyframeStop, KeyframeStopVec);
76crate::impl_vec_eq!(KeyframeStop, KeyframeStopVec);
77crate::impl_option!(
78 KeyframeStop,
79 OptionKeyframeStop,
80 copy = false,
81 [Debug, Clone, PartialEq, Eq]
82);
83
84crate::impl_vec!(
85 Keyframes,
86 KeyframesVec,
87 KeyframesVecDestructor,
88 KeyframesVecDestructorType,
89 KeyframesVecSlice,
90 OptionKeyframes
91);
92crate::impl_vec_mut!(Keyframes, KeyframesVec);
93crate::impl_vec_debug!(Keyframes, KeyframesVec);
94crate::impl_vec_clone!(Keyframes, KeyframesVec, KeyframesVecDestructor);
95crate::impl_vec_partialeq!(Keyframes, KeyframesVec);
96crate::impl_vec_eq!(Keyframes, KeyframesVec);
97crate::impl_option!(
98 Keyframes,
99 OptionKeyframes,
100 copy = false,
101 [Debug, Clone, PartialEq, Eq]
102);
103
104impl_option!(
105 Css,
106 OptionCss,
107 copy = false,
108 [Debug, Clone, PartialEq, Eq, PartialOrd]
109);
110
111impl_vec!(
112 Css,
113 CssVec,
114 CssVecDestructor,
115 CssVecDestructorType,
116 CssVecSlice,
117 OptionCss
118);
119impl_vec_mut!(Css, CssVec);
120impl_vec_debug!(Css, CssVec);
121impl_vec_partialord!(Css, CssVec);
122impl_vec_clone!(Css, CssVec, CssVecDestructor);
123impl_vec_partialeq!(Css, CssVec);
124
125impl_vec!(
126 CssRuleBlock,
127 CssRuleBlockVec,
128 CssRuleBlockVecDestructor,
129 CssRuleBlockVecDestructorType,
130 CssRuleBlockVecSlice,
131 OptionCssRuleBlock
132);
133impl_vec_mut!(CssRuleBlock, CssRuleBlockVec);
134impl_vec_debug!(CssRuleBlock, CssRuleBlockVec);
135impl_vec_partialord!(CssRuleBlock, CssRuleBlockVec);
136impl_vec_clone!(CssRuleBlock, CssRuleBlockVec, CssRuleBlockVecDestructor);
137impl_vec_partialeq!(CssRuleBlock, CssRuleBlockVec);
138
139impl Css {
140 #[must_use]
145 pub fn viewport_breakpoints(&self) -> (Vec<f32>, Vec<f32>) {
146 let mut w = Vec::new();
147 let mut h = Vec::new();
148 for rule in self.rules.as_ref() {
149 crate::dynamic_selector::collect_viewport_thresholds(
150 rule.conditions.as_ref(),
151 &mut w,
152 &mut h,
153 );
154 }
155 w.sort_by_key(|v| v.to_bits());
156 w.dedup_by_key(|v| v.to_bits());
157 h.sort_by_key(|v| v.to_bits());
158 h.dedup_by_key(|v| v.to_bits());
159 (w, h)
160 }
161
162 #[must_use]
163 pub fn is_empty(&self) -> bool {
164 self.rules.as_ref().is_empty()
165 }
166
167 #[must_use]
168 pub fn new(rules: Vec<CssRuleBlock>) -> Self {
169 Self {
170 rules: rules.into(),
171 keyframes: KeyframesVec::from_const_slice(&[]),
172 }
173 }
174
175 #[cfg(feature = "parser")]
176 #[allow(clippy::needless_pass_by_value)]
179 #[must_use]
180 pub fn from_string(s: AzString) -> Self {
181 crate::parser2::new_from_str(s.as_str()).0
182 }
183
184 #[cfg(feature = "parser")]
192 #[must_use]
193 pub fn parse_inline(style: &str) -> Self {
194 use alloc::string::ToString;
195 let mut wrapped = String::with_capacity(style.len() + 6);
196 wrapped.push_str("* {\n");
197 wrapped.push_str(style);
198 wrapped.push_str("\n}");
199 let (mut css, _warnings) = crate::parser2::new_from_str(&wrapped);
200 css.rules.retain(|rule| {
206 matches!(
207 rule.path.selectors.as_ref().first(),
208 None | Some(CssPathSelector::Global)
209 )
210 });
211 for rule in css.rules.as_mut() {
212 rule.priority = rule_priority::INLINE;
213 }
214 css
215 }
216
217 #[cfg(feature = "parser")]
218 #[allow(clippy::needless_pass_by_value)]
221 #[must_use]
222 pub fn from_string_with_warnings(
223 s: AzString,
224 ) -> (Self, Vec<crate::parser2::CssParseWarnMsgOwned>) {
225 let (css, warnings) = crate::parser2::new_from_str(s.as_str());
226 (
227 css,
228 warnings
229 .into_iter()
230 .map(|w| crate::parser2::CssParseWarnMsgOwned {
231 warning: w.warning.to_contained(),
232 location: w.location,
233 })
234 .collect(),
235 )
236 }
237}
238
239impl From<Vec<CssRuleBlock>> for Css {
240 fn from(rules: Vec<CssRuleBlock>) -> Self {
241 Self {
242 rules: rules.into(),
243 keyframes: KeyframesVec::from_const_slice(&[]),
244 }
245 }
246}
247
248impl Eq for Css {}
252impl PartialOrd for Css {
255 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
256 Some(self.cmp(other))
257 }
258}
259impl Ord for Css {
260 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
261 self.rules.as_ref().len().cmp(&other.rules.as_ref().len())
262 }
263}
264impl Eq for CssRuleBlock {}
265impl Ord for CssRuleBlock {
266 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
267 self.path
271 .cmp(&other.path)
272 .then_with(|| self.declarations.cmp(&other.declarations))
273 }
274}
275
276impl From<crate::dynamic_selector::CssPropertyWithConditionsVec> for Css {
285 fn from(props: crate::dynamic_selector::CssPropertyWithConditionsVec) -> Self {
286 let owned = props.into_library_owned_vec();
294 let mut rules: Vec<CssRuleBlock> = Vec::with_capacity(owned.len());
295 for p in owned {
296 rules.push(CssRuleBlock {
297 path: CssPath {
298 selectors: Vec::new().into(),
299 },
300 declarations: alloc::vec![CssDeclaration::Static(p.property)].into(),
301 conditions: p.apply_if,
302 priority: rule_priority::INLINE,
303 });
304 }
305 Self {
306 rules: rules.into(),
307 keyframes: KeyframesVec::from_const_slice(&[]),
308 }
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
314#[repr(C, u8)]
315pub enum CssDeclaration {
316 Static(CssProperty),
318 Dynamic(DynamicCssProperty),
320}
321
322impl_option!(
323 CssDeclaration,
324 OptionCssDeclaration,
325 copy = false,
326 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
327);
328
329impl CssDeclaration {
330 #[must_use]
331 pub const fn new_static(prop: CssProperty) -> Self {
332 Self::Static(prop)
333 }
334
335 #[must_use]
336 pub const fn new_dynamic(prop: DynamicCssProperty) -> Self {
337 Self::Dynamic(prop)
338 }
339
340 #[must_use]
342 pub const fn get_type(&self) -> CssPropertyType {
343 use self::CssDeclaration::{Dynamic, Static};
344 match self {
345 Static(s) => s.get_type(),
346 Dynamic(d) => d.default_value.get_type(),
347 }
348 }
349
350 #[must_use]
353 pub const fn is_inheritable(&self) -> bool {
354 use self::CssDeclaration::{Dynamic, Static};
355 match self {
356 Static(s) => s.get_type().is_inheritable(),
357 Dynamic(d) => d.is_inheritable(),
358 }
359 }
360
361 #[must_use]
364 pub const fn can_trigger_relayout(&self) -> bool {
365 use self::CssDeclaration::{Dynamic, Static};
366 match self {
367 Static(s) => s.get_type().can_trigger_relayout(),
368 Dynamic(d) => d.can_trigger_relayout(),
369 }
370 }
371
372 #[must_use]
373 pub fn to_str(&self) -> String {
374 use self::CssDeclaration::{Dynamic, Static};
375 match self {
376 Static(s) => format!("{s:?}"),
377 Dynamic(d) => match self.env_variable() {
378 Some(v) => format!("env({}, {:?})", v.as_css_name(), d.default_value),
379 None => format!("var(--{}, {:?})", d.dynamic_id, d.default_value),
380 },
381 }
382 }
383
384 #[must_use]
391 pub fn env_variable(&self) -> Option<crate::dynamic_selector::EnvVariable> {
392 match self {
393 Self::Static(_) => None,
394 Self::Dynamic(d) => {
395 crate::dynamic_selector::EnvVariable::from_dynamic_id(d.dynamic_id.as_str())
396 }
397 }
398 }
399
400 #[must_use]
404 pub fn is_cascade_resolvable(&self) -> bool {
405 matches!(self, Self::Static(_)) || self.env_variable().is_some()
406 }
407
408 #[must_use]
413 pub fn depends_on_dynamic_context(&self) -> bool {
414 self.env_variable().is_some()
415 }
416
417 #[must_use]
430 pub fn resolve_in_cascade(
431 &self,
432 ctx: Option<&crate::dynamic_selector::DynamicSelectorContext>,
433 ) -> Option<CssProperty> {
434 match self {
435 Self::Static(s) => Some(s.clone()),
436 Self::Dynamic(d) => {
437 let var = self.env_variable()?;
438 let Some(px) = ctx.and_then(|c| var.resolve(c)) else {
439 return Some(d.default_value.clone());
440 };
441 Some(Self::env_length_as(&d.default_value, px))
442 }
443 }
444 }
445
446 #[allow(unused_variables)]
452 fn env_length_as(like: &CssProperty, px: f32) -> CssProperty {
453 #[cfg(feature = "parser")]
454 {
455 if let Ok(p) =
456 crate::props::property::parse_css_property(like.get_type(), &format!("{px}px"))
457 {
458 return p;
459 }
460 }
461 like.clone()
462 }
463}
464
465#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
486#[repr(C)]
487pub struct DynamicCssProperty {
488 pub dynamic_id: AzString,
490 pub default_value: CssProperty,
492}
493
494#[repr(C, u8)]
504pub enum BoxOrStatic<T> {
505 Boxed(*mut T),
507 Static(*const T),
509}
510
511impl<T> BoxOrStatic<T> {
512 #[inline]
514 pub fn heap(value: T) -> Self {
515 Self::Boxed(Box::into_raw(Box::new(value)))
516 }
517
518 #[inline]
524 #[must_use]
525 pub fn as_ref(&self) -> &T {
526 match self {
527 Self::Boxed(ptr) => unsafe {
528 debug_assert!(
529 !ptr.is_null(),
530 "BoxOrStatic::Boxed contained a null pointer"
531 );
532 &**ptr
533 },
534 Self::Static(ptr) => unsafe {
535 debug_assert!(
536 !ptr.is_null(),
537 "BoxOrStatic::Static contained a null pointer"
538 );
539 &**ptr
540 },
541 }
542 }
543
544 #[inline]
551 pub fn as_mut(&mut self) -> &mut T {
552 match self {
553 Self::Boxed(ptr) => unsafe { &mut **ptr },
554 Self::Static(_) => panic!("Cannot mutate a static BoxOrStatic value"),
555 }
556 }
557
558 #[inline]
560 #[must_use]
561 pub fn into_inner(self) -> T
562 where
563 T: Clone,
564 {
565 self.as_ref().clone()
570 }
571}
572
573impl<T> Drop for BoxOrStatic<T> {
574 fn drop(&mut self) {
575 if let Self::Boxed(ptr) = self {
576 if !ptr.is_null() {
577 unsafe {
578 drop(Box::from_raw(*ptr));
579 }
580 *ptr = core::ptr::null_mut();
581 }
582 }
583 }
584}
585
586impl<T: Clone> Clone for BoxOrStatic<T> {
587 fn clone(&self) -> Self {
588 match self {
589 Self::Boxed(ptr) => {
590 let val = unsafe { &**ptr }.clone();
591 Self::Boxed(Box::into_raw(Box::new(val)))
592 }
593 Self::Static(ptr) => Self::Static(*ptr),
594 }
595 }
596}
597
598impl<T: fmt::Debug> fmt::Debug for BoxOrStatic<T> {
599 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
600 self.as_ref().fmt(f)
601 }
602}
603
604impl<T: fmt::Display> fmt::Display for BoxOrStatic<T> {
605 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606 self.as_ref().fmt(f)
607 }
608}
609
610impl<T: PartialEq> PartialEq for BoxOrStatic<T> {
611 fn eq(&self, other: &Self) -> bool {
612 self.as_ref() == other.as_ref()
613 }
614}
615
616impl<T: Eq> Eq for BoxOrStatic<T> {}
617
618impl<T: core::hash::Hash> core::hash::Hash for BoxOrStatic<T> {
619 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
620 self.as_ref().hash(state);
621 }
622}
623
624impl<T: PartialOrd> PartialOrd for BoxOrStatic<T> {
625 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
626 self.as_ref().partial_cmp(other.as_ref())
627 }
628}
629
630impl<T: Ord> Ord for BoxOrStatic<T> {
631 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
632 self.as_ref().cmp(other.as_ref())
633 }
634}
635
636impl<T> core::ops::Deref for BoxOrStatic<T> {
637 type Target = T;
638 #[inline]
639 fn deref(&self) -> &T {
640 self.as_ref()
641 }
642}
643
644impl<T: Default> Default for BoxOrStatic<T> {
645 fn default() -> Self {
646 Self::heap(T::default())
647 }
648}
649
650impl<T: PrintAsCssValue> PrintAsCssValue for BoxOrStatic<T> {
651 fn print_as_css_value(&self) -> String {
652 self.as_ref().print_as_css_value()
653 }
654}
655
656unsafe impl<T: Send + 'static> Send for BoxOrStatic<T> {}
658unsafe impl<T: Sync + 'static> Sync for BoxOrStatic<T> {}
660
661pub type BoxOrStaticStyleBoxShadow = BoxOrStatic<crate::props::style::box_shadow::StyleBoxShadow>;
663
664pub type BoxOrStaticString = BoxOrStatic<AzString>;
666
667#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
670#[repr(C, u8)] pub enum CssPropertyValue<T> {
672 Auto,
673 None,
674 Initial,
675 Inherit,
676 Revert,
677 Unset,
678 Exact(T),
679}
680
681pub trait PrintAsCssValue {
683 fn print_as_css_value(&self) -> String;
684}
685
686impl<T: PrintAsCssValue> CssPropertyValue<T> {
687 pub fn get_css_value_fmt(&self) -> String {
688 match self {
689 Self::Auto => "auto".to_string(),
690 Self::None => "none".to_string(),
691 Self::Initial => "initial".to_string(),
692 Self::Inherit => "inherit".to_string(),
693 Self::Revert => "revert".to_string(),
694 Self::Unset => "unset".to_string(),
695 Self::Exact(e) => e.print_as_css_value(),
696 }
697 }
698}
699
700impl<T: fmt::Display> fmt::Display for CssPropertyValue<T> {
701 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
702 use self::CssPropertyValue::{Auto, Exact, Inherit, Initial, None, Revert, Unset};
703 match self {
704 Auto => write!(f, "auto"),
705 None => write!(f, "none"),
706 Initial => write!(f, "initial"),
707 Inherit => write!(f, "inherit"),
708 Revert => write!(f, "revert"),
709 Unset => write!(f, "unset"),
710 Exact(e) => write!(f, "{e}"),
711 }
712 }
713}
714
715impl<T> From<T> for CssPropertyValue<T> {
716 fn from(c: T) -> Self {
717 Self::Exact(c)
718 }
719}
720
721impl<T> CssPropertyValue<T> {
722 #[inline]
725 pub fn map_property<F: Fn(T) -> U, U>(self, map_fn: F) -> CssPropertyValue<U> {
726 match self {
727 Self::Exact(c) => CssPropertyValue::Exact(map_fn(c)),
728 Self::Auto => CssPropertyValue::Auto,
729 Self::None => CssPropertyValue::None,
730 Self::Initial => CssPropertyValue::Initial,
731 Self::Inherit => CssPropertyValue::Inherit,
732 Self::Revert => CssPropertyValue::Revert,
733 Self::Unset => CssPropertyValue::Unset,
734 }
735 }
736
737 #[inline]
738 pub const fn get_property(&self) -> Option<&T> {
739 match self {
740 Self::Exact(c) => Some(c),
741 _ => None,
742 }
743 }
744
745 #[inline]
746 pub fn get_property_owned(self) -> Option<T> {
747 match self {
748 Self::Exact(c) => Some(c),
749 _ => None,
750 }
751 }
752
753 #[inline]
754 pub const fn is_auto(&self) -> bool {
755 matches!(self, Self::Auto)
756 }
757
758 #[inline]
759 pub const fn is_none(&self) -> bool {
760 matches!(self, Self::None)
761 }
762
763 #[inline]
764 pub const fn is_initial(&self) -> bool {
765 matches!(self, Self::Initial)
766 }
767
768 #[inline]
769 pub const fn is_inherit(&self) -> bool {
770 matches!(self, Self::Inherit)
771 }
772
773 #[inline]
774 pub const fn is_revert(&self) -> bool {
775 matches!(self, Self::Revert)
776 }
777
778 #[inline]
779 pub const fn is_unset(&self) -> bool {
780 matches!(self, Self::Unset)
781 }
782}
783
784impl<T: Default> CssPropertyValue<T> {
785 #[inline]
786 pub fn get_property_or_default(self) -> Option<T> {
787 match self {
788 Self::Auto | Self::Initial => Some(T::default()),
789 Self::Exact(c) => Some(c),
790 Self::None | Self::Inherit | Self::Revert | Self::Unset => None,
791 }
792 }
793}
794
795impl<T: Default> Default for CssPropertyValue<T> {
796 #[inline]
797 fn default() -> Self {
798 Self::Exact(T::default())
799 }
800}
801
802impl DynamicCssProperty {
803 #[must_use]
804 pub const fn is_inheritable(&self) -> bool {
805 false
809 }
810
811 #[must_use]
812 pub const fn can_trigger_relayout(&self) -> bool {
813 self.default_value.get_type().can_trigger_relayout()
814 }
815}
816
817pub mod rule_priority {
825 pub const UA: u8 = 0;
829
830 pub const SYSTEM: u8 = 10;
835
836 pub const AUTHOR: u8 = 20;
839
840 pub const INLINE: u8 = 30;
844
845 pub const RUNTIME: u8 = 50;
854}
855
856#[derive(Debug, Default, Clone, PartialEq)]
862#[repr(C)]
863pub struct CssRuleBlock {
864 pub path: CssPath,
866 pub declarations: CssDeclarationVec,
869 pub conditions: DynamicSelectorVec,
872 pub priority: u8,
876}
877
878impl_option!(
879 CssRuleBlock,
880 OptionCssRuleBlock,
881 copy = false,
882 [Debug, Clone, PartialEq, Eq, PartialOrd]
883);
884
885impl CssRuleBlock {
886 #[must_use]
892 pub fn depends_on_dynamic_context(&self) -> bool {
893 !self.conditions.as_ref().is_empty()
894 || self
895 .declarations
896 .as_ref()
897 .iter()
898 .any(CssDeclaration::depends_on_dynamic_context)
899 }
900}
901
902impl PartialOrd for CssRuleBlock {
903 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
904 match self.path.partial_cmp(&other.path) {
906 Some(core::cmp::Ordering::Equal) => self.declarations.partial_cmp(&other.declarations),
907 ord => ord,
908 }
909 }
910}
911
912impl_vec!(
913 CssDeclaration,
914 CssDeclarationVec,
915 CssDeclarationVecDestructor,
916 CssDeclarationVecDestructorType,
917 CssDeclarationVecSlice,
918 OptionCssDeclaration
919);
920impl_vec_mut!(CssDeclaration, CssDeclarationVec);
921impl_vec_debug!(CssDeclaration, CssDeclarationVec);
922impl_vec_partialord!(CssDeclaration, CssDeclarationVec);
923impl_vec_ord!(CssDeclaration, CssDeclarationVec);
924impl_vec_clone!(
925 CssDeclaration,
926 CssDeclarationVec,
927 CssDeclarationVecDestructor
928);
929impl_vec_partialeq!(CssDeclaration, CssDeclarationVec);
930impl_vec_eq!(CssDeclaration, CssDeclarationVec);
931impl_vec_hash!(CssDeclaration, CssDeclarationVec);
932
933impl CssRuleBlock {
934 #[must_use]
935 pub fn new(path: CssPath, declarations: Vec<CssDeclaration>) -> Self {
936 Self {
937 path,
938 declarations: declarations.into(),
939 conditions: DynamicSelectorVec::from_const_slice(&[]),
940 priority: rule_priority::AUTHOR,
941 }
942 }
943
944 #[must_use]
945 pub fn with_conditions(
946 path: CssPath,
947 declarations: Vec<CssDeclaration>,
948 conditions: Vec<crate::dynamic_selector::DynamicSelector>,
949 ) -> Self {
950 Self {
951 path,
952 declarations: declarations.into(),
953 conditions: conditions.into(),
954 priority: rule_priority::AUTHOR,
955 }
956 }
957}
958
959pub type CssContentGroup<'a> = Vec<&'a CssPathSelector>;
961
962#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
964#[repr(C)]
965pub enum NodeTypeTag {
966 Html,
968 Head,
969 Body,
970
971 Div,
973 P,
974 Article,
975 Section,
976 Nav,
977 Aside,
978 Header,
979 Footer,
980 Main,
981 Figure,
982 FigCaption,
983
984 H1,
986 H2,
987 H3,
988 H4,
989 H5,
990 H6,
991
992 Br,
994 Hr,
995 Pre,
996 BlockQuote,
997 Address,
998 Details,
999 Summary,
1000 Dialog,
1001
1002 Ul,
1004 Ol,
1005 Li,
1006 Dl,
1007 Dt,
1008 Dd,
1009 Menu,
1010 MenuItem,
1011 Dir,
1012
1013 Table,
1015 Caption,
1016 THead,
1017 TBody,
1018 TFoot,
1019 Tr,
1020 Th,
1021 Td,
1022 ColGroup,
1023 Col,
1024
1025 Form,
1027 FieldSet,
1028 Legend,
1029 Label,
1030 Input,
1031 Button,
1032 Select,
1033 OptGroup,
1034 SelectOption,
1035 TextArea,
1036 Output,
1037 Progress,
1038 Meter,
1039 DataList,
1040
1041 Span,
1043 A,
1044 Em,
1045 Strong,
1046 B,
1047 I,
1048 U,
1049 S,
1050 Mark,
1051 Del,
1052 Ins,
1053 Code,
1054 Samp,
1055 Kbd,
1056 Var,
1057 Cite,
1058 Dfn,
1059 Abbr,
1060 Acronym,
1061 Q,
1062 Time,
1063 Sub,
1064 Sup,
1065 Small,
1066 Big,
1067 Bdo,
1068 Bdi,
1069 Wbr,
1070 Ruby,
1071 Rt,
1072 Rtc,
1073 Rp,
1074 Data,
1075
1076 Canvas,
1078 Object,
1079 Param,
1080 Embed,
1081 Audio,
1082 Video,
1083 Source,
1084 Track,
1085 Map,
1086 Area,
1087 Svg,
1088 SvgPath,
1090 SvgCircle,
1092 SvgRect,
1094 SvgEllipse,
1096 SvgLine,
1098 SvgPolygon,
1100 SvgPolyline,
1102 SvgG,
1104
1105 SvgDefs,
1108 SvgSymbol,
1110 SvgUse,
1112 SvgSwitch,
1114
1115 SvgText,
1118 SvgTspan,
1120 SvgTextPath,
1122
1123 SvgLinearGradient,
1126 SvgRadialGradient,
1128 SvgStop,
1130 SvgPattern,
1132
1133 SvgClipPathElement,
1136 SvgMask,
1138
1139 SvgFilter,
1142 SvgFeBlend,
1144 SvgFeColorMatrix,
1146 SvgFeComponentTransfer,
1148 SvgFeComposite,
1150 SvgFeConvolveMatrix,
1152 SvgFeDiffuseLighting,
1154 SvgFeDisplacementMap,
1156 SvgFeDistantLight,
1158 SvgFeDropShadow,
1160 SvgFeFlood,
1162 SvgFeFuncR,
1164 SvgFeFuncG,
1166 SvgFeFuncB,
1168 SvgFeFuncA,
1170 SvgFeGaussianBlur,
1172 SvgFeImage,
1174 SvgFeMerge,
1176 SvgFeMergeNode,
1178 SvgFeMorphology,
1180 SvgFeOffset,
1182 SvgFePointLight,
1184 SvgFeSpecularLighting,
1186 SvgFeSpotLight,
1188 SvgFeTile,
1190 SvgFeTurbulence,
1192
1193 SvgMarker,
1196 SvgImage,
1198 SvgForeignObject,
1200
1201 SvgTitle,
1204 SvgDesc,
1206 SvgMetadata,
1208 SvgA,
1210 SvgView,
1212 SvgStyle,
1214 SvgScript,
1216
1217 SvgAnimate,
1220 SvgAnimateMotion,
1222 SvgAnimateTransform,
1224 SvgSet,
1226 SvgMpath,
1228
1229 Title,
1231 Meta,
1232 Link,
1233 Script,
1234 Style,
1235 Base,
1236
1237 Text,
1239 Img,
1240 VirtualView,
1241 TransientWindow,
1243 Icon,
1245 GeolocationProbe,
1248
1249 Before,
1251 After,
1252 Marker,
1253 Placeholder,
1254
1255 PageBreak,
1259}
1260
1261#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1263pub enum NodeTypeTagParseError<'a> {
1264 Invalid(&'a str),
1265}
1266
1267impl fmt::Display for NodeTypeTagParseError<'_> {
1268 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1269 match &self {
1270 NodeTypeTagParseError::Invalid(e) => write!(f, "Invalid node type: {e}"),
1271 }
1272 }
1273}
1274
1275#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1277#[repr(C, u8)]
1278pub enum NodeTypeTagParseErrorOwned {
1279 Invalid(AzString),
1280}
1281
1282impl NodeTypeTagParseError<'_> {
1283 #[must_use]
1284 pub fn to_contained(&self) -> NodeTypeTagParseErrorOwned {
1285 match self {
1286 NodeTypeTagParseError::Invalid(s) => {
1287 NodeTypeTagParseErrorOwned::Invalid((*s).to_string().into())
1288 }
1289 }
1290 }
1291}
1292
1293impl NodeTypeTagParseErrorOwned {
1294 #[must_use]
1295 pub fn to_shared(&self) -> NodeTypeTagParseError<'_> {
1296 match self {
1297 Self::Invalid(s) => NodeTypeTagParseError::Invalid(s),
1298 }
1299 }
1300}
1301
1302impl NodeTypeTag {
1304 #[allow(clippy::too_many_lines)]
1305 pub fn from_str(css_key: &str) -> Result<Self, NodeTypeTagParseError<'_>> {
1310 match css_key {
1311 "html" => Ok(Self::Html),
1313 "head" => Ok(Self::Head),
1314 "body" => Ok(Self::Body),
1315
1316 "div" => Ok(Self::Div),
1318 "p" => Ok(Self::P),
1319 "article" => Ok(Self::Article),
1320 "section" => Ok(Self::Section),
1321 "nav" => Ok(Self::Nav),
1322 "aside" => Ok(Self::Aside),
1323 "header" => Ok(Self::Header),
1324 "footer" => Ok(Self::Footer),
1325 "main" => Ok(Self::Main),
1326 "figure" => Ok(Self::Figure),
1327 "figcaption" => Ok(Self::FigCaption),
1328
1329 "h1" => Ok(Self::H1),
1331 "h2" => Ok(Self::H2),
1332 "h3" => Ok(Self::H3),
1333 "h4" => Ok(Self::H4),
1334 "h5" => Ok(Self::H5),
1335 "h6" => Ok(Self::H6),
1336
1337 "br" => Ok(Self::Br),
1339 "hr" => Ok(Self::Hr),
1340 "pre" => Ok(Self::Pre),
1341 "blockquote" => Ok(Self::BlockQuote),
1342 "address" => Ok(Self::Address),
1343 "details" => Ok(Self::Details),
1344 "summary" => Ok(Self::Summary),
1345 "dialog" => Ok(Self::Dialog),
1346
1347 "ul" => Ok(Self::Ul),
1349 "ol" => Ok(Self::Ol),
1350 "li" => Ok(Self::Li),
1351 "dl" => Ok(Self::Dl),
1352 "dt" => Ok(Self::Dt),
1353 "dd" => Ok(Self::Dd),
1354 "menu" => Ok(Self::Menu),
1355 "menuitem" => Ok(Self::MenuItem),
1356 "dir" => Ok(Self::Dir),
1357
1358 "table" => Ok(Self::Table),
1360 "caption" => Ok(Self::Caption),
1361 "thead" => Ok(Self::THead),
1362 "tbody" => Ok(Self::TBody),
1363 "tfoot" => Ok(Self::TFoot),
1364 "tr" => Ok(Self::Tr),
1365 "th" => Ok(Self::Th),
1366 "td" => Ok(Self::Td),
1367 "colgroup" => Ok(Self::ColGroup),
1368 "col" => Ok(Self::Col),
1369
1370 "form" => Ok(Self::Form),
1372 "fieldset" => Ok(Self::FieldSet),
1373 "legend" => Ok(Self::Legend),
1374 "label" => Ok(Self::Label),
1375 "input" => Ok(Self::Input),
1376 "button" => Ok(Self::Button),
1377 "select" => Ok(Self::Select),
1378 "optgroup" => Ok(Self::OptGroup),
1379 "option" => Ok(Self::SelectOption),
1380 "textarea" => Ok(Self::TextArea),
1381 "output" => Ok(Self::Output),
1382 "progress" => Ok(Self::Progress),
1383 "meter" => Ok(Self::Meter),
1384 "datalist" => Ok(Self::DataList),
1385
1386 "span" => Ok(Self::Span),
1388 "a" => Ok(Self::A),
1389 "em" => Ok(Self::Em),
1390 "strong" => Ok(Self::Strong),
1391 "b" => Ok(Self::B),
1392 "i" => Ok(Self::I),
1393 "u" => Ok(Self::U),
1394 "s" => Ok(Self::S),
1395 "mark" => Ok(Self::Mark),
1396 "del" => Ok(Self::Del),
1397 "ins" => Ok(Self::Ins),
1398 "code" => Ok(Self::Code),
1399 "samp" => Ok(Self::Samp),
1400 "kbd" => Ok(Self::Kbd),
1401 "var" => Ok(Self::Var),
1402 "cite" => Ok(Self::Cite),
1403 "dfn" => Ok(Self::Dfn),
1404 "abbr" => Ok(Self::Abbr),
1405 "acronym" => Ok(Self::Acronym),
1406 "q" => Ok(Self::Q),
1407 "time" => Ok(Self::Time),
1408 "sub" => Ok(Self::Sub),
1409 "sup" => Ok(Self::Sup),
1410 "small" => Ok(Self::Small),
1411 "big" => Ok(Self::Big),
1412 "bdo" => Ok(Self::Bdo),
1413 "bdi" => Ok(Self::Bdi),
1414 "wbr" => Ok(Self::Wbr),
1415 "ruby" => Ok(Self::Ruby),
1416 "rt" => Ok(Self::Rt),
1417 "rtc" => Ok(Self::Rtc),
1418 "rp" => Ok(Self::Rp),
1419 "data" => Ok(Self::Data),
1420
1421 "canvas" => Ok(Self::Canvas),
1423 "object" => Ok(Self::Object),
1424 "param" => Ok(Self::Param),
1425 "embed" => Ok(Self::Embed),
1426 "audio" => Ok(Self::Audio),
1427 "video" => Ok(Self::Video),
1428 "source" => Ok(Self::Source),
1429 "track" => Ok(Self::Track),
1430 "map" => Ok(Self::Map),
1431 "area" => Ok(Self::Area),
1432 "svg" => Ok(Self::Svg),
1433
1434 "path" => Ok(Self::SvgPath),
1436 "circle" => Ok(Self::SvgCircle),
1437 "rect" => Ok(Self::SvgRect),
1438 "ellipse" => Ok(Self::SvgEllipse),
1439 "line" => Ok(Self::SvgLine),
1440 "polygon" => Ok(Self::SvgPolygon),
1441 "polyline" => Ok(Self::SvgPolyline),
1442 "g" => Ok(Self::SvgG),
1443
1444 "defs" => Ok(Self::SvgDefs),
1446 "symbol" => Ok(Self::SvgSymbol),
1447 "use" => Ok(Self::SvgUse),
1448 "switch" => Ok(Self::SvgSwitch),
1449
1450 "svg:text" => Ok(Self::SvgText),
1452 "tspan" => Ok(Self::SvgTspan),
1453 "textpath" => Ok(Self::SvgTextPath),
1454
1455 "lineargradient" => Ok(Self::SvgLinearGradient),
1457 "radialgradient" => Ok(Self::SvgRadialGradient),
1458 "stop" => Ok(Self::SvgStop),
1459 "pattern" => Ok(Self::SvgPattern),
1460
1461 "clippath" => Ok(Self::SvgClipPathElement),
1463 "mask" => Ok(Self::SvgMask),
1464
1465 "filter" => Ok(Self::SvgFilter),
1467 "feblend" => Ok(Self::SvgFeBlend),
1468 "fecolormatrix" => Ok(Self::SvgFeColorMatrix),
1469 "fecomponenttransfer" => Ok(Self::SvgFeComponentTransfer),
1470 "fecomposite" => Ok(Self::SvgFeComposite),
1471 "feconvolvematrix" => Ok(Self::SvgFeConvolveMatrix),
1472 "fediffuselighting" => Ok(Self::SvgFeDiffuseLighting),
1473 "fedisplacementmap" => Ok(Self::SvgFeDisplacementMap),
1474 "fedistantlight" => Ok(Self::SvgFeDistantLight),
1475 "fedropshadow" => Ok(Self::SvgFeDropShadow),
1476 "feflood" => Ok(Self::SvgFeFlood),
1477 "fefuncr" => Ok(Self::SvgFeFuncR),
1478 "fefuncg" => Ok(Self::SvgFeFuncG),
1479 "fefuncb" => Ok(Self::SvgFeFuncB),
1480 "fefunca" => Ok(Self::SvgFeFuncA),
1481 "fegaussianblur" => Ok(Self::SvgFeGaussianBlur),
1482 "feimage" => Ok(Self::SvgFeImage),
1483 "femerge" => Ok(Self::SvgFeMerge),
1484 "femergenode" => Ok(Self::SvgFeMergeNode),
1485 "femorphology" => Ok(Self::SvgFeMorphology),
1486 "feoffset" => Ok(Self::SvgFeOffset),
1487 "fepointlight" => Ok(Self::SvgFePointLight),
1488 "fespecularlighting" => Ok(Self::SvgFeSpecularLighting),
1489 "fespotlight" => Ok(Self::SvgFeSpotLight),
1490 "fetile" => Ok(Self::SvgFeTile),
1491 "feturbulence" => Ok(Self::SvgFeTurbulence),
1492
1493 "image" | "svg:image" => Ok(Self::SvgImage),
1495 "svg:marker" => Ok(Self::SvgMarker),
1496 "foreignobject" => Ok(Self::SvgForeignObject),
1497
1498 "svg:title" => Ok(Self::SvgTitle),
1500 "svg:a" => Ok(Self::SvgA),
1501 "svg:style" => Ok(Self::SvgStyle),
1502 "svg:script" => Ok(Self::SvgScript),
1503 "desc" => Ok(Self::SvgDesc),
1504 "metadata" => Ok(Self::SvgMetadata),
1505 "view" => Ok(Self::SvgView),
1506
1507 "animate" => Ok(Self::SvgAnimate),
1509 "animatemotion" => Ok(Self::SvgAnimateMotion),
1510 "animatetransform" => Ok(Self::SvgAnimateTransform),
1511 "set" => Ok(Self::SvgSet),
1512 "mpath" => Ok(Self::SvgMpath),
1513
1514 "title" => Ok(Self::Title),
1516 "meta" => Ok(Self::Meta),
1517 "link" => Ok(Self::Link),
1518 "script" => Ok(Self::Script),
1519 "style" => Ok(Self::Style),
1520 "base" => Ok(Self::Base),
1521
1522 "text" => Ok(Self::Text), "img" => Ok(Self::Img),
1525 "virtual-view" | "iframe" => Ok(Self::VirtualView),
1526 "transient-window" => Ok(Self::TransientWindow),
1527 "icon" => Ok(Self::Icon),
1528 "geolocation-probe" => Ok(Self::GeolocationProbe),
1529 "pagebreak" => Ok(Self::PageBreak),
1530
1531 "before" | "::before" => Ok(Self::Before),
1533 "after" | "::after" => Ok(Self::After),
1534 "marker" | "::marker" => Ok(Self::Marker),
1535 "placeholder" | "::placeholder" => Ok(Self::Placeholder),
1536
1537 other => Err(NodeTypeTagParseError::Invalid(other)),
1538 }
1539 }
1540}
1541
1542impl fmt::Display for NodeTypeTag {
1543 #[allow(clippy::too_many_lines)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1545 match self {
1546 Self::Html => write!(f, "html"),
1548 Self::Head => write!(f, "head"),
1549 Self::Body => write!(f, "body"),
1550
1551 Self::Div => write!(f, "div"),
1553 Self::P => write!(f, "p"),
1554 Self::Article => write!(f, "article"),
1555 Self::Section => write!(f, "section"),
1556 Self::Nav => write!(f, "nav"),
1557 Self::Aside => write!(f, "aside"),
1558 Self::Header => write!(f, "header"),
1559 Self::Footer => write!(f, "footer"),
1560 Self::Main => write!(f, "main"),
1561 Self::Figure => write!(f, "figure"),
1562 Self::FigCaption => write!(f, "figcaption"),
1563
1564 Self::H1 => write!(f, "h1"),
1566 Self::H2 => write!(f, "h2"),
1567 Self::H3 => write!(f, "h3"),
1568 Self::H4 => write!(f, "h4"),
1569 Self::H5 => write!(f, "h5"),
1570 Self::H6 => write!(f, "h6"),
1571
1572 Self::Br => write!(f, "br"),
1574 Self::Hr => write!(f, "hr"),
1575 Self::Pre => write!(f, "pre"),
1576 Self::BlockQuote => write!(f, "blockquote"),
1577 Self::Address => write!(f, "address"),
1578 Self::Details => write!(f, "details"),
1579 Self::Summary => write!(f, "summary"),
1580 Self::Dialog => write!(f, "dialog"),
1581
1582 Self::Ul => write!(f, "ul"),
1584 Self::Ol => write!(f, "ol"),
1585 Self::Li => write!(f, "li"),
1586 Self::Dl => write!(f, "dl"),
1587 Self::Dt => write!(f, "dt"),
1588 Self::Dd => write!(f, "dd"),
1589 Self::Menu => write!(f, "menu"),
1590 Self::MenuItem => write!(f, "menuitem"),
1591 Self::Dir => write!(f, "dir"),
1592
1593 Self::Table => write!(f, "table"),
1595 Self::Caption => write!(f, "caption"),
1596 Self::THead => write!(f, "thead"),
1597 Self::TBody => write!(f, "tbody"),
1598 Self::TFoot => write!(f, "tfoot"),
1599 Self::Tr => write!(f, "tr"),
1600 Self::Th => write!(f, "th"),
1601 Self::Td => write!(f, "td"),
1602 Self::ColGroup => write!(f, "colgroup"),
1603 Self::Col => write!(f, "col"),
1604
1605 Self::Form => write!(f, "form"),
1607 Self::FieldSet => write!(f, "fieldset"),
1608 Self::Legend => write!(f, "legend"),
1609 Self::Label => write!(f, "label"),
1610 Self::Input => write!(f, "input"),
1611 Self::Button => write!(f, "button"),
1612 Self::Select => write!(f, "select"),
1613 Self::OptGroup => write!(f, "optgroup"),
1614 Self::SelectOption => write!(f, "option"),
1615 Self::TextArea => write!(f, "textarea"),
1616 Self::Output => write!(f, "output"),
1617 Self::Progress => write!(f, "progress"),
1618 Self::Meter => write!(f, "meter"),
1619 Self::DataList => write!(f, "datalist"),
1620
1621 Self::Span => write!(f, "span"),
1623 Self::A => write!(f, "a"),
1624 Self::Em => write!(f, "em"),
1625 Self::Strong => write!(f, "strong"),
1626 Self::B => write!(f, "b"),
1627 Self::I => write!(f, "i"),
1628 Self::U => write!(f, "u"),
1629 Self::S => write!(f, "s"),
1630 Self::Mark => write!(f, "mark"),
1631 Self::Del => write!(f, "del"),
1632 Self::Ins => write!(f, "ins"),
1633 Self::Code => write!(f, "code"),
1634 Self::Samp => write!(f, "samp"),
1635 Self::Kbd => write!(f, "kbd"),
1636 Self::Var => write!(f, "var"),
1637 Self::Cite => write!(f, "cite"),
1638 Self::Dfn => write!(f, "dfn"),
1639 Self::Abbr => write!(f, "abbr"),
1640 Self::Acronym => write!(f, "acronym"),
1641 Self::Q => write!(f, "q"),
1642 Self::Time => write!(f, "time"),
1643 Self::Sub => write!(f, "sub"),
1644 Self::Sup => write!(f, "sup"),
1645 Self::Small => write!(f, "small"),
1646 Self::Big => write!(f, "big"),
1647 Self::Bdo => write!(f, "bdo"),
1648 Self::Bdi => write!(f, "bdi"),
1649 Self::Wbr => write!(f, "wbr"),
1650 Self::Ruby => write!(f, "ruby"),
1651 Self::Rt => write!(f, "rt"),
1652 Self::Rtc => write!(f, "rtc"),
1653 Self::Rp => write!(f, "rp"),
1654 Self::Data => write!(f, "data"),
1655
1656 Self::Canvas => write!(f, "canvas"),
1658 Self::Object => write!(f, "object"),
1659 Self::Param => write!(f, "param"),
1660 Self::Embed => write!(f, "embed"),
1661 Self::Audio => write!(f, "audio"),
1662 Self::Video => write!(f, "video"),
1663 Self::Source => write!(f, "source"),
1664 Self::Track => write!(f, "track"),
1665 Self::Map => write!(f, "map"),
1666 Self::Area => write!(f, "area"),
1667 Self::Svg => write!(f, "svg"),
1668 Self::SvgPath => write!(f, "path"),
1669 Self::SvgCircle => write!(f, "circle"),
1670 Self::SvgRect => write!(f, "rect"),
1671 Self::SvgEllipse => write!(f, "ellipse"),
1672 Self::SvgLine => write!(f, "line"),
1673 Self::SvgPolygon => write!(f, "polygon"),
1674 Self::SvgPolyline => write!(f, "polyline"),
1675 Self::SvgG => write!(f, "g"),
1676
1677 Self::SvgDefs => write!(f, "defs"),
1679 Self::SvgSymbol => write!(f, "symbol"),
1680 Self::SvgUse => write!(f, "use"),
1681 Self::SvgSwitch => write!(f, "switch"),
1682
1683 Self::SvgText => write!(f, "svg:text"),
1685 Self::SvgTspan => write!(f, "tspan"),
1686 Self::SvgTextPath => write!(f, "textpath"),
1687
1688 Self::SvgLinearGradient => write!(f, "lineargradient"),
1690 Self::SvgRadialGradient => write!(f, "radialgradient"),
1691 Self::SvgStop => write!(f, "stop"),
1692 Self::SvgPattern => write!(f, "pattern"),
1693
1694 Self::SvgClipPathElement => write!(f, "clippath"),
1696 Self::SvgMask => write!(f, "mask"),
1697
1698 Self::SvgFilter => write!(f, "filter"),
1700 Self::SvgFeBlend => write!(f, "feblend"),
1701 Self::SvgFeColorMatrix => write!(f, "fecolormatrix"),
1702 Self::SvgFeComponentTransfer => write!(f, "fecomponenttransfer"),
1703 Self::SvgFeComposite => write!(f, "fecomposite"),
1704 Self::SvgFeConvolveMatrix => write!(f, "feconvolvematrix"),
1705 Self::SvgFeDiffuseLighting => write!(f, "fediffuselighting"),
1706 Self::SvgFeDisplacementMap => write!(f, "fedisplacementmap"),
1707 Self::SvgFeDistantLight => write!(f, "fedistantlight"),
1708 Self::SvgFeDropShadow => write!(f, "fedropshadow"),
1709 Self::SvgFeFlood => write!(f, "feflood"),
1710 Self::SvgFeFuncR => write!(f, "fefuncr"),
1711 Self::SvgFeFuncG => write!(f, "fefuncg"),
1712 Self::SvgFeFuncB => write!(f, "fefuncb"),
1713 Self::SvgFeFuncA => write!(f, "fefunca"),
1714 Self::SvgFeGaussianBlur => write!(f, "fegaussianblur"),
1715 Self::SvgFeImage => write!(f, "feimage"),
1716 Self::SvgFeMerge => write!(f, "femerge"),
1717 Self::SvgFeMergeNode => write!(f, "femergenode"),
1718 Self::SvgFeMorphology => write!(f, "femorphology"),
1719 Self::SvgFeOffset => write!(f, "feoffset"),
1720 Self::SvgFePointLight => write!(f, "fepointlight"),
1721 Self::SvgFeSpecularLighting => write!(f, "fespecularlighting"),
1722 Self::SvgFeSpotLight => write!(f, "fespotlight"),
1723 Self::SvgFeTile => write!(f, "fetile"),
1724 Self::SvgFeTurbulence => write!(f, "feturbulence"),
1725
1726 Self::SvgMarker => write!(f, "svg:marker"),
1728 Self::SvgImage => write!(f, "svg:image"),
1729 Self::SvgForeignObject => write!(f, "foreignobject"),
1730
1731 Self::SvgTitle => write!(f, "svg:title"),
1733 Self::SvgDesc => write!(f, "desc"),
1734 Self::SvgMetadata => write!(f, "metadata"),
1735 Self::SvgA => write!(f, "svg:a"),
1736 Self::SvgView => write!(f, "view"),
1737 Self::SvgStyle => write!(f, "svg:style"),
1738 Self::SvgScript => write!(f, "svg:script"),
1739
1740 Self::SvgAnimate => write!(f, "animate"),
1742 Self::SvgAnimateMotion => write!(f, "animatemotion"),
1743 Self::SvgAnimateTransform => write!(f, "animatetransform"),
1744 Self::SvgSet => write!(f, "set"),
1745 Self::SvgMpath => write!(f, "mpath"),
1746
1747 Self::Title => write!(f, "title"),
1749 Self::Meta => write!(f, "meta"),
1750 Self::Link => write!(f, "link"),
1751 Self::Script => write!(f, "script"),
1752 Self::Style => write!(f, "style"),
1753 Self::Base => write!(f, "base"),
1754
1755 Self::Text => write!(f, "text"),
1757 Self::Img => write!(f, "img"),
1758 Self::VirtualView => write!(f, "virtual-view"),
1759 Self::TransientWindow => write!(f, "transient-window"),
1760 Self::Icon => write!(f, "icon"),
1761 Self::GeolocationProbe => write!(f, "geolocation-probe"),
1762 Self::PageBreak => write!(f, "pagebreak"),
1763
1764 Self::Before => write!(f, "::before"),
1766 Self::After => write!(f, "::after"),
1767 Self::Marker => write!(f, "::marker"),
1768 Self::Placeholder => write!(f, "::placeholder"),
1769 }
1770 }
1771}
1772
1773#[derive(Clone, Hash, Default, PartialEq, Eq, PartialOrd, Ord)]
1785#[repr(C)]
1786pub struct CssPath {
1787 pub selectors: CssPathSelectorVec,
1788}
1789
1790impl_vec!(
1791 CssPathSelector,
1792 CssPathSelectorVec,
1793 CssPathSelectorVecDestructor,
1794 CssPathSelectorVecDestructorType,
1795 CssPathSelectorVecSlice,
1796 OptionCssPathSelector
1797);
1798impl_vec_debug!(CssPathSelector, CssPathSelectorVec);
1799impl_vec_partialord!(CssPathSelector, CssPathSelectorVec);
1800impl_vec_ord!(CssPathSelector, CssPathSelectorVec);
1801impl_vec_clone!(
1802 CssPathSelector,
1803 CssPathSelectorVec,
1804 CssPathSelectorVecDestructor
1805);
1806impl_vec_partialeq!(CssPathSelector, CssPathSelectorVec);
1807impl_vec_eq!(CssPathSelector, CssPathSelectorVec);
1808impl_vec_hash!(CssPathSelector, CssPathSelectorVec);
1809
1810impl CssPath {
1811 #[must_use]
1812 pub fn new(selectors: Vec<CssPathSelector>) -> Self {
1813 Self {
1814 selectors: selectors.into(),
1815 }
1816 }
1817
1818 pub fn push_front_scope(&mut self, start: usize, end: usize) {
1832 self.push_front_scope_for(start, end, true);
1833 }
1834
1835 pub fn push_front_scope_for(&mut self, start: usize, end: usize, node_only_bare_global: bool) {
1849 let is_bare_global = self.selectors.as_ref().len() == 1
1850 && matches!(
1851 self.selectors.as_ref().first(),
1852 Some(CssPathSelector::Global)
1853 );
1854 let range = if is_bare_global && node_only_bare_global {
1855 CssScopeRange { start, end: start }
1856 } else {
1857 CssScopeRange { start, end }
1858 };
1859 let mut selectors = Vec::with_capacity(self.selectors.as_ref().len() + 1);
1860 selectors.push(CssPathSelector::Root(range));
1861 selectors.extend(self.selectors.as_ref().iter().cloned());
1862 self.selectors = selectors.into();
1863 }
1864}
1865
1866impl fmt::Display for CssPath {
1867 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1868 for selector in self.selectors.as_ref() {
1869 write!(f, "{selector}")?;
1870 }
1871 Ok(())
1872 }
1873}
1874
1875impl fmt::Debug for CssPath {
1876 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1877 write!(f, "{self}")
1878 }
1879}
1880
1881#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1889#[repr(C)]
1890pub struct CssScopeRange {
1891 pub start: usize,
1893 pub end: usize,
1895}
1896
1897impl CssScopeRange {
1898 #[inline]
1900 #[must_use]
1901 pub const fn contains(&self, node: usize) -> bool {
1902 self.start <= node && node <= self.end
1903 }
1904}
1905
1906#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1907#[repr(C, u8)]
1908#[derive(Default)]
1909pub enum CssPathSelector {
1910 #[default]
1912 Global,
1913 Root(CssScopeRange),
1922 Type(NodeTypeTag),
1924 Class(AzString),
1926 Id(AzString),
1928 PseudoSelector(CssPathPseudoSelector),
1930 Attribute(CssAttributeSelector),
1932 DirectChildren,
1934 Children,
1936 AdjacentSibling,
1938 GeneralSibling,
1940}
1941
1942#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1944#[repr(C)]
1945pub struct CssAttributeSelector {
1946 pub name: AzString,
1947 pub op: AttributeMatchOp,
1948 pub value: OptionString,
1949}
1950
1951impl Default for CssAttributeSelector {
1952 fn default() -> Self {
1953 Self {
1954 name: AzString::default(),
1955 op: AttributeMatchOp::Exists,
1956 value: OptionString::None,
1957 }
1958 }
1959}
1960
1961#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1963#[repr(C)]
1964#[derive(Default)]
1965pub enum AttributeMatchOp {
1966 #[default]
1968 Exists,
1969 Eq,
1971 Includes,
1973 DashMatch,
1975 Prefix,
1977 Suffix,
1979 Substring,
1981}
1982
1983impl_option!(
1984 CssPathSelector,
1985 OptionCssPathSelector,
1986 copy = false,
1987 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1988);
1989
1990impl fmt::Display for CssPathSelector {
1991 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1992 use self::CssPathSelector::{
1993 AdjacentSibling, Attribute, Children, Class, DirectChildren, GeneralSibling, Global,
1994 Id, PseudoSelector, Root, Type,
1995 };
1996 match &self {
1997 Global => write!(f, "*"),
1998 Root(r) => write!(f, ":root({}..={})", r.start, r.end),
1999 Type(n) => write!(f, "{n}"),
2000 Class(c) => write!(f, ".{c}"),
2001 Id(i) => write!(f, "#{i}"),
2002 PseudoSelector(p) => write!(f, ":{p}"),
2003 Attribute(a) => write!(f, "{a}"),
2004 DirectChildren => write!(f, ">"),
2005 Children => write!(f, " "),
2006 AdjacentSibling => write!(f, "+"),
2007 GeneralSibling => write!(f, "~"),
2008 }
2009 }
2010}
2011
2012impl fmt::Display for CssAttributeSelector {
2013 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2014 match (&self.op, self.value.as_ref()) {
2015 (AttributeMatchOp::Exists, _) => write!(f, "[{}]", self.name),
2016 (op, Some(v)) => write!(f, "[{}{}=\"{}\"]", self.name, op.symbol_prefix(), v),
2017 (op, None) => write!(f, "[{}{}=\"\"]", self.name, op.symbol_prefix()),
2018 }
2019 }
2020}
2021
2022impl AttributeMatchOp {
2023 #[must_use]
2026 pub const fn symbol_prefix(&self) -> &'static str {
2027 match self {
2028 Self::Exists | Self::Eq => "",
2029 Self::Includes => "~",
2030 Self::DashMatch => "|",
2031 Self::Prefix => "^",
2032 Self::Suffix => "$",
2033 Self::Substring => "*",
2034 }
2035 }
2036}
2037
2038#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2039#[repr(C, u8)]
2040pub enum CssPathPseudoSelector {
2041 First,
2043 Last,
2045 NthChild(CssNthChildSelector),
2047 Hover,
2049 Active,
2051 Focus,
2053 SeatFocus,
2056 Lang(AzString),
2058 Backdrop,
2060 Dragging,
2062 DragOver,
2064 Root,
2067 Placeholder,
2071}
2072
2073#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2075#[repr(C, u8)]
2076pub enum CssNthChildSelector {
2077 Number(u32),
2078 Even,
2079 Odd,
2080 Pattern(CssNthChildPattern),
2081}
2082
2083#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2085#[repr(C)]
2086pub struct CssNthChildPattern {
2087 pub pattern_repeat: u32,
2088 pub offset: u32,
2089}
2090
2091impl fmt::Display for CssNthChildSelector {
2092 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2093 use self::CssNthChildSelector::{Even, Number, Odd, Pattern};
2094 match &self {
2095 Number(u) => write!(f, "{u}"),
2096 Even => write!(f, "even"),
2097 Odd => write!(f, "odd"),
2098 Pattern(p) => write!(f, "{}n + {}", p.pattern_repeat, p.offset),
2099 }
2100 }
2101}
2102
2103impl fmt::Display for CssPathPseudoSelector {
2104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2105 use self::CssPathPseudoSelector::{
2106 Active, Backdrop, DragOver, Dragging, First, Focus, Hover, Lang, Last, NthChild,
2107 Placeholder, Root, SeatFocus,
2108 };
2109 match &self {
2110 First => write!(f, "first"),
2111 Last => write!(f, "last"),
2112 NthChild(u) => write!(f, "nth-child({u})"),
2113 Hover => write!(f, "hover"),
2114 Active => write!(f, "active"),
2115 Focus => write!(f, "focus"),
2116 SeatFocus => write!(f, "seat-focus"),
2117 Lang(lang) => write!(f, "lang({})", lang.as_str()),
2118 Backdrop => write!(f, "backdrop"),
2119 Dragging => write!(f, "dragging"),
2120 DragOver => write!(f, "drag-over"),
2121 Root => write!(f, "root"),
2122 Placeholder => write!(f, ":placeholder"),
2125 }
2126 }
2127}
2128
2129impl Css {
2130 #[must_use]
2132 pub fn empty() -> Self {
2133 Self::default()
2134 }
2135
2136 pub fn sort_by_specificity(&mut self) {
2141 self.rules.as_mut().sort_by(|a, b| {
2142 a.priority
2143 .cmp(&b.priority)
2144 .then_with(|| get_specificity(&a.path).cmp(&get_specificity(&b.path)))
2145 });
2146 }
2147
2148 pub fn rules(&self) -> core::slice::Iter<'_, CssRuleBlock> {
2149 self.rules.as_ref().iter()
2150 }
2151
2152 pub fn iter_inline_properties(
2160 &self,
2161 ) -> impl Iterator<Item = (&CssProperty, &DynamicSelectorVec)> + '_ {
2162 self.rules.as_ref().iter().flat_map(|r| {
2163 r.declarations.as_ref().iter().filter_map(move |d| match d {
2164 CssDeclaration::Static(p) => Some((p, &r.conditions)),
2165 CssDeclaration::Dynamic(_) => None,
2166 })
2167 })
2168 }
2169}
2170
2171#[cfg(test)]
2172mod root_scope_tests {
2173 use super::*;
2174
2175 #[test]
2176 fn scope_range_contains() {
2177 let r = CssScopeRange { start: 3, end: 7 };
2178 assert!(r.contains(3) && r.contains(5) && r.contains(7));
2179 assert!(!r.contains(2) && !r.contains(8));
2180 let leaf = CssScopeRange { start: 4, end: 4 };
2182 assert!(leaf.contains(4));
2183 assert!(!leaf.contains(3) && !leaf.contains(5));
2184 }
2185
2186 #[test]
2187 fn push_front_scope_compounds_with_wrapper() {
2188 let mut p = CssPath::new(vec![CssPathSelector::Global]);
2191 p.push_front_scope(5, 9);
2192 assert_eq!(
2193 p.selectors.as_ref(),
2194 &[
2195 CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
2196 CssPathSelector::Global
2197 ][..]
2198 );
2199 let subtree = CssScopeRange { start: 5, end: 9 };
2201 let mut p2 = CssPath::new(vec![
2202 CssPathSelector::Global,
2203 CssPathSelector::Children,
2204 CssPathSelector::Class("foo".to_string().into()),
2205 ]);
2206 p2.push_front_scope(5, 9);
2207 assert_eq!(p2.selectors.as_ref()[0], CssPathSelector::Root(subtree));
2208 assert_eq!(p2.selectors.as_ref().len(), 4);
2209 }
2210
2211 #[test]
2212 fn root_display_roundtrips() {
2213 let s = CssPathSelector::Root(CssScopeRange { start: 2, end: 6 });
2214 assert_eq!(format!("{s}"), ":root(2..=6)");
2215 }
2216
2217 #[test]
2218 fn parse_inline_keeps_layout_and_style_decls() {
2219 let css = Css::parse_inline("width: 200px; height: 100px; background: red");
2222 let mut types = Vec::new();
2223 for r in css.rules.as_ref() {
2224 for d in r.declarations.as_ref() {
2225 if let CssDeclaration::Static(p) = d {
2226 types.push(alloc::format!("{:?}", p.get_type()));
2227 }
2228 }
2229 }
2230 println!("INLINE PROP TYPES: {types:?}");
2231 assert!(
2232 types.iter().any(|t| t.contains("width")),
2233 "width must survive parse_inline as a Static decl; got {types:?}"
2234 );
2235 assert!(
2236 types.iter().any(|t| t.contains("height")),
2237 "height must survive parse_inline; got {types:?}"
2238 );
2239 }
2240}
2241
2242#[cfg(test)]
2243mod priority_sort_tests {
2244 use super::*;
2245 use crate::css::rule_priority;
2246
2247 fn rule_with(priority: u8, selectors: Vec<CssPathSelector>) -> CssRuleBlock {
2248 CssRuleBlock {
2249 path: CssPath {
2250 selectors: selectors.into(),
2251 },
2252 declarations: Vec::new().into(),
2253 conditions: DynamicSelectorVec::from_const_slice(&[]),
2254 priority,
2255 }
2256 }
2257
2258 #[test]
2261 fn sort_by_priority_then_specificity() {
2262 let mut css = Css::new(vec![
2263 rule_with(rule_priority::AUTHOR, vec![CssPathSelector::Global]),
2265 rule_with(
2267 rule_priority::UA,
2268 vec![
2269 CssPathSelector::Id("ua-id".to_string().into()),
2270 CssPathSelector::Class("ua-class".to_string().into()),
2271 ],
2272 ),
2273 rule_with(
2275 rule_priority::AUTHOR,
2276 vec![CssPathSelector::Id("a-id".to_string().into())],
2277 ),
2278 rule_with(rule_priority::SYSTEM, vec![CssPathSelector::Global]),
2280 ]);
2281 css.sort_by_specificity();
2282 let priorities: Vec<u8> = css.rules.as_ref().iter().map(|r| r.priority).collect();
2283 assert_eq!(
2284 priorities,
2285 vec![
2286 rule_priority::UA,
2287 rule_priority::SYSTEM,
2288 rule_priority::AUTHOR,
2289 rule_priority::AUTHOR
2290 ],
2291 "rules must sort by layer first; specificity only breaks ties within a layer"
2292 );
2293 let last_two_specificity: Vec<_> = css
2295 .rules
2296 .as_ref()
2297 .iter()
2298 .filter(|r| r.priority == rule_priority::AUTHOR)
2299 .map(|r| get_specificity(&r.path))
2300 .collect();
2301 assert!(last_two_specificity[0] < last_two_specificity[1]);
2302 }
2303}
2304
2305#[must_use]
2308pub fn get_specificity(path: &CssPath) -> (usize, usize, usize, usize) {
2309 let id_count = path
2310 .selectors
2311 .iter()
2312 .filter(|x| matches!(x, CssPathSelector::Id(_)))
2313 .count();
2314 let class_count = path
2315 .selectors
2316 .iter()
2317 .filter(|x| {
2318 matches!(
2319 x,
2320 CssPathSelector::Class(_)
2321 | CssPathSelector::PseudoSelector(_)
2322 | CssPathSelector::Attribute(_)
2323 )
2324 })
2325 .count();
2326 let div_count = path
2327 .selectors
2328 .iter()
2329 .filter(|x| matches!(x, CssPathSelector::Type(_)))
2330 .count();
2331 (id_count, class_count, div_count, path.selectors.len())
2332}
2333
2334#[cfg(test)]
2335#[allow(clippy::pedantic, clippy::nursery)]
2336mod autotest_generated {
2337 use core::hash::{Hash, Hasher};
2338 use std::collections::hash_map::DefaultHasher;
2339
2340 use super::*;
2341 use crate::{
2342 dynamic_selector::DynamicSelector,
2343 props::{
2344 basic::{color::ColorU, pixel::PixelValue},
2345 layout::dimensions::LayoutWidth,
2346 style::text::StyleTextColor,
2347 },
2348 };
2349
2350 fn prop_width(px: f32) -> CssProperty {
2356 CssProperty::width(LayoutWidth::Px(PixelValue::px(px)))
2357 }
2358
2359 fn prop_text_color(r: u8) -> CssProperty {
2361 CssProperty::const_text_color(StyleTextColor {
2362 inner: ColorU::new(r, 0, 0, 255),
2363 })
2364 }
2365
2366 fn dyn_prop(id: &str, default_value: CssProperty) -> DynamicCssProperty {
2367 DynamicCssProperty {
2368 dynamic_id: id.to_string().into(),
2369 default_value,
2370 }
2371 }
2372
2373 fn rule_at(priority: u8, selectors: Vec<CssPathSelector>) -> CssRuleBlock {
2374 let mut r = CssRuleBlock::new(
2375 CssPath::new(selectors),
2376 vec![CssDeclaration::Static(prop_width(1.0))],
2377 );
2378 r.priority = priority;
2379 r
2380 }
2381
2382 fn hash_of<T: Hash>(t: &T) -> u64 {
2383 let mut h = DefaultHasher::new();
2384 t.hash(&mut h);
2385 h.finish()
2386 }
2387
2388 #[derive(Debug, Copy, Clone, PartialEq, Default)]
2391 struct TestVal(f32);
2392
2393 impl PrintAsCssValue for TestVal {
2394 fn print_as_css_value(&self) -> String {
2395 format!("{}", self.0)
2396 }
2397 }
2398
2399 impl fmt::Display for TestVal {
2400 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2401 write!(f, "{}", self.0)
2402 }
2403 }
2404
2405 fn keyword_values() -> Vec<CssPropertyValue<TestVal>> {
2407 vec![
2408 CssPropertyValue::Auto,
2409 CssPropertyValue::None,
2410 CssPropertyValue::Initial,
2411 CssPropertyValue::Inherit,
2412 CssPropertyValue::Revert,
2413 CssPropertyValue::Unset,
2414 ]
2415 }
2416
2417 #[test]
2422 fn css_empty_is_the_neutral_element() {
2423 let e = Css::empty();
2424 assert!(e.is_empty());
2425 assert_eq!(e.rules().count(), 0);
2426 assert_eq!(e.iter_inline_properties().count(), 0);
2427 assert_eq!(e, Css::default());
2429 assert_eq!(e, Css::new(Vec::new()));
2430 assert_eq!(e, Css::from(Vec::<CssRuleBlock>::new()));
2431 }
2432
2433 #[test]
2434 fn css_new_preserves_length_and_order() {
2435 let rules = vec![
2436 rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
2437 rule_at(
2438 rule_priority::AUTHOR,
2439 vec![CssPathSelector::Id("a".to_string().into())],
2440 ),
2441 rule_at(rule_priority::INLINE, vec![CssPathSelector::Children]),
2442 ];
2443 let css = Css::new(rules.clone());
2444 assert!(!css.is_empty());
2445 assert_eq!(css.rules().count(), 3);
2446 assert_eq!(css.rules.as_ref(), &rules[..]);
2447 assert_eq!(css.rules().count(), css.rules.as_ref().len());
2449 }
2450
2451 #[test]
2452 fn css_new_with_many_rules_does_not_panic() {
2453 let rules: Vec<CssRuleBlock> = (0..10_000)
2454 .map(|i| rule_at((i % 256) as u8, vec![CssPathSelector::Global]))
2455 .collect();
2456 let css = Css::new(rules);
2457 assert_eq!(css.rules.as_ref().len(), 10_000);
2458 assert!(!css.is_empty());
2459 assert_eq!(css.iter_inline_properties().count(), 10_000);
2461 }
2462
2463 #[test]
2464 fn css_sort_by_specificity_on_empty_and_singleton_is_a_noop() {
2465 let mut empty = Css::empty();
2466 empty.sort_by_specificity();
2467 assert!(empty.is_empty());
2468
2469 let one = rule_at(rule_priority::AUTHOR, vec![CssPathSelector::Global]);
2470 let mut css = Css::new(vec![one.clone()]);
2471 css.sort_by_specificity();
2472 assert_eq!(css.rules.as_ref(), &[one][..]);
2473 }
2474
2475 #[test]
2476 fn css_sort_by_specificity_is_idempotent() {
2477 let mut css = Css::new(vec![
2478 rule_at(rule_priority::RUNTIME, vec![CssPathSelector::Global]),
2479 rule_at(
2480 rule_priority::UA,
2481 vec![
2482 CssPathSelector::Id("x".to_string().into()),
2483 CssPathSelector::Class("y".to_string().into()),
2484 ],
2485 ),
2486 rule_at(rule_priority::INLINE, vec![CssPathSelector::Global]),
2487 rule_at(
2488 rule_priority::AUTHOR,
2489 vec![CssPathSelector::Type(NodeTypeTag::Div)],
2490 ),
2491 rule_at(rule_priority::SYSTEM, vec![CssPathSelector::Global]),
2492 ]);
2493 css.sort_by_specificity();
2494 let once = css.clone();
2495 css.sort_by_specificity();
2496 assert_eq!(css, once, "sort_by_specificity must be idempotent");
2497
2498 let priorities: Vec<u8> = css.rules().map(|r| r.priority).collect();
2500 assert_eq!(
2501 priorities,
2502 vec![
2503 rule_priority::UA,
2504 rule_priority::SYSTEM,
2505 rule_priority::AUTHOR,
2506 rule_priority::INLINE,
2507 rule_priority::RUNTIME,
2508 ]
2509 );
2510 }
2511
2512 #[test]
2513 fn css_sort_by_specificity_keeps_equal_keys_in_source_order() {
2514 let a = CssRuleBlock::new(
2516 CssPath::new(vec![CssPathSelector::Global]),
2517 vec![CssDeclaration::Static(prop_width(1.0))],
2518 );
2519 let b = CssRuleBlock::new(
2520 CssPath::new(vec![CssPathSelector::Global]),
2521 vec![CssDeclaration::Static(prop_width(2.0))],
2522 );
2523 let mut css = Css::new(vec![a.clone(), b.clone()]);
2524 css.sort_by_specificity();
2525 assert_eq!(
2526 css.rules.as_ref(),
2527 &[a, b][..],
2528 "ties must keep source order (last-wins cascade depends on it)"
2529 );
2530 }
2531
2532 #[test]
2533 fn css_iter_inline_properties_skips_dynamic_declarations() {
2534 let css = Css::new(vec![CssRuleBlock::with_conditions(
2535 CssPath::new(vec![CssPathSelector::Global]),
2536 vec![
2537 CssDeclaration::Static(prop_width(10.0)),
2538 CssDeclaration::Dynamic(dyn_prop("d", prop_text_color(1))),
2539 CssDeclaration::Static(prop_text_color(2)),
2540 ],
2541 vec![DynamicSelector::ContainerName("c".to_string().into())],
2542 )]);
2543
2544 let collected: Vec<_> = css.iter_inline_properties().collect();
2545 assert_eq!(collected.len(), 2, "Dynamic declarations must be skipped");
2546 assert_eq!(collected[0].0.get_type(), CssPropertyType::Width);
2547 assert_eq!(collected[1].0.get_type(), CssPropertyType::TextColor);
2548 for (_, conds) in &collected {
2550 assert_eq!(conds.as_ref().len(), 1);
2551 }
2552 }
2553
2554 #[test]
2555 fn css_iter_inline_properties_on_rule_without_declarations() {
2556 let css = Css::new(vec![CssRuleBlock::new(
2557 CssPath::new(vec![CssPathSelector::Global]),
2558 Vec::new(),
2559 )]);
2560 assert!(
2561 !css.is_empty(),
2562 "a rule with 0 declarations is still a rule"
2563 );
2564 assert_eq!(css.iter_inline_properties().count(), 0);
2565 }
2566
2567 #[test]
2568 fn css_ord_is_length_based_by_design() {
2569 let a = Css::new(vec![rule_at(
2574 rule_priority::UA,
2575 vec![CssPathSelector::Global],
2576 )]);
2577 let b = Css::new(vec![rule_at(
2578 rule_priority::RUNTIME,
2579 vec![CssPathSelector::Type(NodeTypeTag::Div)],
2580 )]);
2581 assert_ne!(a, b);
2582 assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
2583 assert_eq!(a.partial_cmp(&b), Some(core::cmp::Ordering::Equal));
2584 let longer = Css::new(vec![
2586 rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
2587 rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
2588 ]);
2589 assert_eq!(a.cmp(&longer), core::cmp::Ordering::Less);
2590 assert_eq!(Css::empty().cmp(&a), core::cmp::Ordering::Less);
2591 }
2592
2593 #[test]
2598 fn css_declaration_new_static_matches_the_wrapped_property() {
2599 let p = prop_width(42.0);
2600 let d = CssDeclaration::new_static(p.clone());
2601 assert_eq!(d, CssDeclaration::Static(p.clone()));
2602 assert_eq!(d.get_type(), p.get_type());
2603 assert_eq!(d.get_type(), CssPropertyType::Width);
2604 }
2605
2606 #[test]
2607 fn css_declaration_new_dynamic_takes_its_type_from_the_default_value() {
2608 let dp = dyn_prop("my_id", prop_text_color(7));
2609 let d = CssDeclaration::new_dynamic(dp.clone());
2610 assert_eq!(d, CssDeclaration::Dynamic(dp));
2611 assert_eq!(
2612 d.get_type(),
2613 CssPropertyType::TextColor,
2614 "a Dynamic declaration's type is its default value's type"
2615 );
2616 }
2617
2618 #[test]
2619 fn css_declaration_is_inheritable_matrix() {
2620 assert!(CssDeclaration::new_static(prop_text_color(1)).is_inheritable());
2622 assert!(!CssDeclaration::new_static(prop_width(1.0)).is_inheritable());
2623 assert!(
2626 !CssDeclaration::new_dynamic(dyn_prop("c", prop_text_color(1))).is_inheritable(),
2627 "Dynamic declarations must never inherit, even wrapping an inheritable prop"
2628 );
2629 assert!(!CssDeclaration::new_dynamic(dyn_prop("w", prop_width(1.0))).is_inheritable());
2630 }
2631
2632 #[test]
2633 fn css_declaration_can_trigger_relayout_matrix() {
2634 assert!(CssDeclaration::new_static(prop_width(1.0)).can_trigger_relayout());
2635 assert!(!CssDeclaration::new_static(prop_text_color(1)).can_trigger_relayout());
2636 assert!(CssDeclaration::new_dynamic(dyn_prop("w", prop_width(1.0))).can_trigger_relayout());
2638 assert!(
2639 !CssDeclaration::new_dynamic(dyn_prop("c", prop_text_color(1))).can_trigger_relayout()
2640 );
2641 }
2642
2643 #[test]
2644 fn css_declaration_to_str_static_is_non_empty_for_edge_floats() {
2645 for px in [
2646 0.0_f32,
2647 -0.0,
2648 f32::MIN,
2649 f32::MAX,
2650 f32::NAN,
2651 f32::INFINITY,
2652 f32::NEG_INFINITY,
2653 f32::EPSILON,
2654 ] {
2655 let s = CssDeclaration::new_static(prop_width(px)).to_str();
2656 assert!(
2657 !s.is_empty(),
2658 "to_str() must render something for width: {px:?}"
2659 );
2660 }
2661 }
2662
2663 #[test]
2664 fn css_declaration_to_str_dynamic_renders_var_syntax() {
2665 let s = CssDeclaration::new_dynamic(dyn_prop("my_id", prop_width(5.0))).to_str();
2666 assert!(
2667 s.starts_with("var(--my_id, "),
2668 "dynamic to_str must render CSS var() syntax, got {s:?}"
2669 );
2670 assert!(s.ends_with(')'));
2671 }
2672
2673 #[test]
2674 fn css_declaration_to_str_dynamic_with_hostile_ids_does_not_panic() {
2675 let long = "x".repeat(100_000);
2676 for id in [
2677 "",
2678 " ",
2679 "😀",
2680 "a\u{0301}\u{0301}",
2681 "--)",
2682 "\u{0}",
2683 "a\nb",
2684 long.as_str(),
2685 ] {
2686 let s = CssDeclaration::new_dynamic(dyn_prop(id, prop_width(1.0))).to_str();
2687 assert!(s.starts_with("var(--"));
2688 }
2689 }
2690
2691 #[test]
2696 fn dynamic_css_property_is_never_inheritable() {
2697 for p in [
2698 prop_text_color(0),
2699 prop_width(0.0),
2700 prop_width(f32::NAN),
2701 CssProperty::const_none(CssPropertyType::FontSize),
2702 CssProperty::const_inherit(CssPropertyType::TextColor),
2703 ] {
2704 assert!(
2705 !dyn_prop("id", p).is_inheritable(),
2706 "DynamicCssProperty::is_inheritable is unconditionally false"
2707 );
2708 }
2709 }
2710
2711 #[test]
2712 fn dynamic_css_property_relayout_follows_the_default_value_type() {
2713 assert!(dyn_prop("a", prop_width(1.0)).can_trigger_relayout());
2714 assert!(!dyn_prop("a", prop_text_color(1)).can_trigger_relayout());
2715 assert!(
2717 dyn_prop("a", CssProperty::const_auto(CssPropertyType::Width)).can_trigger_relayout()
2718 );
2719 assert!(
2720 !dyn_prop("a", CssProperty::const_none(CssPropertyType::TextColor))
2721 .can_trigger_relayout()
2722 );
2723 }
2724
2725 static STATIC_U32: u32 = 0xDEAD_BEEF;
2730
2731 #[test]
2732 fn box_or_static_heap_round_trips_through_as_ref_and_deref() {
2733 let b = BoxOrStatic::heap(123_u32);
2734 assert_eq!(*b.as_ref(), 123);
2735 assert_eq!(*b, 123, "Deref must agree with as_ref");
2736 assert_eq!(*BoxOrStatic::heap(u32::MAX).as_ref(), u32::MAX);
2737 assert_eq!(*BoxOrStatic::heap(0_u32).as_ref(), 0);
2738 let z = BoxOrStatic::heap(());
2741 let _: &() = z.as_ref();
2742 let () = *z;
2743 let big = BoxOrStatic::heap(vec![0_u8; 1_000_000]);
2744 assert_eq!(big.as_ref().len(), 1_000_000);
2745 }
2746
2747 #[test]
2748 fn box_or_static_static_variant_reads_through_as_ref() {
2749 let b: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2750 assert_eq!(*b.as_ref(), 0xDEAD_BEEF);
2751 assert_eq!(*b, 0xDEAD_BEEF);
2752 }
2753
2754 #[test]
2755 fn box_or_static_as_mut_mutates_the_boxed_value() {
2756 let mut b = BoxOrStatic::heap(1_u32);
2757 *b.as_mut() = 9;
2758 assert_eq!(*b.as_ref(), 9);
2759 }
2760
2761 #[test]
2762 #[should_panic(expected = "Cannot mutate a static BoxOrStatic value")]
2763 fn box_or_static_as_mut_on_static_panics_as_documented() {
2764 let mut b: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2765 let _ = b.as_mut();
2766 }
2767
2768 #[test]
2769 fn box_or_static_clone_is_deep_for_boxed() {
2770 let a = BoxOrStatic::heap(5_u32);
2771 let mut b = a.clone();
2772 *b.as_mut() = 6;
2773 assert_eq!(*a.as_ref(), 5, "cloning a Boxed value must not alias it");
2774 assert_eq!(*b.as_ref(), 6);
2775 assert_ne!(a, b);
2776 }
2777
2778 #[test]
2779 fn box_or_static_eq_ord_hash_all_delegate_to_the_inner_value() {
2780 let heap = BoxOrStatic::heap(0xDEAD_BEEF_u32);
2781 let stat: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2782 assert_eq!(heap, stat);
2784 assert_eq!(hash_of(&heap), hash_of(&stat));
2785 assert_eq!(heap.cmp(&stat), core::cmp::Ordering::Equal);
2786 assert_eq!(heap.partial_cmp(&stat), Some(core::cmp::Ordering::Equal));
2787
2788 let smaller = BoxOrStatic::heap(1_u32);
2789 assert!(smaller < heap);
2790 }
2791
2792 #[test]
2793 fn box_or_static_debug_and_display_render_the_inner_value() {
2794 let b = BoxOrStatic::heap(42_u32);
2795 assert_eq!(format!("{b:?}"), "42");
2796 assert_eq!(format!("{b}"), "42");
2797 let s: BoxOrStaticString = BoxOrStatic::heap(String::new().into());
2798 assert!(
2799 !format!("{s:?}").is_empty(),
2800 "Debug of an empty string payload is still well-formed"
2801 );
2802 for f in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, f64::MIN] {
2804 let bf = BoxOrStatic::heap(f);
2805 assert!(!format!("{bf:?}").is_empty());
2806 assert!(!format!("{bf}").is_empty());
2807 }
2808 }
2809
2810 #[test]
2811 fn box_or_static_default_is_a_heap_allocated_default() {
2812 let b: BoxOrStatic<u32> = BoxOrStatic::default();
2813 assert_eq!(*b.as_ref(), 0);
2814 assert!(matches!(b, BoxOrStatic::Boxed(_)));
2815 let s: BoxOrStaticString = BoxOrStatic::default();
2816 assert_eq!(s.as_ref().as_str(), "");
2817 }
2818
2819 #[test]
2820 fn box_or_static_into_inner_returns_the_payload() {
2821 assert_eq!(BoxOrStatic::heap(7_u32).into_inner(), 7);
2822 let stat: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2823 assert_eq!(stat.into_inner(), 0xDEAD_BEEF);
2824 let s: BoxOrStaticString = BoxOrStatic::heap("hello".to_string().into());
2825 assert_eq!(s.into_inner().as_str(), "hello");
2826 }
2827
2828 #[test]
2829 fn box_or_static_into_inner_must_not_leak_the_box() {
2830 use core::sync::atomic::{AtomicIsize, Ordering};
2831
2832 static LIVE: AtomicIsize = AtomicIsize::new(0);
2835
2836 struct Tracked(u32);
2837 impl Tracked {
2838 fn new(v: u32) -> Self {
2839 LIVE.fetch_add(1, Ordering::SeqCst);
2840 Self(v)
2841 }
2842 }
2843 impl Clone for Tracked {
2844 fn clone(&self) -> Self {
2845 LIVE.fetch_add(1, Ordering::SeqCst);
2846 Self(self.0)
2847 }
2848 }
2849 impl Drop for Tracked {
2850 fn drop(&mut self) {
2851 LIVE.fetch_sub(1, Ordering::SeqCst);
2852 }
2853 }
2854
2855 let boxed = BoxOrStatic::heap(Tracked::new(7));
2856 assert_eq!(LIVE.load(Ordering::SeqCst), 1);
2857
2858 let inner = boxed.into_inner();
2859 assert_eq!(inner.0, 7);
2860 drop(inner);
2861
2862 assert_eq!(
2863 LIVE.load(Ordering::SeqCst),
2864 0,
2865 "into_inner() on a Boxed variant leaks: it clones the payload and then \
2866 mem::forget(self), so `Drop for BoxOrStatic` never runs and the Box \
2867 (plus the T inside it) is never freed"
2868 );
2869 }
2870
2871 #[test]
2872 #[cfg(target_pointer_width = "64")]
2873 fn box_or_static_is_the_documented_16_bytes() {
2874 assert_eq!(size_of::<BoxOrStatic<u32>>(), 16);
2875 assert_eq!(size_of::<BoxOrStaticString>(), 16);
2876 }
2877
2878 #[test]
2883 fn css_property_value_predicates_are_mutually_exclusive() {
2884 for v in keyword_values() {
2885 let flags = [
2886 v.is_auto(),
2887 v.is_none(),
2888 v.is_initial(),
2889 v.is_inherit(),
2890 v.is_revert(),
2891 v.is_unset(),
2892 ];
2893 assert_eq!(
2894 flags.iter().filter(|b| **b).count(),
2895 1,
2896 "exactly one predicate must fire for {v:?}"
2897 );
2898 assert!(
2899 v.get_property().is_none(),
2900 "a keyword variant has no property"
2901 );
2902 }
2903
2904 let exact = CssPropertyValue::Exact(TestVal(1.0));
2905 assert!(
2906 !exact.is_auto()
2907 && !exact.is_none()
2908 && !exact.is_initial()
2909 && !exact.is_inherit()
2910 && !exact.is_revert()
2911 && !exact.is_unset(),
2912 "Exact must answer false to every keyword predicate"
2913 );
2914 }
2915
2916 #[test]
2917 fn css_property_value_predicates_pick_the_right_variant() {
2918 assert!(CssPropertyValue::<TestVal>::Auto.is_auto());
2919 assert!(CssPropertyValue::<TestVal>::None.is_none());
2920 assert!(CssPropertyValue::<TestVal>::Initial.is_initial());
2921 assert!(CssPropertyValue::<TestVal>::Inherit.is_inherit());
2922 assert!(CssPropertyValue::<TestVal>::Revert.is_revert());
2923 assert!(CssPropertyValue::<TestVal>::Unset.is_unset());
2924 }
2925
2926 #[test]
2927 fn css_property_value_get_property_and_get_property_owned_agree() {
2928 let exact = CssPropertyValue::Exact(TestVal(3.5));
2929 assert_eq!(exact.get_property(), Some(&TestVal(3.5)));
2930 assert_eq!(exact.get_property_owned(), Some(TestVal(3.5)));
2931 for v in keyword_values() {
2932 assert_eq!(v.get_property(), None);
2933 assert_eq!(v.get_property_owned(), None);
2934 }
2935 }
2936
2937 #[test]
2938 fn css_property_value_get_property_or_default_substitutes_only_auto_and_initial() {
2939 assert_eq!(
2942 CssPropertyValue::<TestVal>::Auto.get_property_or_default(),
2943 Some(TestVal::default())
2944 );
2945 assert_eq!(
2946 CssPropertyValue::<TestVal>::Initial.get_property_or_default(),
2947 Some(TestVal::default())
2948 );
2949 assert_eq!(
2950 CssPropertyValue::<TestVal>::None.get_property_or_default(),
2951 None
2952 );
2953 assert_eq!(
2954 CssPropertyValue::<TestVal>::Inherit.get_property_or_default(),
2955 None
2956 );
2957 assert_eq!(
2958 CssPropertyValue::<TestVal>::Revert.get_property_or_default(),
2959 None
2960 );
2961 assert_eq!(
2962 CssPropertyValue::<TestVal>::Unset.get_property_or_default(),
2963 None
2964 );
2965 assert_eq!(
2966 CssPropertyValue::Exact(TestVal(9.0)).get_property_or_default(),
2967 Some(TestVal(9.0))
2968 );
2969 }
2970
2971 #[test]
2972 fn css_property_value_default_is_exact_default() {
2973 assert_eq!(
2974 CssPropertyValue::<TestVal>::default(),
2975 CssPropertyValue::Exact(TestVal::default())
2976 );
2977 assert!(!CssPropertyValue::<TestVal>::default().is_auto());
2978 }
2979
2980 #[test]
2981 fn css_property_value_from_wraps_into_exact() {
2982 let v: CssPropertyValue<TestVal> = TestVal(2.0).into();
2983 assert_eq!(v, CssPropertyValue::Exact(TestVal(2.0)));
2984 }
2985
2986 #[test]
2987 fn css_property_value_keyword_serialization_is_the_css_keyword() {
2988 let cases: [(CssPropertyValue<TestVal>, &str); 6] = [
2989 (CssPropertyValue::Auto, "auto"),
2990 (CssPropertyValue::None, "none"),
2991 (CssPropertyValue::Initial, "initial"),
2992 (CssPropertyValue::Inherit, "inherit"),
2993 (CssPropertyValue::Revert, "revert"),
2994 (CssPropertyValue::Unset, "unset"),
2995 ];
2996 for (v, expected) in cases {
2997 assert_eq!(v.get_css_value_fmt(), expected);
2998 assert_eq!(
2999 format!("{v}"),
3000 expected,
3001 "Display and get_css_value_fmt must not diverge for keywords"
3002 );
3003 }
3004 let exact = CssPropertyValue::Exact(TestVal(1.5));
3005 assert_eq!(exact.get_css_value_fmt(), "1.5");
3006 assert_eq!(format!("{exact}"), "1.5");
3007 }
3008
3009 #[test]
3010 fn css_property_value_serializes_hostile_floats_without_panicking() {
3011 for f in [
3012 f32::NAN,
3013 f32::INFINITY,
3014 f32::NEG_INFINITY,
3015 f32::MIN,
3016 f32::MAX,
3017 -0.0,
3018 f32::MIN_POSITIVE,
3019 ] {
3020 let v = CssPropertyValue::Exact(TestVal(f));
3021 assert!(!v.get_css_value_fmt().is_empty());
3022 assert!(!format!("{v}").is_empty());
3023 }
3024 }
3025
3026 #[test]
3027 fn css_property_value_map_property_preserves_keyword_variants() {
3028 for v in keyword_values() {
3030 let before = format!("{v}");
3031 let mapped: CssPropertyValue<u32> =
3032 v.map_property(|_| panic!("map_fn must not run on a keyword variant"));
3033 assert_eq!(
3034 format!("{mapped}"),
3035 before,
3036 "the keyword must survive the map"
3037 );
3038 }
3039 let mapped = CssPropertyValue::Exact(TestVal(2.0)).map_property(|t| t.0 as u32);
3041 assert_eq!(mapped, CssPropertyValue::Exact(2_u32));
3042 }
3043
3044 #[test]
3045 fn css_property_value_map_property_handles_nan_and_type_changing_maps() {
3046 let mapped = CssPropertyValue::Exact(TestVal(f32::NAN)).map_property(|t| t.0.is_nan());
3047 assert_eq!(mapped, CssPropertyValue::Exact(true));
3048 let mapped = CssPropertyValue::Exact(TestVal(f32::INFINITY)).map_property(|t| t.0 as i64);
3050 assert_eq!(mapped, CssPropertyValue::Exact(i64::MAX));
3051 }
3052
3053 #[test]
3058 fn css_rule_block_new_defaults_to_author_priority_and_no_conditions() {
3059 let decls = vec![
3060 CssDeclaration::Static(prop_width(1.0)),
3061 CssDeclaration::Static(prop_text_color(2)),
3062 ];
3063 let r = CssRuleBlock::new(
3064 CssPath::new(vec![CssPathSelector::Type(NodeTypeTag::Div)]),
3065 decls.clone(),
3066 );
3067 assert_eq!(r.priority, rule_priority::AUTHOR);
3068 assert!(r.conditions.as_ref().is_empty());
3069 assert_eq!(r.declarations.as_ref(), &decls[..]);
3070 assert_eq!(r.path.selectors.as_ref().len(), 1);
3071 }
3072
3073 #[test]
3074 fn css_rule_block_with_conditions_keeps_every_condition() {
3075 let conds: Vec<DynamicSelector> = (0..1_000)
3076 .map(|i| DynamicSelector::ContainerName(format!("c{i}").into()))
3077 .collect();
3078 let r = CssRuleBlock::with_conditions(CssPath::default(), Vec::new(), conds.clone());
3079 assert_eq!(r.conditions.as_ref().len(), 1_000);
3080 assert_eq!(r.conditions.as_ref(), &conds[..]);
3081 assert_eq!(
3082 r.priority,
3083 rule_priority::AUTHOR,
3084 "with_conditions must not change the layer"
3085 );
3086 assert!(r.declarations.as_ref().is_empty());
3087 assert!(r.path.selectors.as_ref().is_empty());
3088 }
3089
3090 #[test]
3091 fn css_rule_block_default_is_empty_and_ua_priority() {
3092 let r = CssRuleBlock::default();
3093 assert!(r.path.selectors.as_ref().is_empty());
3094 assert!(r.declarations.as_ref().is_empty());
3095 assert!(r.conditions.as_ref().is_empty());
3096 assert_eq!(r.priority, rule_priority::UA, "u8::default() == 0 == UA");
3097 }
3098
3099 #[test]
3100 fn rule_priority_slots_are_strictly_ordered() {
3101 const _: () = assert!(rule_priority::UA < rule_priority::SYSTEM);
3102 const _: () = assert!(rule_priority::SYSTEM < rule_priority::AUTHOR);
3103 const _: () = assert!(rule_priority::AUTHOR < rule_priority::INLINE);
3104 const _: () = assert!(rule_priority::INLINE < rule_priority::RUNTIME);
3105 }
3106
3107 const ALL_TAGS: &[NodeTypeTag] = {
3112 use NodeTypeTag::*;
3113 &[
3114 Html,
3115 Head,
3116 Body,
3117 Div,
3118 P,
3119 Article,
3120 Section,
3121 Nav,
3122 Aside,
3123 Header,
3124 Footer,
3125 Main,
3126 Figure,
3127 FigCaption,
3128 H1,
3129 H2,
3130 H3,
3131 H4,
3132 H5,
3133 H6,
3134 Br,
3135 Hr,
3136 Pre,
3137 BlockQuote,
3138 Address,
3139 Details,
3140 Summary,
3141 Dialog,
3142 Ul,
3143 Ol,
3144 Li,
3145 Dl,
3146 Dt,
3147 Dd,
3148 Menu,
3149 MenuItem,
3150 Dir,
3151 Table,
3152 Caption,
3153 THead,
3154 TBody,
3155 TFoot,
3156 Tr,
3157 Th,
3158 Td,
3159 ColGroup,
3160 Col,
3161 Form,
3162 FieldSet,
3163 Legend,
3164 Label,
3165 Input,
3166 Button,
3167 Select,
3168 OptGroup,
3169 SelectOption,
3170 TextArea,
3171 Output,
3172 Progress,
3173 Meter,
3174 DataList,
3175 Span,
3176 A,
3177 Em,
3178 Strong,
3179 B,
3180 I,
3181 U,
3182 S,
3183 Mark,
3184 Del,
3185 Ins,
3186 Code,
3187 Samp,
3188 Kbd,
3189 Var,
3190 Cite,
3191 Dfn,
3192 Abbr,
3193 Acronym,
3194 Q,
3195 Time,
3196 Sub,
3197 Sup,
3198 Small,
3199 Big,
3200 Bdo,
3201 Bdi,
3202 Wbr,
3203 Ruby,
3204 Rt,
3205 Rtc,
3206 Rp,
3207 Data,
3208 Canvas,
3209 Object,
3210 Param,
3211 Embed,
3212 Audio,
3213 Video,
3214 Source,
3215 Track,
3216 Map,
3217 Area,
3218 Svg,
3219 SvgPath,
3220 SvgCircle,
3221 SvgRect,
3222 SvgEllipse,
3223 SvgLine,
3224 SvgPolygon,
3225 SvgPolyline,
3226 SvgG,
3227 SvgDefs,
3228 SvgSymbol,
3229 SvgUse,
3230 SvgSwitch,
3231 SvgText,
3232 SvgTspan,
3233 SvgTextPath,
3234 SvgLinearGradient,
3235 SvgRadialGradient,
3236 SvgStop,
3237 SvgPattern,
3238 SvgClipPathElement,
3239 SvgMask,
3240 SvgFilter,
3241 SvgFeBlend,
3242 SvgFeColorMatrix,
3243 SvgFeComponentTransfer,
3244 SvgFeComposite,
3245 SvgFeConvolveMatrix,
3246 SvgFeDiffuseLighting,
3247 SvgFeDisplacementMap,
3248 SvgFeDistantLight,
3249 SvgFeDropShadow,
3250 SvgFeFlood,
3251 SvgFeFuncR,
3252 SvgFeFuncG,
3253 SvgFeFuncB,
3254 SvgFeFuncA,
3255 SvgFeGaussianBlur,
3256 SvgFeImage,
3257 SvgFeMerge,
3258 SvgFeMergeNode,
3259 SvgFeMorphology,
3260 SvgFeOffset,
3261 SvgFePointLight,
3262 SvgFeSpecularLighting,
3263 SvgFeSpotLight,
3264 SvgFeTile,
3265 SvgFeTurbulence,
3266 SvgMarker,
3267 SvgImage,
3268 SvgForeignObject,
3269 SvgTitle,
3270 SvgDesc,
3271 SvgMetadata,
3272 SvgA,
3273 SvgView,
3274 SvgStyle,
3275 SvgScript,
3276 SvgAnimate,
3277 SvgAnimateMotion,
3278 SvgAnimateTransform,
3279 SvgSet,
3280 SvgMpath,
3281 Title,
3282 Meta,
3283 Link,
3284 Script,
3285 Style,
3286 Base,
3287 Text,
3288 Img,
3289 VirtualView,
3290 TransientWindow,
3291 Icon,
3292 GeolocationProbe,
3293 Before,
3294 After,
3295 Marker,
3296 Placeholder,
3297 ]
3298 };
3299
3300 #[test]
3301 fn node_type_tag_variant_list_is_complete_and_unique() {
3302 assert_eq!(
3305 ALL_TAGS.len(),
3306 183, "ALL_TAGS is out of sync with the NodeTypeTag enum"
3308 );
3309 let mut seen: Vec<NodeTypeTag> = Vec::new();
3310 for t in ALL_TAGS {
3311 assert!(!seen.contains(t), "duplicate entry in ALL_TAGS: {t:?}");
3312 seen.push(*t);
3313 }
3314 }
3315
3316 #[test]
3317 fn node_type_tag_display_names_are_all_distinct() {
3318 let mut names: Vec<String> = ALL_TAGS.iter().map(ToString::to_string).collect();
3319 names.sort();
3320 let before = names.len();
3321 names.dedup();
3322 assert_eq!(
3323 names.len(),
3324 before,
3325 "two NodeTypeTag variants serialize to the same CSS tag name — \
3326 the string is then ambiguous on the way back in"
3327 );
3328 }
3329
3330 #[test]
3331 fn node_type_tag_display_then_from_str_round_trips_every_variant() {
3332 let mut broken: Vec<(NodeTypeTag, String)> = Vec::new();
3333 for tag in ALL_TAGS {
3334 let serialized = tag.to_string();
3335 let round_trips = matches!(NodeTypeTag::from_str(&serialized), Ok(t) if t == *tag);
3338 if !round_trips {
3339 broken.push((*tag, serialized));
3340 }
3341 }
3342 assert!(
3343 broken.is_empty(),
3344 "from_str(Display(tag)) must yield tag back, but these variants do not \
3345 round-trip: {broken:?}"
3346 );
3347 }
3348
3349 #[test]
3350 fn node_type_tag_serialize_parse_serialize_is_stable() {
3351 for tag in ALL_TAGS {
3354 let once = tag.to_string();
3355 if let Ok(parsed) = NodeTypeTag::from_str(&once) {
3356 assert_eq!(
3357 parsed.to_string(),
3358 once,
3359 "serialize(parse(serialize({tag:?}))) drifted"
3360 );
3361 }
3362 }
3363 }
3364
3365 #[test]
3366 fn node_type_tag_from_str_valid_minimal() {
3367 assert_eq!(NodeTypeTag::from_str("div"), Ok(NodeTypeTag::Div));
3368 assert_eq!(NodeTypeTag::from_str("p"), Ok(NodeTypeTag::P));
3369 assert_eq!(NodeTypeTag::from_str("a"), Ok(NodeTypeTag::A));
3370 }
3371
3372 #[test]
3373 fn node_type_tag_from_str_accepts_documented_aliases() {
3374 assert_eq!(NodeTypeTag::from_str("image"), Ok(NodeTypeTag::SvgImage));
3376 assert_eq!(
3377 NodeTypeTag::from_str("svg:image"),
3378 Ok(NodeTypeTag::SvgImage)
3379 );
3380 assert_eq!(NodeTypeTag::SvgImage.to_string(), "svg:image");
3381
3382 assert_eq!(
3383 NodeTypeTag::from_str("iframe"),
3384 Ok(NodeTypeTag::VirtualView)
3385 );
3386 assert_eq!(
3387 NodeTypeTag::from_str("virtual-view"),
3388 Ok(NodeTypeTag::VirtualView)
3389 );
3390
3391 for (bare, prefixed, tag) in [
3392 ("before", "::before", NodeTypeTag::Before),
3393 ("after", "::after", NodeTypeTag::After),
3394 ("marker", "::marker", NodeTypeTag::Marker),
3395 ("placeholder", "::placeholder", NodeTypeTag::Placeholder),
3396 ] {
3397 assert_eq!(NodeTypeTag::from_str(bare), Ok(tag));
3398 assert_eq!(NodeTypeTag::from_str(prefixed), Ok(tag));
3399 assert_eq!(
3400 tag.to_string(),
3401 prefixed,
3402 "pseudo-elements must serialize in their `::` form"
3403 );
3404 }
3405 }
3406
3407 #[test]
3408 fn node_type_tag_from_str_rejects_hostile_input_without_panicking() {
3409 let long = "a".repeat(1_000_000);
3410 let nested = "<".repeat(10_000);
3411 let hostile = [
3412 "", " ", " \t\n\r ", " div", "div ", " div ", "div;garbage", "DIV", "Div",
3421 "0",
3422 "-0",
3423 "9223372036854775807", "1e400",
3425 "NaN",
3426 "inf",
3427 "-inf",
3428 "\u{1F600}", "e\u{0301}", "\u{0}", "\u{FEFF}div", "div\u{0}",
3433 "*",
3434 "::",
3435 ":::before",
3436 "<script>",
3437 long.as_str(),
3438 nested.as_str(),
3439 ];
3440 for input in hostile {
3441 match NodeTypeTag::from_str(input) {
3442 Ok(t) => panic!("{input:?} must not parse, but produced {t:?}"),
3443 Err(NodeTypeTagParseError::Invalid(echoed)) => {
3444 assert_eq!(echoed, input, "the error must echo the input verbatim");
3445 }
3446 }
3447 }
3448 }
3449
3450 #[test]
3451 fn node_type_tag_parse_error_display_names_the_offending_input() {
3452 let e = NodeTypeTagParseError::Invalid("wat");
3453 assert_eq!(format!("{e}"), "Invalid node type: wat");
3454 assert_eq!(
3456 format!("{}", NodeTypeTagParseError::Invalid("")),
3457 "Invalid node type: "
3458 );
3459 assert!(format!("{}", NodeTypeTagParseError::Invalid("😀")).contains('😀'));
3460 }
3461
3462 #[test]
3463 fn node_type_tag_parse_error_to_contained_to_shared_round_trips() {
3464 let long = "x".repeat(10_000);
3465 for s in ["", " ", "div", "😀", "e\u{0301}", "\u{0}", long.as_str()] {
3466 let shared = NodeTypeTagParseError::Invalid(s);
3467 let owned = shared.to_contained();
3468 assert_eq!(
3469 owned,
3470 NodeTypeTagParseErrorOwned::Invalid(s.to_string().into())
3471 );
3472 assert_eq!(
3473 owned.to_shared(),
3474 shared,
3475 "to_shared(to_contained(x)) must equal x"
3476 );
3477 assert_eq!(format!("{}", owned.to_shared()), format!("{shared}"));
3479 }
3480 }
3481
3482 #[test]
3487 fn css_path_new_preserves_selectors_including_empty() {
3488 assert!(CssPath::new(Vec::new()).selectors.as_ref().is_empty());
3489 assert_eq!(CssPath::new(Vec::new()), CssPath::default());
3490
3491 let sels = vec![
3492 CssPathSelector::Type(NodeTypeTag::Div),
3493 CssPathSelector::DirectChildren,
3494 CssPathSelector::Class("c".to_string().into()),
3495 ];
3496 let p = CssPath::new(sels.clone());
3497 assert_eq!(p.selectors.as_ref(), &sels[..]);
3498 }
3499
3500 #[test]
3501 fn css_path_display_and_debug_agree_and_compose() {
3502 let p = CssPath::new(vec![
3503 CssPathSelector::Type(NodeTypeTag::Div),
3504 CssPathSelector::Id("id".to_string().into()),
3505 CssPathSelector::Class("cls".to_string().into()),
3506 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
3507 ]);
3508 assert_eq!(format!("{p}"), "div#id.cls:hover");
3509 assert_eq!(
3510 format!("{p:?}"),
3511 format!("{p}"),
3512 "Debug delegates to Display"
3513 );
3514 assert_eq!(format!("{}", CssPath::default()), "");
3516 }
3517
3518 #[test]
3519 fn push_front_scope_scopes_a_bare_star_rule_to_the_node_only() {
3520 let mut p = CssPath::new(vec![CssPathSelector::Global]);
3522 p.push_front_scope(5, 9);
3523 assert_eq!(
3524 p.selectors.as_ref(),
3525 &[
3526 CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
3527 CssPathSelector::Global,
3528 ][..],
3529 "inline style must not leak past the owner node (#47)"
3530 );
3531 }
3532
3533 #[test]
3534 fn push_front_scope_scopes_a_real_selector_to_the_whole_subtree() {
3535 let mut p = CssPath::new(vec![CssPathSelector::Class("menu-item".to_string().into())]);
3536 p.push_front_scope(5, 9);
3537 assert_eq!(
3538 p.selectors.as_ref()[0],
3539 CssPathSelector::Root(CssScopeRange { start: 5, end: 9 })
3540 );
3541 assert_eq!(p.selectors.as_ref().len(), 2);
3542 }
3543
3544 #[test]
3545 fn push_front_scope_on_an_empty_path_uses_the_subtree_range() {
3546 let mut p = CssPath::default();
3548 p.push_front_scope(2, 8);
3549 assert_eq!(
3550 p.selectors.as_ref(),
3551 &[CssPathSelector::Root(CssScopeRange { start: 2, end: 8 })][..]
3552 );
3553 }
3554
3555 #[test]
3556 fn push_front_scope_at_numeric_boundaries_does_not_panic() {
3557 for (start, end) in [
3560 (0_usize, 0_usize),
3561 (0, usize::MAX),
3562 (usize::MAX, usize::MAX),
3563 (usize::MAX, 0), (9, 5), ] {
3566 let mut p = CssPath::new(vec![CssPathSelector::Class("c".to_string().into())]);
3567 p.push_front_scope(start, end);
3568 assert_eq!(
3569 p.selectors.as_ref()[0],
3570 CssPathSelector::Root(CssScopeRange { start, end })
3571 );
3572 }
3573 let mut g = CssPath::new(vec![CssPathSelector::Global]);
3575 g.push_front_scope(usize::MAX, 0);
3576 assert_eq!(
3577 g.selectors.as_ref()[0],
3578 CssPathSelector::Root(CssScopeRange {
3579 start: usize::MAX,
3580 end: usize::MAX
3581 })
3582 );
3583 }
3584
3585 #[test]
3586 fn push_front_scope_applied_twice_stacks_root_selectors() {
3587 let mut p = CssPath::new(vec![CssPathSelector::Global]);
3588 p.push_front_scope(5, 9);
3589 p.push_front_scope(1, 20);
3592 assert_eq!(
3593 p.selectors.as_ref(),
3594 &[
3595 CssPathSelector::Root(CssScopeRange { start: 1, end: 20 }),
3596 CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
3597 CssPathSelector::Global,
3598 ][..]
3599 );
3600 }
3601
3602 #[test]
3603 fn push_front_scope_on_a_long_path_preserves_order_and_length() {
3604 let sels: Vec<CssPathSelector> = (0..5_000)
3605 .map(|i| CssPathSelector::Class(format!("c{i}").into()))
3606 .collect();
3607 let mut p = CssPath::new(sels.clone());
3608 p.push_front_scope(3, 4);
3609 assert_eq!(p.selectors.as_ref().len(), 5_001);
3610 assert_eq!(
3611 p.selectors.as_ref()[0],
3612 CssPathSelector::Root(CssScopeRange { start: 3, end: 4 })
3613 );
3614 assert_eq!(
3615 &p.selectors.as_ref()[1..],
3616 &sels[..],
3617 "the tail must be untouched"
3618 );
3619 }
3620
3621 #[test]
3622 fn scope_range_contains_at_zero_and_usize_max() {
3623 let zero = CssScopeRange { start: 0, end: 0 };
3624 assert!(zero.contains(0));
3625 assert!(!zero.contains(1));
3626 assert!(!zero.contains(usize::MAX));
3627
3628 let full = CssScopeRange {
3629 start: 0,
3630 end: usize::MAX,
3631 };
3632 assert!(full.contains(0));
3633 assert!(full.contains(usize::MAX));
3634 assert!(full.contains(usize::MAX / 2));
3635
3636 let top = CssScopeRange {
3637 start: usize::MAX,
3638 end: usize::MAX,
3639 };
3640 assert!(top.contains(usize::MAX));
3641 assert!(!top.contains(usize::MAX - 1));
3642 assert!(!top.contains(0));
3643 }
3644
3645 #[test]
3646 fn scope_range_inverted_contains_nothing() {
3647 let inverted = CssScopeRange { start: 9, end: 5 };
3649 for n in [0_usize, 4, 5, 7, 9, 10, usize::MAX] {
3650 assert!(!inverted.contains(n), "inverted range must never match {n}");
3651 }
3652 }
3653
3654 #[test]
3659 fn css_path_selector_display_covers_every_variant() {
3660 let cases: Vec<(CssPathSelector, String)> = vec![
3661 (CssPathSelector::Global, "*".to_string()),
3662 (
3663 CssPathSelector::Root(CssScopeRange { start: 2, end: 6 }),
3664 ":root(2..=6)".to_string(),
3665 ),
3666 (CssPathSelector::Type(NodeTypeTag::Div), "div".to_string()),
3667 (
3668 CssPathSelector::Class("c".to_string().into()),
3669 ".c".to_string(),
3670 ),
3671 (
3672 CssPathSelector::Id("i".to_string().into()),
3673 "#i".to_string(),
3674 ),
3675 (
3676 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Focus),
3677 ":focus".to_string(),
3678 ),
3679 (
3680 CssPathSelector::Attribute(CssAttributeSelector::default()),
3681 "[]".to_string(),
3682 ),
3683 (CssPathSelector::DirectChildren, ">".to_string()),
3684 (CssPathSelector::Children, " ".to_string()),
3685 (CssPathSelector::AdjacentSibling, "+".to_string()),
3686 (CssPathSelector::GeneralSibling, "~".to_string()),
3687 ];
3688 for (sel, expected) in cases {
3689 assert_eq!(format!("{sel}"), expected);
3690 }
3691 assert_eq!(CssPathSelector::default(), CssPathSelector::Global);
3692 }
3693
3694 #[test]
3695 fn css_path_selector_display_at_scope_range_boundaries() {
3696 let s = CssPathSelector::Root(CssScopeRange {
3697 start: 0,
3698 end: usize::MAX,
3699 });
3700 assert_eq!(format!("{s}"), format!(":root(0..={})", usize::MAX));
3701 }
3702
3703 #[test]
3704 fn css_path_selector_display_with_empty_and_unicode_names() {
3705 assert_eq!(
3707 format!("{}", CssPathSelector::Class(String::new().into())),
3708 "."
3709 );
3710 assert_eq!(
3711 format!("{}", CssPathSelector::Id(String::new().into())),
3712 "#"
3713 );
3714 assert_eq!(
3715 format!("{}", CssPathSelector::Class("😀".to_string().into())),
3716 ".😀"
3717 );
3718 let long = "x".repeat(100_000);
3719 assert_eq!(
3720 format!("{}", CssPathSelector::Id(long.clone().into())).len(),
3721 long.len() + 1
3722 );
3723 }
3724
3725 #[test]
3726 fn attribute_match_op_symbol_prefix_matrix() {
3727 assert_eq!(AttributeMatchOp::Exists.symbol_prefix(), "");
3728 assert_eq!(AttributeMatchOp::Eq.symbol_prefix(), "");
3729 assert_eq!(AttributeMatchOp::Includes.symbol_prefix(), "~");
3730 assert_eq!(AttributeMatchOp::DashMatch.symbol_prefix(), "|");
3731 assert_eq!(AttributeMatchOp::Prefix.symbol_prefix(), "^");
3732 assert_eq!(AttributeMatchOp::Suffix.symbol_prefix(), "$");
3733 assert_eq!(AttributeMatchOp::Substring.symbol_prefix(), "*");
3734 assert_eq!(AttributeMatchOp::default(), AttributeMatchOp::Exists);
3735 }
3736
3737 #[test]
3738 fn css_attribute_selector_display_renders_each_operator() {
3739 let ops = [
3740 (AttributeMatchOp::Eq, "[a=\"v\"]"),
3741 (AttributeMatchOp::Includes, "[a~=\"v\"]"),
3742 (AttributeMatchOp::DashMatch, "[a|=\"v\"]"),
3743 (AttributeMatchOp::Prefix, "[a^=\"v\"]"),
3744 (AttributeMatchOp::Suffix, "[a$=\"v\"]"),
3745 (AttributeMatchOp::Substring, "[a*=\"v\"]"),
3746 ];
3747 for (op, expected) in ops {
3748 let sel = CssAttributeSelector {
3749 name: "a".to_string().into(),
3750 op,
3751 value: OptionString::Some("v".to_string().into()),
3752 };
3753 assert_eq!(format!("{sel}"), expected);
3754 }
3755 }
3756
3757 #[test]
3758 fn css_attribute_selector_exists_ignores_any_value() {
3759 let sel = CssAttributeSelector {
3761 name: "a".to_string().into(),
3762 op: AttributeMatchOp::Exists,
3763 value: OptionString::Some("ignored".to_string().into()),
3764 };
3765 assert_eq!(format!("{sel}"), "[a]");
3766 assert_eq!(format!("{}", CssAttributeSelector::default()), "[]");
3767 }
3768
3769 #[test]
3770 fn css_attribute_selector_missing_value_renders_an_empty_string_literal() {
3771 let sel = CssAttributeSelector {
3772 name: "a".to_string().into(),
3773 op: AttributeMatchOp::Eq,
3774 value: OptionString::None,
3775 };
3776 assert_eq!(format!("{sel}"), "[a=\"\"]");
3777 }
3778
3779 #[test]
3780 fn css_attribute_selector_display_with_hostile_names_and_values_does_not_panic() {
3781 for (name, value) in [
3782 ("", ""),
3783 ("😀", "😀"),
3784 ("a\u{0301}", "e\u{0301}"),
3785 ("a", "has \" quote"), ("a", "]"),
3787 ("a", "\u{0}"),
3788 ("a", "\n"),
3789 ] {
3790 let sel = CssAttributeSelector {
3791 name: name.to_string().into(),
3792 op: AttributeMatchOp::Eq,
3793 value: OptionString::Some(value.to_string().into()),
3794 };
3795 let out = format!("{sel}");
3796 assert!(out.starts_with('[') && out.ends_with(']'));
3797 }
3798 }
3799
3800 #[test]
3801 fn css_nth_child_selector_display_at_numeric_boundaries() {
3802 assert_eq!(format!("{}", CssNthChildSelector::Number(0)), "0");
3803 assert_eq!(
3804 format!("{}", CssNthChildSelector::Number(u32::MAX)),
3805 u32::MAX.to_string()
3806 );
3807 assert_eq!(format!("{}", CssNthChildSelector::Even), "even");
3808 assert_eq!(format!("{}", CssNthChildSelector::Odd), "odd");
3809 assert_eq!(
3810 format!(
3811 "{}",
3812 CssNthChildSelector::Pattern(CssNthChildPattern {
3813 pattern_repeat: 2,
3814 offset: 1,
3815 })
3816 ),
3817 "2n + 1"
3818 );
3819 assert_eq!(
3821 format!(
3822 "{}",
3823 CssNthChildSelector::Pattern(CssNthChildPattern {
3824 pattern_repeat: 0,
3825 offset: 0,
3826 })
3827 ),
3828 "0n + 0"
3829 );
3830 let maxed = CssNthChildSelector::Pattern(CssNthChildPattern {
3831 pattern_repeat: u32::MAX,
3832 offset: u32::MAX,
3833 });
3834 assert_eq!(format!("{maxed}"), format!("{}n + {}", u32::MAX, u32::MAX));
3835 }
3836
3837 #[test]
3838 fn css_path_pseudo_selector_display_covers_every_variant() {
3839 let cases: Vec<(CssPathPseudoSelector, String)> = vec![
3840 (CssPathPseudoSelector::First, "first".to_string()),
3841 (CssPathPseudoSelector::Last, "last".to_string()),
3842 (
3843 CssPathPseudoSelector::NthChild(CssNthChildSelector::Even),
3844 "nth-child(even)".to_string(),
3845 ),
3846 (CssPathPseudoSelector::Hover, "hover".to_string()),
3847 (CssPathPseudoSelector::Active, "active".to_string()),
3848 (CssPathPseudoSelector::Focus, "focus".to_string()),
3849 (CssPathPseudoSelector::SeatFocus, "seat-focus".to_string()),
3850 (
3851 CssPathPseudoSelector::Lang("de-DE".to_string().into()),
3852 "lang(de-DE)".to_string(),
3853 ),
3854 (CssPathPseudoSelector::Backdrop, "backdrop".to_string()),
3855 (CssPathPseudoSelector::Dragging, "dragging".to_string()),
3856 (CssPathPseudoSelector::DragOver, "drag-over".to_string()),
3857 ];
3858 for (p, expected) in cases {
3859 assert_eq!(format!("{p}"), expected);
3860 }
3861 }
3862
3863 #[test]
3864 fn css_path_pseudo_selector_lang_with_hostile_payloads_does_not_panic() {
3865 let long = "l".repeat(100_000);
3866 for lang in ["", " ", "😀", "e\u{0301}", ")", "\u{0}", long.as_str()] {
3867 let p = CssPathPseudoSelector::Lang(lang.to_string().into());
3868 let out = format!("{p}");
3869 assert!(out.starts_with("lang(") && out.ends_with(')'));
3870 }
3871 }
3872
3873 #[test]
3878 fn get_specificity_of_an_empty_path_is_all_zero() {
3879 assert_eq!(get_specificity(&CssPath::default()), (0, 0, 0, 0));
3880 assert_eq!(get_specificity(&CssPath::new(Vec::new())), (0, 0, 0, 0));
3881 }
3882
3883 #[test]
3884 fn get_specificity_counts_ids_classes_and_types_separately() {
3885 let path = CssPath::new(vec![
3886 CssPathSelector::Id("a".to_string().into()),
3887 CssPathSelector::Id("b".to_string().into()),
3888 CssPathSelector::Class("c".to_string().into()),
3889 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
3890 CssPathSelector::Attribute(CssAttributeSelector::default()),
3891 CssPathSelector::Type(NodeTypeTag::Div),
3892 ]);
3893 assert_eq!(get_specificity(&path), (2, 3, 1, 6));
3895 }
3896
3897 #[test]
3898 fn get_specificity_ignores_combinators_and_root_except_in_the_total() {
3899 let path = CssPath::new(vec![
3900 CssPathSelector::Root(CssScopeRange { start: 0, end: 9 }),
3901 CssPathSelector::Global,
3902 CssPathSelector::Children,
3903 CssPathSelector::DirectChildren,
3904 CssPathSelector::AdjacentSibling,
3905 CssPathSelector::GeneralSibling,
3906 ]);
3907 let (ids, classes, types, total) = get_specificity(&path);
3908 assert_eq!((ids, classes, types), (0, 0, 0));
3909 assert_eq!(total, 6, "the 4th field is the raw selector count");
3910 }
3911
3912 #[test]
3913 fn get_specificity_orders_ids_above_classes_above_types() {
3914 let id = get_specificity(&CssPath::new(vec![CssPathSelector::Id(
3915 "x".to_string().into(),
3916 )]));
3917 let class = get_specificity(&CssPath::new(vec![CssPathSelector::Class(
3918 "x".to_string().into(),
3919 )]));
3920 let ty = get_specificity(&CssPath::new(vec![CssPathSelector::Type(NodeTypeTag::Div)]));
3921 let star = get_specificity(&CssPath::new(vec![CssPathSelector::Global]));
3922 assert!(star < ty, "* must be the weakest");
3923 assert!(ty < class);
3924 assert!(class < id);
3925 }
3926
3927 #[test]
3928 fn get_specificity_on_a_huge_path_does_not_overflow_or_hang() {
3929 let sels: Vec<CssPathSelector> = (0..50_000)
3930 .map(|i| CssPathSelector::Id(format!("i{i}").into()))
3931 .collect();
3932 let path = CssPath::new(sels);
3933 assert_eq!(get_specificity(&path), (50_000, 0, 0, 50_000));
3934 }
3935
3936 #[cfg(feature = "parser")]
3941 fn parse(s: &str) -> Css {
3942 Css::from_string(s.to_string().into())
3943 }
3944
3945 #[cfg(feature = "parser")]
3946 #[test]
3947 fn viewport_breakpoints_harvests_media_bounds() {
3948 let css = Css::from_string(
3949 "@media (max-width: 400px) { .a { width: 10px; } }\n\
3950 @media (min-width: 800px) { .b { width: 10px; } }\n\
3951 @media (max-height: 300px) { .c { width: 10px; } }\n\
3952 .d { width: 10px; }"
3953 .into(),
3954 );
3955 let (w, h) = css.viewport_breakpoints();
3956 assert_eq!(w, vec![400.0, 800.0]);
3957 assert_eq!(h, vec![300.0]);
3958 }
3959
3960 #[test]
3961 fn from_string_on_empty_and_whitespace_only_input_yields_no_rules() {
3962 for input in ["", " ", " ", "\t\n\r\n ", "\u{FEFF}", "\u{00A0}"] {
3963 let css = parse(input);
3964 assert!(
3965 css.is_empty(),
3966 "{input:?} must produce zero rules, got {}",
3967 css.rules.as_ref().len()
3968 );
3969 }
3970 }
3971
3972 #[cfg(feature = "parser")]
3973 #[test]
3974 fn from_string_on_garbage_never_panics_and_is_deterministic() {
3975 let garbage = [
3976 "}}}}",
3977 "{{{{",
3978 "@@@@",
3979 ";;;;",
3980 "\u{0}\u{1}\u{2}",
3981 "div {",
3982 "div }",
3983 "} div {",
3984 "div { color",
3985 "div { color: }",
3986 "div { : red; }",
3987 "* * * * *",
3988 ":::::",
3989 "[[[[",
3990 "/* unterminated comment",
3991 "@media {",
3992 "url(",
3993 "\"unterminated",
3994 "div{color:red;}}}}",
3995 ];
3996 for input in garbage {
3997 let a = parse(input);
3998 let b = parse(input);
3999 assert_eq!(a, b, "parsing {input:?} must be deterministic");
4000 }
4001 }
4002
4003 #[cfg(feature = "parser")]
4004 #[test]
4005 fn from_string_valid_minimal_produces_one_author_rule() {
4006 let css = parse("div { width: 200px; }");
4007 assert_eq!(css.rules.as_ref().len(), 1);
4008 let rule = &css.rules.as_ref()[0];
4009 assert_eq!(
4010 rule.priority,
4011 rule_priority::AUTHOR,
4012 "parser output belongs to the author layer"
4013 );
4014 assert_eq!(
4015 rule.path.selectors.as_ref(),
4016 &[CssPathSelector::Type(NodeTypeTag::Div)][..]
4017 );
4018 let props: Vec<CssPropertyType> = css
4019 .iter_inline_properties()
4020 .map(|(p, _)| p.get_type())
4021 .collect();
4022 assert_eq!(props, vec![CssPropertyType::Width]);
4023 }
4024
4025 #[cfg(feature = "parser")]
4026 #[test]
4027 fn from_string_handles_leading_and_trailing_junk_deterministically() {
4028 assert_eq!(
4030 parse(" div { width: 1px; } "),
4031 parse("div { width: 1px; }")
4032 );
4033 let with_junk = parse("div { width: 1px; } @@@ garbage");
4035 assert!(
4036 with_junk.rules().any(
4037 |r| r.path.selectors.as_ref() == &[CssPathSelector::Type(NodeTypeTag::Div)][..]
4038 ),
4039 "the leading valid rule must survive trailing junk"
4040 );
4041 }
4042
4043 #[cfg(feature = "parser")]
4044 #[test]
4045 fn from_string_on_boundary_numbers_does_not_panic() {
4046 let inputs = [
4047 "div { width: 0px; }",
4048 "div { width: -0px; }",
4049 "div { width: -1px; }",
4050 "div { width: 9223372036854775807px; }", "div { width: 340282350000000000000000000000000000000px; }", "div { width: 1e400px; }", "div { width: 1e-400px; }", "div { width: NaN; }",
4055 "div { width: inf; }",
4056 "div { width: -inf; }",
4057 "div { width: 99999999999999999999999999px; }",
4058 "div { opacity: 1e309; }",
4059 "div { z-index: -9223372036854775808; }", "div { width: .....; }",
4061 "div { width: --5px; }",
4062 ];
4063 for input in inputs {
4064 let css = parse(input);
4065 assert!(
4066 css.rules.as_ref().len() <= 1,
4067 "{input:?} must not explode into multiple rules"
4068 );
4069 }
4070 }
4071
4072 #[cfg(feature = "parser")]
4073 #[test]
4074 fn from_string_on_unicode_input_does_not_panic() {
4075 let inputs = [
4076 "div { content: \"😀\"; }",
4077 ".😀 { width: 1px; }",
4078 "#e\u{0301} { width: 1px; }",
4079 "div { font-family: \"日本語\"; }",
4080 "div\u{0301} { width: 1px; }",
4081 "div { width: 1px; } /* 🎉 */",
4082 "\u{202E}div { width: 1px; }", ];
4084 for input in inputs {
4085 let css = parse(input);
4086 assert_eq!(css, parse(input));
4088 }
4089 }
4090
4091 #[cfg(feature = "parser")]
4092 #[test]
4093 fn from_string_on_a_one_megabyte_input_terminates() {
4094 let body = "width:1px;".repeat(100_000);
4096 assert!(body.len() >= 1_000_000);
4097 let css = parse(&format!("div{{{body}}}"));
4098 assert_eq!(css.rules.as_ref().len(), 1);
4099 assert!(!css.rules.as_ref()[0].declarations.as_ref().is_empty());
4100
4101 let junk = "a".repeat(100_000);
4103 let _ = parse(&junk);
4104 }
4105
4106 #[cfg(feature = "parser")]
4107 #[test]
4108 fn from_string_on_deeply_nested_blocks_does_not_stack_overflow() {
4109 let depth = 10_000;
4112 let mut s = String::with_capacity(depth * 8);
4113 for _ in 0..depth {
4114 s.push_str("div{");
4115 }
4116 s.push_str("width:1px;");
4117 for _ in 0..depth {
4118 s.push('}');
4119 }
4120
4121 let handle = std::thread::Builder::new()
4122 .stack_size(64 * 1024 * 1024)
4123 .spawn(move || Css::from_string(s.into()).rules.as_ref().len())
4124 .expect("spawning the parser thread must succeed");
4125
4126 let rule_count = handle
4127 .join()
4128 .expect("10_000 nested blocks must not panic or overflow the stack");
4129 assert!(
4130 rule_count <= depth + 1,
4131 "rule count must stay bounded by the nesting depth"
4132 );
4133 }
4134
4135 #[cfg(feature = "parser")]
4136 #[test]
4137 fn from_string_with_warnings_agrees_with_from_string() {
4138 for input in [
4139 "",
4140 " ",
4141 "div { width: 1px; }",
4142 "div { not-a-property: 1; }",
4143 "}}} garbage {{{",
4144 "div { width: NaN; }",
4145 ] {
4146 let (css, _warnings) = Css::from_string_with_warnings(input.to_string().into());
4147 assert_eq!(
4148 css,
4149 parse(input),
4150 "from_string_with_warnings must parse {input:?} identically to from_string"
4151 );
4152 }
4153 }
4154
4155 #[cfg(feature = "parser")]
4156 #[test]
4157 fn from_string_with_warnings_reports_an_unknown_property() {
4158 let (css, warnings) = Css::from_string_with_warnings(
4159 "div { definitely-not-a-property: 1px; }".to_string().into(),
4160 );
4161 assert!(
4162 !warnings.is_empty(),
4163 "an unknown property must surface as a warning rather than being dropped silently"
4164 );
4165 assert!(css.rules.as_ref().len() <= 1);
4168 }
4169
4170 #[cfg(feature = "parser")]
4171 #[test]
4172 fn from_string_with_warnings_on_empty_input_has_no_rules() {
4173 let (css, warnings) = Css::from_string_with_warnings(String::new().into());
4174 assert!(css.is_empty());
4175 assert!(warnings.is_empty());
4176 }
4177
4178 #[cfg(feature = "parser")]
4179 #[test]
4180 fn parse_inline_marks_every_rule_as_the_inline_layer() {
4181 let css = Css::parse_inline("width: 200px; color: red;");
4182 assert!(!css.is_empty());
4183 for r in css.rules() {
4184 assert_eq!(
4185 r.priority,
4186 rule_priority::INLINE,
4187 "parse_inline must stamp every rule with the INLINE layer"
4188 );
4189 }
4190 let props: Vec<CssPropertyType> = css
4191 .iter_inline_properties()
4192 .map(|(p, _)| p.get_type())
4193 .collect();
4194 assert!(props.contains(&CssPropertyType::Width));
4195 assert!(props.contains(&CssPropertyType::TextColor));
4196 }
4197
4198 #[cfg(feature = "parser")]
4199 #[test]
4200 fn parse_inline_wraps_bare_declarations_in_a_star_rule() {
4201 let css = Css::parse_inline("width: 200px;");
4202 assert_eq!(css.rules.as_ref().len(), 1);
4203 assert_eq!(
4204 css.rules.as_ref()[0].path.selectors.as_ref(),
4205 &[CssPathSelector::Global][..],
4206 "the wrapper path must be exactly `*` — push_front_scope keys node-only \
4207 inline semantics off that shape"
4208 );
4209 }
4210
4211 #[cfg(feature = "parser")]
4212 #[test]
4213 fn parse_inline_on_empty_and_whitespace_input_does_not_panic() {
4214 for input in ["", " ", "\t\n", " \r\n "] {
4215 let css = Css::parse_inline(input);
4216 for r in css.rules() {
4217 assert_eq!(r.priority, rule_priority::INLINE);
4218 assert!(
4219 r.declarations.as_ref().is_empty(),
4220 "an empty inline style must not produce declarations"
4221 );
4222 }
4223 }
4224 }
4225
4226 #[cfg(feature = "parser")]
4227 #[test]
4228 fn parse_inline_on_garbage_never_panics_and_is_deterministic() {
4229 for input in [
4230 "}}}}",
4231 "{{{{",
4232 ";;;;",
4233 ":::",
4234 "color",
4235 "color:",
4236 ": red",
4237 "\u{0}\u{1}",
4238 "/* unterminated",
4239 "@@@",
4240 "width: 1px", ] {
4242 assert_eq!(
4243 Css::parse_inline(input),
4244 Css::parse_inline(input),
4245 "parse_inline({input:?}) must be deterministic"
4246 );
4247 }
4248 }
4249
4250 #[cfg(feature = "parser")]
4251 #[test]
4252 fn parse_inline_on_unicode_and_boundary_numbers_does_not_panic() {
4253 for input in [
4254 "content: \"😀\"",
4255 "font-family: \"日本語\"",
4256 "width: 0px",
4257 "width: -0px",
4258 "width: 9223372036854775807px",
4259 "width: 1e400px",
4260 "width: NaN",
4261 "width: inf",
4262 "opacity: 1e309",
4263 ] {
4264 let css = Css::parse_inline(input);
4265 for r in css.rules() {
4266 assert_eq!(r.priority, rule_priority::INLINE);
4267 }
4268 }
4269 }
4270
4271 #[cfg(feature = "parser")]
4272 #[test]
4273 fn parse_inline_on_a_one_megabyte_style_terminates() {
4274 let style = "width:1px;".repeat(100_000);
4275 assert!(style.len() >= 1_000_000);
4276 let css = Css::parse_inline(&style);
4277 assert!(!css.is_empty());
4278 for r in css.rules() {
4279 assert_eq!(r.priority, rule_priority::INLINE);
4280 }
4281 }
4282
4283 #[cfg(feature = "parser")]
4284 #[test]
4285 fn parse_inline_supports_nested_pseudo_blocks() {
4286 let css = Css::parse_inline(":hover { color: red; }");
4288 assert!(!css.is_empty(), "a nested pseudo block must produce a rule");
4289 for r in css.rules() {
4290 assert_eq!(r.priority, rule_priority::INLINE);
4291 }
4292 }
4293
4294 #[cfg(feature = "parser")]
4295 #[test]
4296 fn parse_inline_must_not_let_a_brace_escape_the_star_wrapper() {
4297 let css = Css::parse_inline("color: red; } div { background: green;");
4303 for r in css.rules() {
4304 let first = r.path.selectors.as_ref().first();
4305 assert!(
4306 matches!(first, None | Some(CssPathSelector::Global)),
4307 "a `}}` in the inline style escaped the `*` wrapper and produced the \
4308 free-standing rule `{}` (selector injection)",
4309 r.path
4310 );
4311 }
4312 }
4313}