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}
35
36impl_option!(
37 Css,
38 OptionCss,
39 copy = false,
40 [Debug, Clone, PartialEq, Eq, PartialOrd]
41);
42
43impl_vec!(Css, CssVec, CssVecDestructor, CssVecDestructorType, CssVecSlice, OptionCss);
44impl_vec_mut!(Css, CssVec);
45impl_vec_debug!(Css, CssVec);
46impl_vec_partialord!(Css, CssVec);
47impl_vec_clone!(Css, CssVec, CssVecDestructor);
48impl_vec_partialeq!(Css, CssVec);
49
50impl_vec!(CssRuleBlock, CssRuleBlockVec, CssRuleBlockVecDestructor, CssRuleBlockVecDestructorType, CssRuleBlockVecSlice, OptionCssRuleBlock);
51impl_vec_mut!(CssRuleBlock, CssRuleBlockVec);
52impl_vec_debug!(CssRuleBlock, CssRuleBlockVec);
53impl_vec_partialord!(CssRuleBlock, CssRuleBlockVec);
54impl_vec_clone!(CssRuleBlock, CssRuleBlockVec, CssRuleBlockVecDestructor);
55impl_vec_partialeq!(CssRuleBlock, CssRuleBlockVec);
56
57impl Css {
58 #[must_use] pub fn is_empty(&self) -> bool {
59 self.rules.as_ref().is_empty()
60 }
61
62 #[must_use] pub fn new(rules: Vec<CssRuleBlock>) -> Self {
63 Self {
64 rules: rules.into(),
65 }
66 }
67
68 #[cfg(feature = "parser")]
69 #[allow(clippy::needless_pass_by_value)]
72 #[must_use] pub fn from_string(s: AzString) -> Self {
73 crate::parser2::new_from_str(s.as_str()).0
74 }
75
76 #[cfg(feature = "parser")]
84 #[must_use] pub fn parse_inline(style: &str) -> Self {
85 use alloc::string::ToString;
86 let mut wrapped = String::with_capacity(style.len() + 6);
87 wrapped.push_str("* {\n");
88 wrapped.push_str(style);
89 wrapped.push_str("\n}");
90 let (mut css, _warnings) = crate::parser2::new_from_str(&wrapped);
91 for rule in css.rules.as_mut() {
92 rule.priority = rule_priority::INLINE;
93 }
94 css
95 }
96
97 #[cfg(feature = "parser")]
98 #[allow(clippy::needless_pass_by_value)]
101 #[must_use] pub fn from_string_with_warnings(
102 s: AzString,
103 ) -> (Self, Vec<crate::parser2::CssParseWarnMsgOwned>) {
104 let (css, warnings) = crate::parser2::new_from_str(s.as_str());
105 (
106 css,
107 warnings
108 .into_iter()
109 .map(|w| crate::parser2::CssParseWarnMsgOwned {
110 warning: w.warning.to_contained(),
111 location: w.location,
112 })
113 .collect(),
114 )
115 }
116}
117
118impl From<Vec<CssRuleBlock>> for Css {
119 fn from(rules: Vec<CssRuleBlock>) -> Self {
120 Self {
121 rules: rules.into(),
122 }
123 }
124}
125
126impl Eq for Css {}
130impl PartialOrd for Css {
133 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
134 Some(self.cmp(other))
135 }
136}
137impl Ord for Css {
138 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
139 self.rules.as_ref().len().cmp(&other.rules.as_ref().len())
140 }
141}
142impl Eq for CssRuleBlock {}
143impl Ord for CssRuleBlock {
144 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
145 self.path.cmp(&other.path).then_with(|| self.declarations.cmp(&other.declarations))
149 }
150}
151
152impl From<crate::dynamic_selector::CssPropertyWithConditionsVec> for Css {
161 fn from(props: crate::dynamic_selector::CssPropertyWithConditionsVec) -> Self {
162 let owned = props.into_library_owned_vec();
170 let mut rules: Vec<CssRuleBlock> = Vec::with_capacity(owned.len());
171 for p in owned {
172 rules.push(CssRuleBlock {
173 path: CssPath { selectors: Vec::new().into() },
174 declarations: alloc::vec![CssDeclaration::Static(p.property)].into(),
175 conditions: p.apply_if,
176 priority: rule_priority::INLINE,
177 });
178 }
179 Self { rules: rules.into() }
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
185#[repr(C, u8)]
186pub enum CssDeclaration {
187 Static(CssProperty),
189 Dynamic(DynamicCssProperty),
191}
192
193impl_option!(
194 CssDeclaration,
195 OptionCssDeclaration,
196 copy = false,
197 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
198);
199
200impl CssDeclaration {
201 #[must_use] pub const fn new_static(prop: CssProperty) -> Self {
202 Self::Static(prop)
203 }
204
205 #[must_use] pub const fn new_dynamic(prop: DynamicCssProperty) -> Self {
206 Self::Dynamic(prop)
207 }
208
209 #[must_use] pub const fn get_type(&self) -> CssPropertyType {
211 use self::CssDeclaration::{Static, Dynamic};
212 match self {
213 Static(s) => s.get_type(),
214 Dynamic(d) => d.default_value.get_type(),
215 }
216 }
217
218 #[must_use] pub const fn is_inheritable(&self) -> bool {
221 use self::CssDeclaration::{Static, Dynamic};
222 match self {
223 Static(s) => s.get_type().is_inheritable(),
224 Dynamic(d) => d.is_inheritable(),
225 }
226 }
227
228 #[must_use] pub const fn can_trigger_relayout(&self) -> bool {
231 use self::CssDeclaration::{Static, Dynamic};
232 match self {
233 Static(s) => s.get_type().can_trigger_relayout(),
234 Dynamic(d) => d.can_trigger_relayout(),
235 }
236 }
237
238 #[must_use] pub fn to_str(&self) -> String {
239 use self::CssDeclaration::{Static, Dynamic};
240 match self {
241 Static(s) => format!("{s:?}"),
242 Dynamic(d) => format!("var(--{}, {:?})", d.dynamic_id, d.default_value),
243 }
244 }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
268#[repr(C)]
269pub struct DynamicCssProperty {
270 pub dynamic_id: AzString,
272 pub default_value: CssProperty,
274}
275
276#[repr(C, u8)]
286pub enum BoxOrStatic<T> {
287 Boxed(*mut T),
289 Static(*const T),
291}
292
293impl<T> BoxOrStatic<T> {
294 #[inline]
296 pub fn heap(value: T) -> Self {
297 Self::Boxed(Box::into_raw(Box::new(value)))
298 }
299
300 #[inline]
306 #[must_use] pub fn as_ref(&self) -> &T {
307 match self {
308 Self::Boxed(ptr) => unsafe {
309 debug_assert!(!ptr.is_null(), "BoxOrStatic::Boxed contained a null pointer");
310 &**ptr
311 },
312 Self::Static(ptr) => unsafe {
313 debug_assert!(!ptr.is_null(), "BoxOrStatic::Static contained a null pointer");
314 &**ptr
315 },
316 }
317 }
318
319 #[inline]
326 pub fn as_mut(&mut self) -> &mut T {
327 match self {
328 Self::Boxed(ptr) => unsafe { &mut **ptr },
329 Self::Static(_) => panic!("Cannot mutate a static BoxOrStatic value"),
330 }
331 }
332
333 #[inline]
335 #[must_use] pub fn into_inner(self) -> T where T: Clone {
336 let val = self.as_ref().clone();
337 core::mem::forget(self);
339 val
340 }
341}
342
343impl<T> Drop for BoxOrStatic<T> {
344 fn drop(&mut self) {
345 if let Self::Boxed(ptr) = self {
346 if !ptr.is_null() {
347 unsafe { drop(Box::from_raw(*ptr)); }
348 *ptr = core::ptr::null_mut();
349 }
350 }
351 }
352}
353
354impl<T: Clone> Clone for BoxOrStatic<T> {
355 fn clone(&self) -> Self {
356 match self {
357 Self::Boxed(ptr) => {
358 let val = unsafe { &**ptr }.clone();
359 Self::Boxed(Box::into_raw(Box::new(val)))
360 }
361 Self::Static(ptr) => Self::Static(*ptr),
362 }
363 }
364}
365
366impl<T: fmt::Debug> fmt::Debug for BoxOrStatic<T> {
367 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368 self.as_ref().fmt(f)
369 }
370}
371
372impl<T: fmt::Display> fmt::Display for BoxOrStatic<T> {
373 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374 self.as_ref().fmt(f)
375 }
376}
377
378impl<T: PartialEq> PartialEq for BoxOrStatic<T> {
379 fn eq(&self, other: &Self) -> bool {
380 self.as_ref() == other.as_ref()
381 }
382}
383
384impl<T: Eq> Eq for BoxOrStatic<T> {}
385
386impl<T: core::hash::Hash> core::hash::Hash for BoxOrStatic<T> {
387 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
388 self.as_ref().hash(state);
389 }
390}
391
392impl<T: PartialOrd> PartialOrd for BoxOrStatic<T> {
393 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
394 self.as_ref().partial_cmp(other.as_ref())
395 }
396}
397
398impl<T: Ord> Ord for BoxOrStatic<T> {
399 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
400 self.as_ref().cmp(other.as_ref())
401 }
402}
403
404impl<T> core::ops::Deref for BoxOrStatic<T> {
405 type Target = T;
406 #[inline]
407 fn deref(&self) -> &T {
408 self.as_ref()
409 }
410}
411
412impl<T: Default> Default for BoxOrStatic<T> {
413 fn default() -> Self {
414 Self::heap(T::default())
415 }
416}
417
418impl<T: PrintAsCssValue> PrintAsCssValue for BoxOrStatic<T> {
419 fn print_as_css_value(&self) -> String {
420 self.as_ref().print_as_css_value()
421 }
422}
423
424unsafe impl<T: Send + 'static> Send for BoxOrStatic<T> {}
426unsafe impl<T: Sync + 'static> Sync for BoxOrStatic<T> {}
428
429pub type BoxOrStaticStyleBoxShadow = BoxOrStatic<crate::props::style::box_shadow::StyleBoxShadow>;
431
432pub type BoxOrStaticString = BoxOrStatic<AzString>;
434
435#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
438#[repr(C, u8)] pub enum CssPropertyValue<T> {
440 Auto,
441 None,
442 Initial,
443 Inherit,
444 Revert,
445 Unset,
446 Exact(T),
447}
448
449pub trait PrintAsCssValue {
451 fn print_as_css_value(&self) -> String;
452}
453
454impl<T: PrintAsCssValue> CssPropertyValue<T> {
455 pub fn get_css_value_fmt(&self) -> String {
456 match self {
457 Self::Auto => "auto".to_string(),
458 Self::None => "none".to_string(),
459 Self::Initial => "initial".to_string(),
460 Self::Inherit => "inherit".to_string(),
461 Self::Revert => "revert".to_string(),
462 Self::Unset => "unset".to_string(),
463 Self::Exact(e) => e.print_as_css_value(),
464 }
465 }
466}
467
468impl<T: fmt::Display> fmt::Display for CssPropertyValue<T> {
469 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470 use self::CssPropertyValue::{Auto, None, Initial, Inherit, Revert, Unset, Exact};
471 match self {
472 Auto => write!(f, "auto"),
473 None => write!(f, "none"),
474 Initial => write!(f, "initial"),
475 Inherit => write!(f, "inherit"),
476 Revert => write!(f, "revert"),
477 Unset => write!(f, "unset"),
478 Exact(e) => write!(f, "{e}"),
479 }
480 }
481}
482
483impl<T> From<T> for CssPropertyValue<T> {
484 fn from(c: T) -> Self {
485 Self::Exact(c)
486 }
487}
488
489impl<T> CssPropertyValue<T> {
490 #[inline]
493 pub fn map_property<F: Fn(T) -> U, U>(self, map_fn: F) -> CssPropertyValue<U> {
494 match self {
495 Self::Exact(c) => CssPropertyValue::Exact(map_fn(c)),
496 Self::Auto => CssPropertyValue::Auto,
497 Self::None => CssPropertyValue::None,
498 Self::Initial => CssPropertyValue::Initial,
499 Self::Inherit => CssPropertyValue::Inherit,
500 Self::Revert => CssPropertyValue::Revert,
501 Self::Unset => CssPropertyValue::Unset,
502 }
503 }
504
505 #[inline]
506 pub const fn get_property(&self) -> Option<&T> {
507 match self {
508 Self::Exact(c) => Some(c),
509 _ => None,
510 }
511 }
512
513 #[inline]
514 pub fn get_property_owned(self) -> Option<T> {
515 match self {
516 Self::Exact(c) => Some(c),
517 _ => None,
518 }
519 }
520
521 #[inline]
522 pub const fn is_auto(&self) -> bool {
523 matches!(self, Self::Auto)
524 }
525
526 #[inline]
527 pub const fn is_none(&self) -> bool {
528 matches!(self, Self::None)
529 }
530
531 #[inline]
532 pub const fn is_initial(&self) -> bool {
533 matches!(self, Self::Initial)
534 }
535
536 #[inline]
537 pub const fn is_inherit(&self) -> bool {
538 matches!(self, Self::Inherit)
539 }
540
541 #[inline]
542 pub const fn is_revert(&self) -> bool {
543 matches!(self, Self::Revert)
544 }
545
546 #[inline]
547 pub const fn is_unset(&self) -> bool {
548 matches!(self, Self::Unset)
549 }
550}
551
552impl<T: Default> CssPropertyValue<T> {
553 #[inline]
554 pub fn get_property_or_default(self) -> Option<T> {
555 match self {
556 Self::Auto | Self::Initial => Some(T::default()),
557 Self::Exact(c) => Some(c),
558 Self::None
559 | Self::Inherit
560 | Self::Revert
561 | Self::Unset => None,
562 }
563 }
564}
565
566impl<T: Default> Default for CssPropertyValue<T> {
567 #[inline]
568 fn default() -> Self {
569 Self::Exact(T::default())
570 }
571}
572
573impl DynamicCssProperty {
574 #[must_use] pub const fn is_inheritable(&self) -> bool {
575 false
579 }
580
581 #[must_use] pub const fn can_trigger_relayout(&self) -> bool {
582 self.default_value.get_type().can_trigger_relayout()
583 }
584}
585
586pub mod rule_priority {
594 pub const UA: u8 = 0;
598
599 pub const SYSTEM: u8 = 10;
604
605 pub const AUTHOR: u8 = 20;
608
609 pub const INLINE: u8 = 30;
613
614 pub const RUNTIME: u8 = 50;
623}
624
625#[derive(Debug, Default, Clone, PartialEq)]
631#[repr(C)]
632pub struct CssRuleBlock {
633 pub path: CssPath,
635 pub declarations: CssDeclarationVec,
638 pub conditions: DynamicSelectorVec,
641 pub priority: u8,
645}
646
647impl_option!(
648 CssRuleBlock,
649 OptionCssRuleBlock,
650 copy = false,
651 [Debug, Clone, PartialEq, Eq, PartialOrd]
652);
653
654impl PartialOrd for CssRuleBlock {
655 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
656 match self.path.partial_cmp(&other.path) {
658 Some(core::cmp::Ordering::Equal) => self.declarations.partial_cmp(&other.declarations),
659 ord => ord,
660 }
661 }
662}
663
664impl_vec!(CssDeclaration, CssDeclarationVec, CssDeclarationVecDestructor, CssDeclarationVecDestructorType, CssDeclarationVecSlice, OptionCssDeclaration);
665impl_vec_mut!(CssDeclaration, CssDeclarationVec);
666impl_vec_debug!(CssDeclaration, CssDeclarationVec);
667impl_vec_partialord!(CssDeclaration, CssDeclarationVec);
668impl_vec_ord!(CssDeclaration, CssDeclarationVec);
669impl_vec_clone!(
670 CssDeclaration,
671 CssDeclarationVec,
672 CssDeclarationVecDestructor
673);
674impl_vec_partialeq!(CssDeclaration, CssDeclarationVec);
675impl_vec_eq!(CssDeclaration, CssDeclarationVec);
676impl_vec_hash!(CssDeclaration, CssDeclarationVec);
677
678impl CssRuleBlock {
679 #[must_use] pub fn new(path: CssPath, declarations: Vec<CssDeclaration>) -> Self {
680 Self {
681 path,
682 declarations: declarations.into(),
683 conditions: DynamicSelectorVec::from_const_slice(&[]),
684 priority: rule_priority::AUTHOR,
685 }
686 }
687
688 #[must_use] pub fn with_conditions(
689 path: CssPath,
690 declarations: Vec<CssDeclaration>,
691 conditions: Vec<crate::dynamic_selector::DynamicSelector>,
692 ) -> Self {
693 Self {
694 path,
695 declarations: declarations.into(),
696 conditions: conditions.into(),
697 priority: rule_priority::AUTHOR,
698 }
699 }
700}
701
702pub type CssContentGroup<'a> = Vec<&'a CssPathSelector>;
704
705#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
707#[repr(C)]
708pub enum NodeTypeTag {
709 Html,
711 Head,
712 Body,
713
714 Div,
716 P,
717 Article,
718 Section,
719 Nav,
720 Aside,
721 Header,
722 Footer,
723 Main,
724 Figure,
725 FigCaption,
726
727 H1,
729 H2,
730 H3,
731 H4,
732 H5,
733 H6,
734
735 Br,
737 Hr,
738 Pre,
739 BlockQuote,
740 Address,
741 Details,
742 Summary,
743 Dialog,
744
745 Ul,
747 Ol,
748 Li,
749 Dl,
750 Dt,
751 Dd,
752 Menu,
753 MenuItem,
754 Dir,
755
756 Table,
758 Caption,
759 THead,
760 TBody,
761 TFoot,
762 Tr,
763 Th,
764 Td,
765 ColGroup,
766 Col,
767
768 Form,
770 FieldSet,
771 Legend,
772 Label,
773 Input,
774 Button,
775 Select,
776 OptGroup,
777 SelectOption,
778 TextArea,
779 Output,
780 Progress,
781 Meter,
782 DataList,
783
784 Span,
786 A,
787 Em,
788 Strong,
789 B,
790 I,
791 U,
792 S,
793 Mark,
794 Del,
795 Ins,
796 Code,
797 Samp,
798 Kbd,
799 Var,
800 Cite,
801 Dfn,
802 Abbr,
803 Acronym,
804 Q,
805 Time,
806 Sub,
807 Sup,
808 Small,
809 Big,
810 Bdo,
811 Bdi,
812 Wbr,
813 Ruby,
814 Rt,
815 Rtc,
816 Rp,
817 Data,
818
819 Canvas,
821 Object,
822 Param,
823 Embed,
824 Audio,
825 Video,
826 Source,
827 Track,
828 Map,
829 Area,
830 Svg,
831 SvgPath,
833 SvgCircle,
835 SvgRect,
837 SvgEllipse,
839 SvgLine,
841 SvgPolygon,
843 SvgPolyline,
845 SvgG,
847
848 SvgDefs,
851 SvgSymbol,
853 SvgUse,
855 SvgSwitch,
857
858 SvgText,
861 SvgTspan,
863 SvgTextPath,
865
866 SvgLinearGradient,
869 SvgRadialGradient,
871 SvgStop,
873 SvgPattern,
875
876 SvgClipPathElement,
879 SvgMask,
881
882 SvgFilter,
885 SvgFeBlend,
887 SvgFeColorMatrix,
889 SvgFeComponentTransfer,
891 SvgFeComposite,
893 SvgFeConvolveMatrix,
895 SvgFeDiffuseLighting,
897 SvgFeDisplacementMap,
899 SvgFeDistantLight,
901 SvgFeDropShadow,
903 SvgFeFlood,
905 SvgFeFuncR,
907 SvgFeFuncG,
909 SvgFeFuncB,
911 SvgFeFuncA,
913 SvgFeGaussianBlur,
915 SvgFeImage,
917 SvgFeMerge,
919 SvgFeMergeNode,
921 SvgFeMorphology,
923 SvgFeOffset,
925 SvgFePointLight,
927 SvgFeSpecularLighting,
929 SvgFeSpotLight,
931 SvgFeTile,
933 SvgFeTurbulence,
935
936 SvgMarker,
939 SvgImage,
941 SvgForeignObject,
943
944 SvgTitle,
947 SvgDesc,
949 SvgMetadata,
951 SvgA,
953 SvgView,
955 SvgStyle,
957 SvgScript,
959
960 SvgAnimate,
963 SvgAnimateMotion,
965 SvgAnimateTransform,
967 SvgSet,
969 SvgMpath,
971
972 Title,
974 Meta,
975 Link,
976 Script,
977 Style,
978 Base,
979
980 Text,
982 Img,
983 VirtualView,
984 Icon,
986 GeolocationProbe,
989
990 Before,
992 After,
993 Marker,
994 Placeholder,
995}
996
997#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
999pub enum NodeTypeTagParseError<'a> {
1000 Invalid(&'a str),
1001}
1002
1003impl fmt::Display for NodeTypeTagParseError<'_> {
1004 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1005 match &self {
1006 NodeTypeTagParseError::Invalid(e) => write!(f, "Invalid node type: {e}"),
1007 }
1008 }
1009}
1010
1011#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1013#[repr(C, u8)]
1014pub enum NodeTypeTagParseErrorOwned {
1015 Invalid(AzString),
1016}
1017
1018impl NodeTypeTagParseError<'_> {
1019 #[must_use] pub fn to_contained(&self) -> NodeTypeTagParseErrorOwned {
1020 match self {
1021 NodeTypeTagParseError::Invalid(s) => NodeTypeTagParseErrorOwned::Invalid((*s).to_string().into()),
1022 }
1023 }
1024}
1025
1026impl NodeTypeTagParseErrorOwned {
1027 #[must_use] pub fn to_shared(&self) -> NodeTypeTagParseError<'_> {
1028 match self {
1029 Self::Invalid(s) => NodeTypeTagParseError::Invalid(s),
1030 }
1031 }
1032}
1033
1034impl NodeTypeTag {
1036 #[allow(clippy::too_many_lines)] pub fn from_str(css_key: &str) -> Result<Self, NodeTypeTagParseError<'_>> {
1041 match css_key {
1042 "html" => Ok(Self::Html),
1044 "head" => Ok(Self::Head),
1045 "body" => Ok(Self::Body),
1046
1047 "div" => Ok(Self::Div),
1049 "p" => Ok(Self::P),
1050 "article" => Ok(Self::Article),
1051 "section" => Ok(Self::Section),
1052 "nav" => Ok(Self::Nav),
1053 "aside" => Ok(Self::Aside),
1054 "header" => Ok(Self::Header),
1055 "footer" => Ok(Self::Footer),
1056 "main" => Ok(Self::Main),
1057 "figure" => Ok(Self::Figure),
1058 "figcaption" => Ok(Self::FigCaption),
1059
1060 "h1" => Ok(Self::H1),
1062 "h2" => Ok(Self::H2),
1063 "h3" => Ok(Self::H3),
1064 "h4" => Ok(Self::H4),
1065 "h5" => Ok(Self::H5),
1066 "h6" => Ok(Self::H6),
1067
1068 "br" => Ok(Self::Br),
1070 "hr" => Ok(Self::Hr),
1071 "pre" => Ok(Self::Pre),
1072 "blockquote" => Ok(Self::BlockQuote),
1073 "address" => Ok(Self::Address),
1074 "details" => Ok(Self::Details),
1075 "summary" => Ok(Self::Summary),
1076 "dialog" => Ok(Self::Dialog),
1077
1078 "ul" => Ok(Self::Ul),
1080 "ol" => Ok(Self::Ol),
1081 "li" => Ok(Self::Li),
1082 "dl" => Ok(Self::Dl),
1083 "dt" => Ok(Self::Dt),
1084 "dd" => Ok(Self::Dd),
1085 "menu" => Ok(Self::Menu),
1086 "menuitem" => Ok(Self::MenuItem),
1087 "dir" => Ok(Self::Dir),
1088
1089 "table" => Ok(Self::Table),
1091 "caption" => Ok(Self::Caption),
1092 "thead" => Ok(Self::THead),
1093 "tbody" => Ok(Self::TBody),
1094 "tfoot" => Ok(Self::TFoot),
1095 "tr" => Ok(Self::Tr),
1096 "th" => Ok(Self::Th),
1097 "td" => Ok(Self::Td),
1098 "colgroup" => Ok(Self::ColGroup),
1099 "col" => Ok(Self::Col),
1100
1101 "form" => Ok(Self::Form),
1103 "fieldset" => Ok(Self::FieldSet),
1104 "legend" => Ok(Self::Legend),
1105 "label" => Ok(Self::Label),
1106 "input" => Ok(Self::Input),
1107 "button" => Ok(Self::Button),
1108 "select" => Ok(Self::Select),
1109 "optgroup" => Ok(Self::OptGroup),
1110 "option" => Ok(Self::SelectOption),
1111 "textarea" => Ok(Self::TextArea),
1112 "output" => Ok(Self::Output),
1113 "progress" => Ok(Self::Progress),
1114 "meter" => Ok(Self::Meter),
1115 "datalist" => Ok(Self::DataList),
1116
1117 "span" => Ok(Self::Span),
1119 "a" => Ok(Self::A),
1120 "em" => Ok(Self::Em),
1121 "strong" => Ok(Self::Strong),
1122 "b" => Ok(Self::B),
1123 "i" => Ok(Self::I),
1124 "u" => Ok(Self::U),
1125 "s" => Ok(Self::S),
1126 "mark" => Ok(Self::Mark),
1127 "del" => Ok(Self::Del),
1128 "ins" => Ok(Self::Ins),
1129 "code" => Ok(Self::Code),
1130 "samp" => Ok(Self::Samp),
1131 "kbd" => Ok(Self::Kbd),
1132 "var" => Ok(Self::Var),
1133 "cite" => Ok(Self::Cite),
1134 "dfn" => Ok(Self::Dfn),
1135 "abbr" => Ok(Self::Abbr),
1136 "acronym" => Ok(Self::Acronym),
1137 "q" => Ok(Self::Q),
1138 "time" => Ok(Self::Time),
1139 "sub" => Ok(Self::Sub),
1140 "sup" => Ok(Self::Sup),
1141 "small" => Ok(Self::Small),
1142 "big" => Ok(Self::Big),
1143 "bdo" => Ok(Self::Bdo),
1144 "bdi" => Ok(Self::Bdi),
1145 "wbr" => Ok(Self::Wbr),
1146 "ruby" => Ok(Self::Ruby),
1147 "rt" => Ok(Self::Rt),
1148 "rtc" => Ok(Self::Rtc),
1149 "rp" => Ok(Self::Rp),
1150 "data" => Ok(Self::Data),
1151
1152 "canvas" => Ok(Self::Canvas),
1154 "object" => Ok(Self::Object),
1155 "param" => Ok(Self::Param),
1156 "embed" => Ok(Self::Embed),
1157 "audio" => Ok(Self::Audio),
1158 "video" => Ok(Self::Video),
1159 "source" => Ok(Self::Source),
1160 "track" => Ok(Self::Track),
1161 "map" => Ok(Self::Map),
1162 "area" => Ok(Self::Area),
1163 "svg" => Ok(Self::Svg),
1164
1165 "path" => Ok(Self::SvgPath),
1167 "circle" => Ok(Self::SvgCircle),
1168 "rect" => Ok(Self::SvgRect),
1169 "ellipse" => Ok(Self::SvgEllipse),
1170 "line" => Ok(Self::SvgLine),
1171 "polygon" => Ok(Self::SvgPolygon),
1172 "polyline" => Ok(Self::SvgPolyline),
1173 "g" => Ok(Self::SvgG),
1174
1175 "defs" => Ok(Self::SvgDefs),
1177 "symbol" => Ok(Self::SvgSymbol),
1178 "use" => Ok(Self::SvgUse),
1179 "switch" => Ok(Self::SvgSwitch),
1180
1181 "svg:text" => Ok(Self::SvgText),
1183 "tspan" => Ok(Self::SvgTspan),
1184 "textpath" => Ok(Self::SvgTextPath),
1185
1186 "lineargradient" => Ok(Self::SvgLinearGradient),
1188 "radialgradient" => Ok(Self::SvgRadialGradient),
1189 "stop" => Ok(Self::SvgStop),
1190 "pattern" => Ok(Self::SvgPattern),
1191
1192 "clippath" => Ok(Self::SvgClipPathElement),
1194 "mask" => Ok(Self::SvgMask),
1195
1196 "filter" => Ok(Self::SvgFilter),
1198 "feblend" => Ok(Self::SvgFeBlend),
1199 "fecolormatrix" => Ok(Self::SvgFeColorMatrix),
1200 "fecomponenttransfer" => Ok(Self::SvgFeComponentTransfer),
1201 "fecomposite" => Ok(Self::SvgFeComposite),
1202 "feconvolvematrix" => Ok(Self::SvgFeConvolveMatrix),
1203 "fediffuselighting" => Ok(Self::SvgFeDiffuseLighting),
1204 "fedisplacementmap" => Ok(Self::SvgFeDisplacementMap),
1205 "fedistantlight" => Ok(Self::SvgFeDistantLight),
1206 "fedropshadow" => Ok(Self::SvgFeDropShadow),
1207 "feflood" => Ok(Self::SvgFeFlood),
1208 "fefuncr" => Ok(Self::SvgFeFuncR),
1209 "fefuncg" => Ok(Self::SvgFeFuncG),
1210 "fefuncb" => Ok(Self::SvgFeFuncB),
1211 "fefunca" => Ok(Self::SvgFeFuncA),
1212 "fegaussianblur" => Ok(Self::SvgFeGaussianBlur),
1213 "feimage" => Ok(Self::SvgFeImage),
1214 "femerge" => Ok(Self::SvgFeMerge),
1215 "femergenode" => Ok(Self::SvgFeMergeNode),
1216 "femorphology" => Ok(Self::SvgFeMorphology),
1217 "feoffset" => Ok(Self::SvgFeOffset),
1218 "fepointlight" => Ok(Self::SvgFePointLight),
1219 "fespecularlighting" => Ok(Self::SvgFeSpecularLighting),
1220 "fespotlight" => Ok(Self::SvgFeSpotLight),
1221 "fetile" => Ok(Self::SvgFeTile),
1222 "feturbulence" => Ok(Self::SvgFeTurbulence),
1223
1224 "image" | "svg:image" => Ok(Self::SvgImage),
1226 "svg:marker" => Ok(Self::SvgMarker),
1227 "foreignobject" => Ok(Self::SvgForeignObject),
1228
1229 "svg:title" => Ok(Self::SvgTitle),
1231 "svg:a" => Ok(Self::SvgA),
1232 "svg:style" => Ok(Self::SvgStyle),
1233 "svg:script" => Ok(Self::SvgScript),
1234 "desc" => Ok(Self::SvgDesc),
1235 "metadata" => Ok(Self::SvgMetadata),
1236 "view" => Ok(Self::SvgView),
1237
1238 "animate" => Ok(Self::SvgAnimate),
1240 "animatemotion" => Ok(Self::SvgAnimateMotion),
1241 "animatetransform" => Ok(Self::SvgAnimateTransform),
1242 "set" => Ok(Self::SvgSet),
1243 "mpath" => Ok(Self::SvgMpath),
1244
1245 "title" => Ok(Self::Title),
1247 "meta" => Ok(Self::Meta),
1248 "link" => Ok(Self::Link),
1249 "script" => Ok(Self::Script),
1250 "style" => Ok(Self::Style),
1251 "base" => Ok(Self::Base),
1252
1253 "img" => Ok(Self::Img),
1255 "virtual-view" | "iframe" => Ok(Self::VirtualView),
1256 "icon" => Ok(Self::Icon),
1257 "geolocation-probe" => Ok(Self::GeolocationProbe),
1258
1259 "before" | "::before" => Ok(Self::Before),
1261 "after" | "::after" => Ok(Self::After),
1262 "marker" | "::marker" => Ok(Self::Marker),
1263 "placeholder" | "::placeholder" => Ok(Self::Placeholder),
1264
1265 other => Err(NodeTypeTagParseError::Invalid(other)),
1266 }
1267 }
1268}
1269
1270impl fmt::Display for NodeTypeTag {
1271 #[allow(clippy::too_many_lines)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1273 match self {
1274 Self::Html => write!(f, "html"),
1276 Self::Head => write!(f, "head"),
1277 Self::Body => write!(f, "body"),
1278
1279 Self::Div => write!(f, "div"),
1281 Self::P => write!(f, "p"),
1282 Self::Article => write!(f, "article"),
1283 Self::Section => write!(f, "section"),
1284 Self::Nav => write!(f, "nav"),
1285 Self::Aside => write!(f, "aside"),
1286 Self::Header => write!(f, "header"),
1287 Self::Footer => write!(f, "footer"),
1288 Self::Main => write!(f, "main"),
1289 Self::Figure => write!(f, "figure"),
1290 Self::FigCaption => write!(f, "figcaption"),
1291
1292 Self::H1 => write!(f, "h1"),
1294 Self::H2 => write!(f, "h2"),
1295 Self::H3 => write!(f, "h3"),
1296 Self::H4 => write!(f, "h4"),
1297 Self::H5 => write!(f, "h5"),
1298 Self::H6 => write!(f, "h6"),
1299
1300 Self::Br => write!(f, "br"),
1302 Self::Hr => write!(f, "hr"),
1303 Self::Pre => write!(f, "pre"),
1304 Self::BlockQuote => write!(f, "blockquote"),
1305 Self::Address => write!(f, "address"),
1306 Self::Details => write!(f, "details"),
1307 Self::Summary => write!(f, "summary"),
1308 Self::Dialog => write!(f, "dialog"),
1309
1310 Self::Ul => write!(f, "ul"),
1312 Self::Ol => write!(f, "ol"),
1313 Self::Li => write!(f, "li"),
1314 Self::Dl => write!(f, "dl"),
1315 Self::Dt => write!(f, "dt"),
1316 Self::Dd => write!(f, "dd"),
1317 Self::Menu => write!(f, "menu"),
1318 Self::MenuItem => write!(f, "menuitem"),
1319 Self::Dir => write!(f, "dir"),
1320
1321 Self::Table => write!(f, "table"),
1323 Self::Caption => write!(f, "caption"),
1324 Self::THead => write!(f, "thead"),
1325 Self::TBody => write!(f, "tbody"),
1326 Self::TFoot => write!(f, "tfoot"),
1327 Self::Tr => write!(f, "tr"),
1328 Self::Th => write!(f, "th"),
1329 Self::Td => write!(f, "td"),
1330 Self::ColGroup => write!(f, "colgroup"),
1331 Self::Col => write!(f, "col"),
1332
1333 Self::Form => write!(f, "form"),
1335 Self::FieldSet => write!(f, "fieldset"),
1336 Self::Legend => write!(f, "legend"),
1337 Self::Label => write!(f, "label"),
1338 Self::Input => write!(f, "input"),
1339 Self::Button => write!(f, "button"),
1340 Self::Select => write!(f, "select"),
1341 Self::OptGroup => write!(f, "optgroup"),
1342 Self::SelectOption => write!(f, "option"),
1343 Self::TextArea => write!(f, "textarea"),
1344 Self::Output => write!(f, "output"),
1345 Self::Progress => write!(f, "progress"),
1346 Self::Meter => write!(f, "meter"),
1347 Self::DataList => write!(f, "datalist"),
1348
1349 Self::Span => write!(f, "span"),
1351 Self::A => write!(f, "a"),
1352 Self::Em => write!(f, "em"),
1353 Self::Strong => write!(f, "strong"),
1354 Self::B => write!(f, "b"),
1355 Self::I => write!(f, "i"),
1356 Self::U => write!(f, "u"),
1357 Self::S => write!(f, "s"),
1358 Self::Mark => write!(f, "mark"),
1359 Self::Del => write!(f, "del"),
1360 Self::Ins => write!(f, "ins"),
1361 Self::Code => write!(f, "code"),
1362 Self::Samp => write!(f, "samp"),
1363 Self::Kbd => write!(f, "kbd"),
1364 Self::Var => write!(f, "var"),
1365 Self::Cite => write!(f, "cite"),
1366 Self::Dfn => write!(f, "dfn"),
1367 Self::Abbr => write!(f, "abbr"),
1368 Self::Acronym => write!(f, "acronym"),
1369 Self::Q => write!(f, "q"),
1370 Self::Time => write!(f, "time"),
1371 Self::Sub => write!(f, "sub"),
1372 Self::Sup => write!(f, "sup"),
1373 Self::Small => write!(f, "small"),
1374 Self::Big => write!(f, "big"),
1375 Self::Bdo => write!(f, "bdo"),
1376 Self::Bdi => write!(f, "bdi"),
1377 Self::Wbr => write!(f, "wbr"),
1378 Self::Ruby => write!(f, "ruby"),
1379 Self::Rt => write!(f, "rt"),
1380 Self::Rtc => write!(f, "rtc"),
1381 Self::Rp => write!(f, "rp"),
1382 Self::Data => write!(f, "data"),
1383
1384 Self::Canvas => write!(f, "canvas"),
1386 Self::Object => write!(f, "object"),
1387 Self::Param => write!(f, "param"),
1388 Self::Embed => write!(f, "embed"),
1389 Self::Audio => write!(f, "audio"),
1390 Self::Video => write!(f, "video"),
1391 Self::Source => write!(f, "source"),
1392 Self::Track => write!(f, "track"),
1393 Self::Map => write!(f, "map"),
1394 Self::Area => write!(f, "area"),
1395 Self::Svg => write!(f, "svg"),
1396 Self::SvgPath => write!(f, "path"),
1397 Self::SvgCircle => write!(f, "circle"),
1398 Self::SvgRect => write!(f, "rect"),
1399 Self::SvgEllipse => write!(f, "ellipse"),
1400 Self::SvgLine => write!(f, "line"),
1401 Self::SvgPolygon => write!(f, "polygon"),
1402 Self::SvgPolyline => write!(f, "polyline"),
1403 Self::SvgG => write!(f, "g"),
1404
1405 Self::SvgDefs => write!(f, "defs"),
1407 Self::SvgSymbol => write!(f, "symbol"),
1408 Self::SvgUse => write!(f, "use"),
1409 Self::SvgSwitch => write!(f, "switch"),
1410
1411 Self::SvgText => write!(f, "svg:text"),
1413 Self::SvgTspan => write!(f, "tspan"),
1414 Self::SvgTextPath => write!(f, "textpath"),
1415
1416 Self::SvgLinearGradient => write!(f, "lineargradient"),
1418 Self::SvgRadialGradient => write!(f, "radialgradient"),
1419 Self::SvgStop => write!(f, "stop"),
1420 Self::SvgPattern => write!(f, "pattern"),
1421
1422 Self::SvgClipPathElement => write!(f, "clippath"),
1424 Self::SvgMask => write!(f, "mask"),
1425
1426 Self::SvgFilter => write!(f, "filter"),
1428 Self::SvgFeBlend => write!(f, "feblend"),
1429 Self::SvgFeColorMatrix => write!(f, "fecolormatrix"),
1430 Self::SvgFeComponentTransfer => write!(f, "fecomponenttransfer"),
1431 Self::SvgFeComposite => write!(f, "fecomposite"),
1432 Self::SvgFeConvolveMatrix => write!(f, "feconvolvematrix"),
1433 Self::SvgFeDiffuseLighting => write!(f, "fediffuselighting"),
1434 Self::SvgFeDisplacementMap => write!(f, "fedisplacementmap"),
1435 Self::SvgFeDistantLight => write!(f, "fedistantlight"),
1436 Self::SvgFeDropShadow => write!(f, "fedropshadow"),
1437 Self::SvgFeFlood => write!(f, "feflood"),
1438 Self::SvgFeFuncR => write!(f, "fefuncr"),
1439 Self::SvgFeFuncG => write!(f, "fefuncg"),
1440 Self::SvgFeFuncB => write!(f, "fefuncb"),
1441 Self::SvgFeFuncA => write!(f, "fefunca"),
1442 Self::SvgFeGaussianBlur => write!(f, "fegaussianblur"),
1443 Self::SvgFeImage => write!(f, "feimage"),
1444 Self::SvgFeMerge => write!(f, "femerge"),
1445 Self::SvgFeMergeNode => write!(f, "femergenode"),
1446 Self::SvgFeMorphology => write!(f, "femorphology"),
1447 Self::SvgFeOffset => write!(f, "feoffset"),
1448 Self::SvgFePointLight => write!(f, "fepointlight"),
1449 Self::SvgFeSpecularLighting => write!(f, "fespecularlighting"),
1450 Self::SvgFeSpotLight => write!(f, "fespotlight"),
1451 Self::SvgFeTile => write!(f, "fetile"),
1452 Self::SvgFeTurbulence => write!(f, "feturbulence"),
1453
1454 Self::SvgMarker => write!(f, "svg:marker"),
1456 Self::SvgImage => write!(f, "svg:image"),
1457 Self::SvgForeignObject => write!(f, "foreignobject"),
1458
1459 Self::SvgTitle => write!(f, "svg:title"),
1461 Self::SvgDesc => write!(f, "desc"),
1462 Self::SvgMetadata => write!(f, "metadata"),
1463 Self::SvgA => write!(f, "svg:a"),
1464 Self::SvgView => write!(f, "view"),
1465 Self::SvgStyle => write!(f, "svg:style"),
1466 Self::SvgScript => write!(f, "svg:script"),
1467
1468 Self::SvgAnimate => write!(f, "animate"),
1470 Self::SvgAnimateMotion => write!(f, "animatemotion"),
1471 Self::SvgAnimateTransform => write!(f, "animatetransform"),
1472 Self::SvgSet => write!(f, "set"),
1473 Self::SvgMpath => write!(f, "mpath"),
1474
1475 Self::Title => write!(f, "title"),
1477 Self::Meta => write!(f, "meta"),
1478 Self::Link => write!(f, "link"),
1479 Self::Script => write!(f, "script"),
1480 Self::Style => write!(f, "style"),
1481 Self::Base => write!(f, "base"),
1482
1483 Self::Text => write!(f, "text"),
1485 Self::Img => write!(f, "img"),
1486 Self::VirtualView => write!(f, "virtual-view"),
1487 Self::Icon => write!(f, "icon"),
1488 Self::GeolocationProbe => write!(f, "geolocation-probe"),
1489
1490 Self::Before => write!(f, "::before"),
1492 Self::After => write!(f, "::after"),
1493 Self::Marker => write!(f, "::marker"),
1494 Self::Placeholder => write!(f, "::placeholder"),
1495 }
1496 }
1497}
1498
1499#[derive(Clone, Hash, Default, PartialEq, Eq, PartialOrd, Ord)]
1511#[repr(C)]
1512pub struct CssPath {
1513 pub selectors: CssPathSelectorVec,
1514}
1515
1516impl_vec!(CssPathSelector, CssPathSelectorVec, CssPathSelectorVecDestructor, CssPathSelectorVecDestructorType, CssPathSelectorVecSlice, OptionCssPathSelector);
1517impl_vec_debug!(CssPathSelector, CssPathSelectorVec);
1518impl_vec_partialord!(CssPathSelector, CssPathSelectorVec);
1519impl_vec_ord!(CssPathSelector, CssPathSelectorVec);
1520impl_vec_clone!(
1521 CssPathSelector,
1522 CssPathSelectorVec,
1523 CssPathSelectorVecDestructor
1524);
1525impl_vec_partialeq!(CssPathSelector, CssPathSelectorVec);
1526impl_vec_eq!(CssPathSelector, CssPathSelectorVec);
1527impl_vec_hash!(CssPathSelector, CssPathSelectorVec);
1528
1529impl CssPath {
1530 #[must_use] pub fn new(selectors: Vec<CssPathSelector>) -> Self {
1531 Self {
1532 selectors: selectors.into(),
1533 }
1534 }
1535
1536 pub fn push_front_scope(&mut self, start: usize, end: usize) {
1550 let is_bare_global = self.selectors.as_ref().len() == 1
1551 && matches!(self.selectors.as_ref().first(), Some(CssPathSelector::Global));
1552 let range = if is_bare_global {
1553 CssScopeRange { start, end: start }
1554 } else {
1555 CssScopeRange { start, end }
1556 };
1557 let mut selectors = Vec::with_capacity(self.selectors.as_ref().len() + 1);
1558 selectors.push(CssPathSelector::Root(range));
1559 selectors.extend(self.selectors.as_ref().iter().cloned());
1560 self.selectors = selectors.into();
1561 }
1562}
1563
1564impl fmt::Display for CssPath {
1565 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1566 for selector in self.selectors.as_ref() {
1567 write!(f, "{selector}")?;
1568 }
1569 Ok(())
1570 }
1571}
1572
1573impl fmt::Debug for CssPath {
1574 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1575 write!(f, "{self}")
1576 }
1577}
1578
1579#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1587#[repr(C)]
1588pub struct CssScopeRange {
1589 pub start: usize,
1591 pub end: usize,
1593}
1594
1595impl CssScopeRange {
1596 #[inline]
1598 #[must_use] pub const fn contains(&self, node: usize) -> bool {
1599 self.start <= node && node <= self.end
1600 }
1601}
1602
1603#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1604#[repr(C, u8)]
1605#[derive(Default)]
1606pub enum CssPathSelector {
1607 #[default]
1609 Global,
1610 Root(CssScopeRange),
1619 Type(NodeTypeTag),
1621 Class(AzString),
1623 Id(AzString),
1625 PseudoSelector(CssPathPseudoSelector),
1627 Attribute(CssAttributeSelector),
1629 DirectChildren,
1631 Children,
1633 AdjacentSibling,
1635 GeneralSibling,
1637}
1638
1639#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1641#[repr(C)]
1642pub struct CssAttributeSelector {
1643 pub name: AzString,
1644 pub op: AttributeMatchOp,
1645 pub value: OptionString,
1646}
1647
1648impl Default for CssAttributeSelector {
1649 fn default() -> Self {
1650 Self {
1651 name: AzString::default(),
1652 op: AttributeMatchOp::Exists,
1653 value: OptionString::None,
1654 }
1655 }
1656}
1657
1658#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1660#[repr(C)]
1661#[derive(Default)]
1662pub enum AttributeMatchOp {
1663 #[default]
1665 Exists,
1666 Eq,
1668 Includes,
1670 DashMatch,
1672 Prefix,
1674 Suffix,
1676 Substring,
1678}
1679
1680
1681impl_option!(
1682 CssPathSelector,
1683 OptionCssPathSelector,
1684 copy = false,
1685 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1686);
1687
1688
1689impl fmt::Display for CssPathSelector {
1690 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1691 use self::CssPathSelector::{Global, Root, Type, Class, Id, PseudoSelector, Attribute, DirectChildren, Children, AdjacentSibling, GeneralSibling};
1692 match &self {
1693 Global => write!(f, "*"),
1694 Root(r) => write!(f, ":root({}..={})", r.start, r.end),
1695 Type(n) => write!(f, "{n}"),
1696 Class(c) => write!(f, ".{c}"),
1697 Id(i) => write!(f, "#{i}"),
1698 PseudoSelector(p) => write!(f, ":{p}"),
1699 Attribute(a) => write!(f, "{a}"),
1700 DirectChildren => write!(f, ">"),
1701 Children => write!(f, " "),
1702 AdjacentSibling => write!(f, "+"),
1703 GeneralSibling => write!(f, "~"),
1704 }
1705 }
1706}
1707
1708impl fmt::Display for CssAttributeSelector {
1709 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1710 match (&self.op, self.value.as_ref()) {
1711 (AttributeMatchOp::Exists, _) => write!(f, "[{}]", self.name),
1712 (op, Some(v)) => write!(f, "[{}{}=\"{}\"]", self.name, op.symbol_prefix(), v),
1713 (op, None) => write!(f, "[{}{}=\"\"]", self.name, op.symbol_prefix()),
1714 }
1715 }
1716}
1717
1718impl AttributeMatchOp {
1719 #[must_use] pub const fn symbol_prefix(&self) -> &'static str {
1722 match self {
1723 Self::Exists | Self::Eq => "",
1724 Self::Includes => "~",
1725 Self::DashMatch => "|",
1726 Self::Prefix => "^",
1727 Self::Suffix => "$",
1728 Self::Substring => "*",
1729 }
1730 }
1731}
1732
1733#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1734#[repr(C, u8)]
1735pub enum CssPathPseudoSelector {
1736 First,
1738 Last,
1740 NthChild(CssNthChildSelector),
1742 Hover,
1744 Active,
1746 Focus,
1748 Lang(AzString),
1750 Backdrop,
1752 Dragging,
1754 DragOver,
1756}
1757
1758#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1760#[repr(C, u8)]
1761pub enum CssNthChildSelector {
1762 Number(u32),
1763 Even,
1764 Odd,
1765 Pattern(CssNthChildPattern),
1766}
1767
1768#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1770#[repr(C)]
1771pub struct CssNthChildPattern {
1772 pub pattern_repeat: u32,
1773 pub offset: u32,
1774}
1775
1776impl fmt::Display for CssNthChildSelector {
1777 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1778 use self::CssNthChildSelector::{Number, Even, Odd, Pattern};
1779 match &self {
1780 Number(u) => write!(f, "{u}"),
1781 Even => write!(f, "even"),
1782 Odd => write!(f, "odd"),
1783 Pattern(p) => write!(f, "{}n + {}", p.pattern_repeat, p.offset),
1784 }
1785 }
1786}
1787
1788impl fmt::Display for CssPathPseudoSelector {
1789 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1790 use self::CssPathPseudoSelector::{First, Last, NthChild, Hover, Active, Focus, Lang, Backdrop, Dragging, DragOver};
1791 match &self {
1792 First => write!(f, "first"),
1793 Last => write!(f, "last"),
1794 NthChild(u) => write!(f, "nth-child({u})"),
1795 Hover => write!(f, "hover"),
1796 Active => write!(f, "active"),
1797 Focus => write!(f, "focus"),
1798 Lang(lang) => write!(f, "lang({})", lang.as_str()),
1799 Backdrop => write!(f, "backdrop"),
1800 Dragging => write!(f, "dragging"),
1801 DragOver => write!(f, "drag-over"),
1802 }
1803 }
1804}
1805
1806impl Css {
1807 #[must_use] pub fn empty() -> Self {
1809 Self::default()
1810 }
1811
1812 pub fn sort_by_specificity(&mut self) {
1817 self.rules.as_mut().sort_by(|a, b| {
1818 a.priority.cmp(&b.priority)
1819 .then_with(|| get_specificity(&a.path).cmp(&get_specificity(&b.path)))
1820 });
1821 }
1822
1823 pub fn rules(&self) -> core::slice::Iter<'_, CssRuleBlock> {
1824 self.rules.as_ref().iter()
1825 }
1826
1827 pub fn iter_inline_properties(
1835 &self,
1836 ) -> impl Iterator<
1837 Item = (
1838 &CssProperty,
1839 &DynamicSelectorVec,
1840 ),
1841 > + '_ {
1842 self.rules.as_ref().iter().flat_map(|r| {
1843 r.declarations.as_ref().iter().filter_map(move |d| match d {
1844 CssDeclaration::Static(p) => Some((p, &r.conditions)),
1845 CssDeclaration::Dynamic(_) => None,
1846 })
1847 })
1848 }
1849}
1850
1851#[cfg(test)]
1852mod root_scope_tests {
1853 use super::*;
1854
1855 #[test]
1856 fn scope_range_contains() {
1857 let r = CssScopeRange { start: 3, end: 7 };
1858 assert!(r.contains(3) && r.contains(5) && r.contains(7));
1859 assert!(!r.contains(2) && !r.contains(8));
1860 let leaf = CssScopeRange { start: 4, end: 4 };
1862 assert!(leaf.contains(4));
1863 assert!(!leaf.contains(3) && !leaf.contains(5));
1864 }
1865
1866 #[test]
1867 fn push_front_scope_compounds_with_wrapper() {
1868 let mut p = CssPath::new(vec![CssPathSelector::Global]);
1871 p.push_front_scope(5, 9);
1872 assert_eq!(
1873 p.selectors.as_ref(),
1874 &[
1875 CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
1876 CssPathSelector::Global
1877 ][..]
1878 );
1879 let subtree = CssScopeRange { start: 5, end: 9 };
1881 let mut p2 = CssPath::new(vec![
1882 CssPathSelector::Global,
1883 CssPathSelector::Children,
1884 CssPathSelector::Class("foo".to_string().into()),
1885 ]);
1886 p2.push_front_scope(5, 9);
1887 assert_eq!(p2.selectors.as_ref()[0], CssPathSelector::Root(subtree));
1888 assert_eq!(p2.selectors.as_ref().len(), 4);
1889 }
1890
1891 #[test]
1892 fn root_display_roundtrips() {
1893 let s = CssPathSelector::Root(CssScopeRange { start: 2, end: 6 });
1894 assert_eq!(format!("{s}"), ":root(2..=6)");
1895 }
1896
1897 #[test]
1898 fn parse_inline_keeps_layout_and_style_decls() {
1899 let css = Css::parse_inline("width: 200px; height: 100px; background: red");
1902 let mut types = Vec::new();
1903 for r in css.rules.as_ref() {
1904 for d in r.declarations.as_ref() {
1905 if let CssDeclaration::Static(p) = d {
1906 types.push(alloc::format!("{:?}", p.get_type()));
1907 }
1908 }
1909 }
1910 println!("INLINE PROP TYPES: {types:?}");
1911 assert!(
1912 types.iter().any(|t| t.contains("width")),
1913 "width must survive parse_inline as a Static decl; got {types:?}"
1914 );
1915 assert!(
1916 types.iter().any(|t| t.contains("height")),
1917 "height must survive parse_inline; got {types:?}"
1918 );
1919 }
1920}
1921
1922#[cfg(test)]
1923mod priority_sort_tests {
1924 use super::*;
1925 use crate::css::rule_priority;
1926
1927 fn rule_with(priority: u8, selectors: Vec<CssPathSelector>) -> CssRuleBlock {
1928 CssRuleBlock {
1929 path: CssPath { selectors: selectors.into() },
1930 declarations: Vec::new().into(),
1931 conditions: DynamicSelectorVec::from_const_slice(&[]),
1932 priority,
1933 }
1934 }
1935
1936 #[test]
1939 fn sort_by_priority_then_specificity() {
1940 let mut css = Css::new(vec![
1941 rule_with(rule_priority::AUTHOR, vec![CssPathSelector::Global]),
1943 rule_with(rule_priority::UA, vec![
1945 CssPathSelector::Id("ua-id".to_string().into()),
1946 CssPathSelector::Class("ua-class".to_string().into()),
1947 ]),
1948 rule_with(rule_priority::AUTHOR, vec![
1950 CssPathSelector::Id("a-id".to_string().into()),
1951 ]),
1952 rule_with(rule_priority::SYSTEM, vec![CssPathSelector::Global]),
1954 ]);
1955 css.sort_by_specificity();
1956 let priorities: Vec<u8> = css.rules.as_ref().iter().map(|r| r.priority).collect();
1957 assert_eq!(
1958 priorities,
1959 vec![rule_priority::UA, rule_priority::SYSTEM, rule_priority::AUTHOR, rule_priority::AUTHOR],
1960 "rules must sort by layer first; specificity only breaks ties within a layer"
1961 );
1962 let last_two_specificity: Vec<_> = css.rules.as_ref().iter()
1964 .filter(|r| r.priority == rule_priority::AUTHOR)
1965 .map(|r| get_specificity(&r.path))
1966 .collect();
1967 assert!(last_two_specificity[0] < last_two_specificity[1]);
1968 }
1969}
1970
1971#[must_use] pub fn get_specificity(path: &CssPath) -> (usize, usize, usize, usize) {
1974 let id_count = path
1975 .selectors
1976 .iter()
1977 .filter(|x| matches!(x, CssPathSelector::Id(_)))
1978 .count();
1979 let class_count = path
1980 .selectors
1981 .iter()
1982 .filter(|x| {
1983 matches!(
1984 x,
1985 CssPathSelector::Class(_)
1986 | CssPathSelector::PseudoSelector(_)
1987 | CssPathSelector::Attribute(_)
1988 )
1989 })
1990 .count();
1991 let div_count = path
1992 .selectors
1993 .iter()
1994 .filter(|x| matches!(x, CssPathSelector::Type(_)))
1995 .count();
1996 (id_count, class_count, div_count, path.selectors.len())
1997}
1998
1999#[cfg(test)]
2000#[allow(clippy::pedantic, clippy::nursery)]
2001mod autotest_generated {
2002 use core::hash::{Hash, Hasher};
2003 use std::collections::hash_map::DefaultHasher;
2004
2005 use super::*;
2006 use crate::{
2007 dynamic_selector::DynamicSelector,
2008 props::{
2009 basic::{color::ColorU, pixel::PixelValue},
2010 layout::dimensions::LayoutWidth,
2011 style::text::StyleTextColor,
2012 },
2013 };
2014
2015 fn prop_width(px: f32) -> CssProperty {
2021 CssProperty::width(LayoutWidth::Px(PixelValue::px(px)))
2022 }
2023
2024 fn prop_text_color(r: u8) -> CssProperty {
2026 CssProperty::const_text_color(StyleTextColor {
2027 inner: ColorU::new(r, 0, 0, 255),
2028 })
2029 }
2030
2031 fn dyn_prop(id: &str, default_value: CssProperty) -> DynamicCssProperty {
2032 DynamicCssProperty {
2033 dynamic_id: id.to_string().into(),
2034 default_value,
2035 }
2036 }
2037
2038 fn rule_at(priority: u8, selectors: Vec<CssPathSelector>) -> CssRuleBlock {
2039 let mut r = CssRuleBlock::new(
2040 CssPath::new(selectors),
2041 vec![CssDeclaration::Static(prop_width(1.0))],
2042 );
2043 r.priority = priority;
2044 r
2045 }
2046
2047 fn hash_of<T: Hash>(t: &T) -> u64 {
2048 let mut h = DefaultHasher::new();
2049 t.hash(&mut h);
2050 h.finish()
2051 }
2052
2053 #[derive(Debug, Copy, Clone, PartialEq, Default)]
2056 struct TestVal(f32);
2057
2058 impl PrintAsCssValue for TestVal {
2059 fn print_as_css_value(&self) -> String {
2060 format!("{}", self.0)
2061 }
2062 }
2063
2064 impl fmt::Display for TestVal {
2065 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2066 write!(f, "{}", self.0)
2067 }
2068 }
2069
2070 fn keyword_values() -> Vec<CssPropertyValue<TestVal>> {
2072 vec![
2073 CssPropertyValue::Auto,
2074 CssPropertyValue::None,
2075 CssPropertyValue::Initial,
2076 CssPropertyValue::Inherit,
2077 CssPropertyValue::Revert,
2078 CssPropertyValue::Unset,
2079 ]
2080 }
2081
2082 #[test]
2087 fn css_empty_is_the_neutral_element() {
2088 let e = Css::empty();
2089 assert!(e.is_empty());
2090 assert_eq!(e.rules().count(), 0);
2091 assert_eq!(e.iter_inline_properties().count(), 0);
2092 assert_eq!(e, Css::default());
2094 assert_eq!(e, Css::new(Vec::new()));
2095 assert_eq!(e, Css::from(Vec::<CssRuleBlock>::new()));
2096 }
2097
2098 #[test]
2099 fn css_new_preserves_length_and_order() {
2100 let rules = vec![
2101 rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
2102 rule_at(
2103 rule_priority::AUTHOR,
2104 vec![CssPathSelector::Id("a".to_string().into())],
2105 ),
2106 rule_at(rule_priority::INLINE, vec![CssPathSelector::Children]),
2107 ];
2108 let css = Css::new(rules.clone());
2109 assert!(!css.is_empty());
2110 assert_eq!(css.rules().count(), 3);
2111 assert_eq!(css.rules.as_ref(), &rules[..]);
2112 assert_eq!(css.rules().count(), css.rules.as_ref().len());
2114 }
2115
2116 #[test]
2117 fn css_new_with_many_rules_does_not_panic() {
2118 let rules: Vec<CssRuleBlock> = (0..10_000)
2119 .map(|i| rule_at((i % 256) as u8, vec![CssPathSelector::Global]))
2120 .collect();
2121 let css = Css::new(rules);
2122 assert_eq!(css.rules.as_ref().len(), 10_000);
2123 assert!(!css.is_empty());
2124 assert_eq!(css.iter_inline_properties().count(), 10_000);
2126 }
2127
2128 #[test]
2129 fn css_sort_by_specificity_on_empty_and_singleton_is_a_noop() {
2130 let mut empty = Css::empty();
2131 empty.sort_by_specificity();
2132 assert!(empty.is_empty());
2133
2134 let one = rule_at(rule_priority::AUTHOR, vec![CssPathSelector::Global]);
2135 let mut css = Css::new(vec![one.clone()]);
2136 css.sort_by_specificity();
2137 assert_eq!(css.rules.as_ref(), &[one][..]);
2138 }
2139
2140 #[test]
2141 fn css_sort_by_specificity_is_idempotent() {
2142 let mut css = Css::new(vec![
2143 rule_at(rule_priority::RUNTIME, vec![CssPathSelector::Global]),
2144 rule_at(
2145 rule_priority::UA,
2146 vec![
2147 CssPathSelector::Id("x".to_string().into()),
2148 CssPathSelector::Class("y".to_string().into()),
2149 ],
2150 ),
2151 rule_at(rule_priority::INLINE, vec![CssPathSelector::Global]),
2152 rule_at(
2153 rule_priority::AUTHOR,
2154 vec![CssPathSelector::Type(NodeTypeTag::Div)],
2155 ),
2156 rule_at(rule_priority::SYSTEM, vec![CssPathSelector::Global]),
2157 ]);
2158 css.sort_by_specificity();
2159 let once = css.clone();
2160 css.sort_by_specificity();
2161 assert_eq!(css, once, "sort_by_specificity must be idempotent");
2162
2163 let priorities: Vec<u8> = css.rules().map(|r| r.priority).collect();
2165 assert_eq!(
2166 priorities,
2167 vec![
2168 rule_priority::UA,
2169 rule_priority::SYSTEM,
2170 rule_priority::AUTHOR,
2171 rule_priority::INLINE,
2172 rule_priority::RUNTIME,
2173 ]
2174 );
2175 }
2176
2177 #[test]
2178 fn css_sort_by_specificity_keeps_equal_keys_in_source_order() {
2179 let a = CssRuleBlock::new(
2181 CssPath::new(vec![CssPathSelector::Global]),
2182 vec![CssDeclaration::Static(prop_width(1.0))],
2183 );
2184 let b = CssRuleBlock::new(
2185 CssPath::new(vec![CssPathSelector::Global]),
2186 vec![CssDeclaration::Static(prop_width(2.0))],
2187 );
2188 let mut css = Css::new(vec![a.clone(), b.clone()]);
2189 css.sort_by_specificity();
2190 assert_eq!(
2191 css.rules.as_ref(),
2192 &[a, b][..],
2193 "ties must keep source order (last-wins cascade depends on it)"
2194 );
2195 }
2196
2197 #[test]
2198 fn css_iter_inline_properties_skips_dynamic_declarations() {
2199 let css = Css::new(vec![CssRuleBlock::with_conditions(
2200 CssPath::new(vec![CssPathSelector::Global]),
2201 vec![
2202 CssDeclaration::Static(prop_width(10.0)),
2203 CssDeclaration::Dynamic(dyn_prop("d", prop_text_color(1))),
2204 CssDeclaration::Static(prop_text_color(2)),
2205 ],
2206 vec![DynamicSelector::ContainerName("c".to_string().into())],
2207 )]);
2208
2209 let collected: Vec<_> = css.iter_inline_properties().collect();
2210 assert_eq!(collected.len(), 2, "Dynamic declarations must be skipped");
2211 assert_eq!(collected[0].0.get_type(), CssPropertyType::Width);
2212 assert_eq!(collected[1].0.get_type(), CssPropertyType::TextColor);
2213 for (_, conds) in &collected {
2215 assert_eq!(conds.as_ref().len(), 1);
2216 }
2217 }
2218
2219 #[test]
2220 fn css_iter_inline_properties_on_rule_without_declarations() {
2221 let css = Css::new(vec![CssRuleBlock::new(
2222 CssPath::new(vec![CssPathSelector::Global]),
2223 Vec::new(),
2224 )]);
2225 assert!(!css.is_empty(), "a rule with 0 declarations is still a rule");
2226 assert_eq!(css.iter_inline_properties().count(), 0);
2227 }
2228
2229 #[test]
2230 fn css_ord_is_length_based_by_design() {
2231 let a = Css::new(vec![rule_at(
2236 rule_priority::UA,
2237 vec![CssPathSelector::Global],
2238 )]);
2239 let b = Css::new(vec![rule_at(
2240 rule_priority::RUNTIME,
2241 vec![CssPathSelector::Type(NodeTypeTag::Div)],
2242 )]);
2243 assert_ne!(a, b);
2244 assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
2245 assert_eq!(a.partial_cmp(&b), Some(core::cmp::Ordering::Equal));
2246 let longer = Css::new(vec![
2248 rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
2249 rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
2250 ]);
2251 assert_eq!(a.cmp(&longer), core::cmp::Ordering::Less);
2252 assert_eq!(Css::empty().cmp(&a), core::cmp::Ordering::Less);
2253 }
2254
2255 #[test]
2260 fn css_declaration_new_static_matches_the_wrapped_property() {
2261 let p = prop_width(42.0);
2262 let d = CssDeclaration::new_static(p.clone());
2263 assert_eq!(d, CssDeclaration::Static(p.clone()));
2264 assert_eq!(d.get_type(), p.get_type());
2265 assert_eq!(d.get_type(), CssPropertyType::Width);
2266 }
2267
2268 #[test]
2269 fn css_declaration_new_dynamic_takes_its_type_from_the_default_value() {
2270 let dp = dyn_prop("my_id", prop_text_color(7));
2271 let d = CssDeclaration::new_dynamic(dp.clone());
2272 assert_eq!(d, CssDeclaration::Dynamic(dp));
2273 assert_eq!(
2274 d.get_type(),
2275 CssPropertyType::TextColor,
2276 "a Dynamic declaration's type is its default value's type"
2277 );
2278 }
2279
2280 #[test]
2281 fn css_declaration_is_inheritable_matrix() {
2282 assert!(CssDeclaration::new_static(prop_text_color(1)).is_inheritable());
2284 assert!(!CssDeclaration::new_static(prop_width(1.0)).is_inheritable());
2285 assert!(
2288 !CssDeclaration::new_dynamic(dyn_prop("c", prop_text_color(1))).is_inheritable(),
2289 "Dynamic declarations must never inherit, even wrapping an inheritable prop"
2290 );
2291 assert!(!CssDeclaration::new_dynamic(dyn_prop("w", prop_width(1.0))).is_inheritable());
2292 }
2293
2294 #[test]
2295 fn css_declaration_can_trigger_relayout_matrix() {
2296 assert!(CssDeclaration::new_static(prop_width(1.0)).can_trigger_relayout());
2297 assert!(!CssDeclaration::new_static(prop_text_color(1)).can_trigger_relayout());
2298 assert!(CssDeclaration::new_dynamic(dyn_prop("w", prop_width(1.0))).can_trigger_relayout());
2300 assert!(
2301 !CssDeclaration::new_dynamic(dyn_prop("c", prop_text_color(1))).can_trigger_relayout()
2302 );
2303 }
2304
2305 #[test]
2306 fn css_declaration_to_str_static_is_non_empty_for_edge_floats() {
2307 for px in [
2308 0.0_f32,
2309 -0.0,
2310 f32::MIN,
2311 f32::MAX,
2312 f32::NAN,
2313 f32::INFINITY,
2314 f32::NEG_INFINITY,
2315 f32::EPSILON,
2316 ] {
2317 let s = CssDeclaration::new_static(prop_width(px)).to_str();
2318 assert!(
2319 !s.is_empty(),
2320 "to_str() must render something for width: {px:?}"
2321 );
2322 }
2323 }
2324
2325 #[test]
2326 fn css_declaration_to_str_dynamic_renders_var_syntax() {
2327 let s = CssDeclaration::new_dynamic(dyn_prop("my_id", prop_width(5.0))).to_str();
2328 assert!(
2329 s.starts_with("var(--my_id, "),
2330 "dynamic to_str must render CSS var() syntax, got {s:?}"
2331 );
2332 assert!(s.ends_with(')'));
2333 }
2334
2335 #[test]
2336 fn css_declaration_to_str_dynamic_with_hostile_ids_does_not_panic() {
2337 let long = "x".repeat(100_000);
2338 for id in [
2339 "",
2340 " ",
2341 "😀",
2342 "a\u{0301}\u{0301}",
2343 "--)",
2344 "\u{0}",
2345 "a\nb",
2346 long.as_str(),
2347 ] {
2348 let s = CssDeclaration::new_dynamic(dyn_prop(id, prop_width(1.0))).to_str();
2349 assert!(s.starts_with("var(--"));
2350 }
2351 }
2352
2353 #[test]
2358 fn dynamic_css_property_is_never_inheritable() {
2359 for p in [
2360 prop_text_color(0),
2361 prop_width(0.0),
2362 prop_width(f32::NAN),
2363 CssProperty::const_none(CssPropertyType::FontSize),
2364 CssProperty::const_inherit(CssPropertyType::TextColor),
2365 ] {
2366 assert!(
2367 !dyn_prop("id", p).is_inheritable(),
2368 "DynamicCssProperty::is_inheritable is unconditionally false"
2369 );
2370 }
2371 }
2372
2373 #[test]
2374 fn dynamic_css_property_relayout_follows_the_default_value_type() {
2375 assert!(dyn_prop("a", prop_width(1.0)).can_trigger_relayout());
2376 assert!(!dyn_prop("a", prop_text_color(1)).can_trigger_relayout());
2377 assert!(dyn_prop("a", CssProperty::const_auto(CssPropertyType::Width)).can_trigger_relayout());
2379 assert!(
2380 !dyn_prop("a", CssProperty::const_none(CssPropertyType::TextColor))
2381 .can_trigger_relayout()
2382 );
2383 }
2384
2385 static STATIC_U32: u32 = 0xDEAD_BEEF;
2390
2391 #[test]
2392 fn box_or_static_heap_round_trips_through_as_ref_and_deref() {
2393 let b = BoxOrStatic::heap(123_u32);
2394 assert_eq!(*b.as_ref(), 123);
2395 assert_eq!(*b, 123, "Deref must agree with as_ref");
2396 assert_eq!(*BoxOrStatic::heap(u32::MAX).as_ref(), u32::MAX);
2397 assert_eq!(*BoxOrStatic::heap(0_u32).as_ref(), 0);
2398 assert_eq!(*BoxOrStatic::heap(()).as_ref(), ());
2400 let big = BoxOrStatic::heap(vec![0_u8; 1_000_000]);
2401 assert_eq!(big.as_ref().len(), 1_000_000);
2402 }
2403
2404 #[test]
2405 fn box_or_static_static_variant_reads_through_as_ref() {
2406 let b: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2407 assert_eq!(*b.as_ref(), 0xDEAD_BEEF);
2408 assert_eq!(*b, 0xDEAD_BEEF);
2409 }
2410
2411 #[test]
2412 fn box_or_static_as_mut_mutates_the_boxed_value() {
2413 let mut b = BoxOrStatic::heap(1_u32);
2414 *b.as_mut() = 9;
2415 assert_eq!(*b.as_ref(), 9);
2416 }
2417
2418 #[test]
2419 #[should_panic(expected = "Cannot mutate a static BoxOrStatic value")]
2420 fn box_or_static_as_mut_on_static_panics_as_documented() {
2421 let mut b: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2422 let _ = b.as_mut();
2423 }
2424
2425 #[test]
2426 fn box_or_static_clone_is_deep_for_boxed() {
2427 let a = BoxOrStatic::heap(5_u32);
2428 let mut b = a.clone();
2429 *b.as_mut() = 6;
2430 assert_eq!(*a.as_ref(), 5, "cloning a Boxed value must not alias it");
2431 assert_eq!(*b.as_ref(), 6);
2432 assert_ne!(a, b);
2433 }
2434
2435 #[test]
2436 fn box_or_static_eq_ord_hash_all_delegate_to_the_inner_value() {
2437 let heap = BoxOrStatic::heap(0xDEAD_BEEF_u32);
2438 let stat: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2439 assert_eq!(heap, stat);
2441 assert_eq!(hash_of(&heap), hash_of(&stat));
2442 assert_eq!(heap.cmp(&stat), core::cmp::Ordering::Equal);
2443 assert_eq!(heap.partial_cmp(&stat), Some(core::cmp::Ordering::Equal));
2444
2445 let smaller = BoxOrStatic::heap(1_u32);
2446 assert!(smaller < heap);
2447 }
2448
2449 #[test]
2450 fn box_or_static_debug_and_display_render_the_inner_value() {
2451 let b = BoxOrStatic::heap(42_u32);
2452 assert_eq!(format!("{b:?}"), "42");
2453 assert_eq!(format!("{b}"), "42");
2454 let s: BoxOrStaticString = BoxOrStatic::heap(String::new().into());
2455 assert!(
2456 !format!("{s:?}").is_empty(),
2457 "Debug of an empty string payload is still well-formed"
2458 );
2459 for f in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, f64::MIN] {
2461 let bf = BoxOrStatic::heap(f);
2462 assert!(!format!("{bf:?}").is_empty());
2463 assert!(!format!("{bf}").is_empty());
2464 }
2465 }
2466
2467 #[test]
2468 fn box_or_static_default_is_a_heap_allocated_default() {
2469 let b: BoxOrStatic<u32> = BoxOrStatic::default();
2470 assert_eq!(*b.as_ref(), 0);
2471 assert!(matches!(b, BoxOrStatic::Boxed(_)));
2472 let s: BoxOrStaticString = BoxOrStatic::default();
2473 assert_eq!(s.as_ref().as_str(), "");
2474 }
2475
2476 #[test]
2477 fn box_or_static_into_inner_returns_the_payload() {
2478 assert_eq!(BoxOrStatic::heap(7_u32).into_inner(), 7);
2479 let stat: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2480 assert_eq!(stat.into_inner(), 0xDEAD_BEEF);
2481 let s: BoxOrStaticString = BoxOrStatic::heap("hello".to_string().into());
2482 assert_eq!(s.into_inner().as_str(), "hello");
2483 }
2484
2485 #[test]
2486 fn box_or_static_into_inner_must_not_leak_the_box() {
2487 use core::sync::atomic::{AtomicIsize, Ordering};
2488
2489 static LIVE: AtomicIsize = AtomicIsize::new(0);
2492
2493 struct Tracked(u32);
2494 impl Tracked {
2495 fn new(v: u32) -> Self {
2496 LIVE.fetch_add(1, Ordering::SeqCst);
2497 Self(v)
2498 }
2499 }
2500 impl Clone for Tracked {
2501 fn clone(&self) -> Self {
2502 LIVE.fetch_add(1, Ordering::SeqCst);
2503 Self(self.0)
2504 }
2505 }
2506 impl Drop for Tracked {
2507 fn drop(&mut self) {
2508 LIVE.fetch_sub(1, Ordering::SeqCst);
2509 }
2510 }
2511
2512 let boxed = BoxOrStatic::heap(Tracked::new(7));
2513 assert_eq!(LIVE.load(Ordering::SeqCst), 1);
2514
2515 let inner = boxed.into_inner();
2516 assert_eq!(inner.0, 7);
2517 drop(inner);
2518
2519 assert_eq!(
2520 LIVE.load(Ordering::SeqCst),
2521 0,
2522 "into_inner() on a Boxed variant leaks: it clones the payload and then \
2523 mem::forget(self), so `Drop for BoxOrStatic` never runs and the Box \
2524 (plus the T inside it) is never freed"
2525 );
2526 }
2527
2528 #[test]
2529 #[cfg(target_pointer_width = "64")]
2530 fn box_or_static_is_the_documented_16_bytes() {
2531 assert_eq!(core::mem::size_of::<BoxOrStatic<u32>>(), 16);
2532 assert_eq!(core::mem::size_of::<BoxOrStaticString>(), 16);
2533 }
2534
2535 #[test]
2540 fn css_property_value_predicates_are_mutually_exclusive() {
2541 for v in keyword_values() {
2542 let flags = [
2543 v.is_auto(),
2544 v.is_none(),
2545 v.is_initial(),
2546 v.is_inherit(),
2547 v.is_revert(),
2548 v.is_unset(),
2549 ];
2550 assert_eq!(
2551 flags.iter().filter(|b| **b).count(),
2552 1,
2553 "exactly one predicate must fire for {v:?}"
2554 );
2555 assert!(
2556 v.get_property().is_none(),
2557 "a keyword variant has no property"
2558 );
2559 }
2560
2561 let exact = CssPropertyValue::Exact(TestVal(1.0));
2562 assert!(
2563 !exact.is_auto()
2564 && !exact.is_none()
2565 && !exact.is_initial()
2566 && !exact.is_inherit()
2567 && !exact.is_revert()
2568 && !exact.is_unset(),
2569 "Exact must answer false to every keyword predicate"
2570 );
2571 }
2572
2573 #[test]
2574 fn css_property_value_predicates_pick_the_right_variant() {
2575 assert!(CssPropertyValue::<TestVal>::Auto.is_auto());
2576 assert!(CssPropertyValue::<TestVal>::None.is_none());
2577 assert!(CssPropertyValue::<TestVal>::Initial.is_initial());
2578 assert!(CssPropertyValue::<TestVal>::Inherit.is_inherit());
2579 assert!(CssPropertyValue::<TestVal>::Revert.is_revert());
2580 assert!(CssPropertyValue::<TestVal>::Unset.is_unset());
2581 }
2582
2583 #[test]
2584 fn css_property_value_get_property_and_get_property_owned_agree() {
2585 let exact = CssPropertyValue::Exact(TestVal(3.5));
2586 assert_eq!(exact.get_property(), Some(&TestVal(3.5)));
2587 assert_eq!(exact.get_property_owned(), Some(TestVal(3.5)));
2588 for v in keyword_values() {
2589 assert_eq!(v.get_property(), None);
2590 assert_eq!(v.get_property_owned(), None);
2591 }
2592 }
2593
2594 #[test]
2595 fn css_property_value_get_property_or_default_substitutes_only_auto_and_initial() {
2596 assert_eq!(
2599 CssPropertyValue::<TestVal>::Auto.get_property_or_default(),
2600 Some(TestVal::default())
2601 );
2602 assert_eq!(
2603 CssPropertyValue::<TestVal>::Initial.get_property_or_default(),
2604 Some(TestVal::default())
2605 );
2606 assert_eq!(CssPropertyValue::<TestVal>::None.get_property_or_default(), None);
2607 assert_eq!(
2608 CssPropertyValue::<TestVal>::Inherit.get_property_or_default(),
2609 None
2610 );
2611 assert_eq!(
2612 CssPropertyValue::<TestVal>::Revert.get_property_or_default(),
2613 None
2614 );
2615 assert_eq!(CssPropertyValue::<TestVal>::Unset.get_property_or_default(), None);
2616 assert_eq!(
2617 CssPropertyValue::Exact(TestVal(9.0)).get_property_or_default(),
2618 Some(TestVal(9.0))
2619 );
2620 }
2621
2622 #[test]
2623 fn css_property_value_default_is_exact_default() {
2624 assert_eq!(
2625 CssPropertyValue::<TestVal>::default(),
2626 CssPropertyValue::Exact(TestVal::default())
2627 );
2628 assert!(!CssPropertyValue::<TestVal>::default().is_auto());
2629 }
2630
2631 #[test]
2632 fn css_property_value_from_wraps_into_exact() {
2633 let v: CssPropertyValue<TestVal> = TestVal(2.0).into();
2634 assert_eq!(v, CssPropertyValue::Exact(TestVal(2.0)));
2635 }
2636
2637 #[test]
2638 fn css_property_value_keyword_serialization_is_the_css_keyword() {
2639 let cases: [(CssPropertyValue<TestVal>, &str); 6] = [
2640 (CssPropertyValue::Auto, "auto"),
2641 (CssPropertyValue::None, "none"),
2642 (CssPropertyValue::Initial, "initial"),
2643 (CssPropertyValue::Inherit, "inherit"),
2644 (CssPropertyValue::Revert, "revert"),
2645 (CssPropertyValue::Unset, "unset"),
2646 ];
2647 for (v, expected) in cases {
2648 assert_eq!(v.get_css_value_fmt(), expected);
2649 assert_eq!(
2650 format!("{v}"),
2651 expected,
2652 "Display and get_css_value_fmt must not diverge for keywords"
2653 );
2654 }
2655 let exact = CssPropertyValue::Exact(TestVal(1.5));
2656 assert_eq!(exact.get_css_value_fmt(), "1.5");
2657 assert_eq!(format!("{exact}"), "1.5");
2658 }
2659
2660 #[test]
2661 fn css_property_value_serializes_hostile_floats_without_panicking() {
2662 for f in [
2663 f32::NAN,
2664 f32::INFINITY,
2665 f32::NEG_INFINITY,
2666 f32::MIN,
2667 f32::MAX,
2668 -0.0,
2669 f32::MIN_POSITIVE,
2670 ] {
2671 let v = CssPropertyValue::Exact(TestVal(f));
2672 assert!(!v.get_css_value_fmt().is_empty());
2673 assert!(!format!("{v}").is_empty());
2674 }
2675 }
2676
2677 #[test]
2678 fn css_property_value_map_property_preserves_keyword_variants() {
2679 for v in keyword_values() {
2681 let before = format!("{v}");
2682 let mapped: CssPropertyValue<u32> =
2683 v.map_property(|_| panic!("map_fn must not run on a keyword variant"));
2684 assert_eq!(format!("{mapped}"), before, "the keyword must survive the map");
2685 }
2686 let mapped = CssPropertyValue::Exact(TestVal(2.0)).map_property(|t| t.0 as u32);
2688 assert_eq!(mapped, CssPropertyValue::Exact(2_u32));
2689 }
2690
2691 #[test]
2692 fn css_property_value_map_property_handles_nan_and_type_changing_maps() {
2693 let mapped = CssPropertyValue::Exact(TestVal(f32::NAN)).map_property(|t| t.0.is_nan());
2694 assert_eq!(mapped, CssPropertyValue::Exact(true));
2695 let mapped = CssPropertyValue::Exact(TestVal(f32::INFINITY)).map_property(|t| t.0 as i64);
2697 assert_eq!(mapped, CssPropertyValue::Exact(i64::MAX));
2698 }
2699
2700 #[test]
2705 fn css_rule_block_new_defaults_to_author_priority_and_no_conditions() {
2706 let decls = vec![
2707 CssDeclaration::Static(prop_width(1.0)),
2708 CssDeclaration::Static(prop_text_color(2)),
2709 ];
2710 let r = CssRuleBlock::new(
2711 CssPath::new(vec![CssPathSelector::Type(NodeTypeTag::Div)]),
2712 decls.clone(),
2713 );
2714 assert_eq!(r.priority, rule_priority::AUTHOR);
2715 assert!(r.conditions.as_ref().is_empty());
2716 assert_eq!(r.declarations.as_ref(), &decls[..]);
2717 assert_eq!(r.path.selectors.as_ref().len(), 1);
2718 }
2719
2720 #[test]
2721 fn css_rule_block_with_conditions_keeps_every_condition() {
2722 let conds: Vec<DynamicSelector> = (0..1_000)
2723 .map(|i| DynamicSelector::ContainerName(format!("c{i}").into()))
2724 .collect();
2725 let r = CssRuleBlock::with_conditions(
2726 CssPath::default(),
2727 Vec::new(),
2728 conds.clone(),
2729 );
2730 assert_eq!(r.conditions.as_ref().len(), 1_000);
2731 assert_eq!(r.conditions.as_ref(), &conds[..]);
2732 assert_eq!(
2733 r.priority,
2734 rule_priority::AUTHOR,
2735 "with_conditions must not change the layer"
2736 );
2737 assert!(r.declarations.as_ref().is_empty());
2738 assert!(r.path.selectors.as_ref().is_empty());
2739 }
2740
2741 #[test]
2742 fn css_rule_block_default_is_empty_and_ua_priority() {
2743 let r = CssRuleBlock::default();
2744 assert!(r.path.selectors.as_ref().is_empty());
2745 assert!(r.declarations.as_ref().is_empty());
2746 assert!(r.conditions.as_ref().is_empty());
2747 assert_eq!(r.priority, rule_priority::UA, "u8::default() == 0 == UA");
2748 }
2749
2750 #[test]
2751 fn rule_priority_slots_are_strictly_ordered() {
2752 assert!(rule_priority::UA < rule_priority::SYSTEM);
2753 assert!(rule_priority::SYSTEM < rule_priority::AUTHOR);
2754 assert!(rule_priority::AUTHOR < rule_priority::INLINE);
2755 assert!(rule_priority::INLINE < rule_priority::RUNTIME);
2756 }
2757
2758 const ALL_TAGS: &[NodeTypeTag] = {
2763 use NodeTypeTag::*;
2764 &[
2765 Html, Head, Body, Div, P, Article, Section, Nav, Aside, Header, Footer, Main, Figure,
2766 FigCaption, H1, H2, H3, H4, H5, H6, Br, Hr, Pre, BlockQuote, Address, Details, Summary,
2767 Dialog, Ul, Ol, Li, Dl, Dt, Dd, Menu, MenuItem, Dir, Table, Caption, THead, TBody,
2768 TFoot, Tr, Th, Td, ColGroup, Col, Form, FieldSet, Legend, Label, Input, Button, Select,
2769 OptGroup, SelectOption, TextArea, Output, Progress, Meter, DataList, Span, A, Em,
2770 Strong, B, I, U, S, Mark, Del, Ins, Code, Samp, Kbd, Var, Cite, Dfn, Abbr, Acronym, Q,
2771 Time, Sub, Sup, Small, Big, Bdo, Bdi, Wbr, Ruby, Rt, Rtc, Rp, Data, Canvas, Object,
2772 Param, Embed, Audio, Video, Source, Track, Map, Area, Svg, SvgPath, SvgCircle, SvgRect,
2773 SvgEllipse, SvgLine, SvgPolygon, SvgPolyline, SvgG, SvgDefs, SvgSymbol, SvgUse,
2774 SvgSwitch, SvgText, SvgTspan, SvgTextPath, SvgLinearGradient, SvgRadialGradient,
2775 SvgStop, SvgPattern, SvgClipPathElement, SvgMask, SvgFilter, SvgFeBlend,
2776 SvgFeColorMatrix, SvgFeComponentTransfer, SvgFeComposite, SvgFeConvolveMatrix,
2777 SvgFeDiffuseLighting, SvgFeDisplacementMap, SvgFeDistantLight, SvgFeDropShadow,
2778 SvgFeFlood, SvgFeFuncR, SvgFeFuncG, SvgFeFuncB, SvgFeFuncA, SvgFeGaussianBlur,
2779 SvgFeImage, SvgFeMerge, SvgFeMergeNode, SvgFeMorphology, SvgFeOffset, SvgFePointLight,
2780 SvgFeSpecularLighting, SvgFeSpotLight, SvgFeTile, SvgFeTurbulence, SvgMarker, SvgImage,
2781 SvgForeignObject, SvgTitle, SvgDesc, SvgMetadata, SvgA, SvgView, SvgStyle, SvgScript,
2782 SvgAnimate, SvgAnimateMotion, SvgAnimateTransform, SvgSet, SvgMpath, Title, Meta, Link,
2783 Script, Style, Base, Text, Img, VirtualView, Icon, GeolocationProbe, Before, After,
2784 Marker, Placeholder,
2785 ]
2786 };
2787
2788 #[test]
2789 fn node_type_tag_variant_list_is_complete_and_unique() {
2790 assert_eq!(
2793 ALL_TAGS.len(),
2794 182,
2795 "ALL_TAGS is out of sync with the NodeTypeTag enum"
2796 );
2797 let mut seen: Vec<NodeTypeTag> = Vec::new();
2798 for t in ALL_TAGS {
2799 assert!(!seen.contains(t), "duplicate entry in ALL_TAGS: {t:?}");
2800 seen.push(*t);
2801 }
2802 }
2803
2804 #[test]
2805 fn node_type_tag_display_names_are_all_distinct() {
2806 let mut names: Vec<String> = ALL_TAGS.iter().map(ToString::to_string).collect();
2807 names.sort();
2808 let before = names.len();
2809 names.dedup();
2810 assert_eq!(
2811 names.len(),
2812 before,
2813 "two NodeTypeTag variants serialize to the same CSS tag name — \
2814 the string is then ambiguous on the way back in"
2815 );
2816 }
2817
2818 #[test]
2819 fn node_type_tag_display_then_from_str_round_trips_every_variant() {
2820 let mut broken: Vec<(NodeTypeTag, String)> = Vec::new();
2821 for tag in ALL_TAGS {
2822 let serialized = tag.to_string();
2823 let round_trips = matches!(NodeTypeTag::from_str(&serialized), Ok(t) if t == *tag);
2826 if !round_trips {
2827 broken.push((*tag, serialized));
2828 }
2829 }
2830 assert!(
2831 broken.is_empty(),
2832 "from_str(Display(tag)) must yield tag back, but these variants do not \
2833 round-trip: {broken:?}"
2834 );
2835 }
2836
2837 #[test]
2838 fn node_type_tag_serialize_parse_serialize_is_stable() {
2839 for tag in ALL_TAGS {
2842 let once = tag.to_string();
2843 if let Ok(parsed) = NodeTypeTag::from_str(&once) {
2844 assert_eq!(
2845 parsed.to_string(),
2846 once,
2847 "serialize(parse(serialize({tag:?}))) drifted"
2848 );
2849 }
2850 }
2851 }
2852
2853 #[test]
2854 fn node_type_tag_from_str_valid_minimal() {
2855 assert_eq!(NodeTypeTag::from_str("div"), Ok(NodeTypeTag::Div));
2856 assert_eq!(NodeTypeTag::from_str("p"), Ok(NodeTypeTag::P));
2857 assert_eq!(NodeTypeTag::from_str("a"), Ok(NodeTypeTag::A));
2858 }
2859
2860 #[test]
2861 fn node_type_tag_from_str_accepts_documented_aliases() {
2862 assert_eq!(NodeTypeTag::from_str("image"), Ok(NodeTypeTag::SvgImage));
2864 assert_eq!(NodeTypeTag::from_str("svg:image"), Ok(NodeTypeTag::SvgImage));
2865 assert_eq!(NodeTypeTag::SvgImage.to_string(), "svg:image");
2866
2867 assert_eq!(NodeTypeTag::from_str("iframe"), Ok(NodeTypeTag::VirtualView));
2868 assert_eq!(
2869 NodeTypeTag::from_str("virtual-view"),
2870 Ok(NodeTypeTag::VirtualView)
2871 );
2872
2873 for (bare, prefixed, tag) in [
2874 ("before", "::before", NodeTypeTag::Before),
2875 ("after", "::after", NodeTypeTag::After),
2876 ("marker", "::marker", NodeTypeTag::Marker),
2877 ("placeholder", "::placeholder", NodeTypeTag::Placeholder),
2878 ] {
2879 assert_eq!(NodeTypeTag::from_str(bare), Ok(tag));
2880 assert_eq!(NodeTypeTag::from_str(prefixed), Ok(tag));
2881 assert_eq!(
2882 tag.to_string(),
2883 prefixed,
2884 "pseudo-elements must serialize in their `::` form"
2885 );
2886 }
2887 }
2888
2889 #[test]
2890 fn node_type_tag_from_str_rejects_hostile_input_without_panicking() {
2891 let long = "a".repeat(1_000_000);
2892 let nested = "<".repeat(10_000);
2893 let hostile = [
2894 "", " ", " \t\n\r ", " div", "div ", " div ", "div;garbage", "DIV", "Div",
2903 "0",
2904 "-0",
2905 "9223372036854775807", "1e400",
2907 "NaN",
2908 "inf",
2909 "-inf",
2910 "\u{1F600}", "e\u{0301}", "\u{0}", "\u{FEFF}div", "div\u{0}",
2915 "*",
2916 "::",
2917 ":::before",
2918 "<script>",
2919 long.as_str(),
2920 nested.as_str(),
2921 ];
2922 for input in hostile {
2923 match NodeTypeTag::from_str(input) {
2924 Ok(t) => panic!("{input:?} must not parse, but produced {t:?}"),
2925 Err(NodeTypeTagParseError::Invalid(echoed)) => {
2926 assert_eq!(echoed, input, "the error must echo the input verbatim");
2927 }
2928 }
2929 }
2930 }
2931
2932 #[test]
2933 fn node_type_tag_parse_error_display_names_the_offending_input() {
2934 let e = NodeTypeTagParseError::Invalid("wat");
2935 assert_eq!(format!("{e}"), "Invalid node type: wat");
2936 assert_eq!(format!("{}", NodeTypeTagParseError::Invalid("")), "Invalid node type: ");
2938 assert!(format!("{}", NodeTypeTagParseError::Invalid("😀")).contains('😀'));
2939 }
2940
2941 #[test]
2942 fn node_type_tag_parse_error_to_contained_to_shared_round_trips() {
2943 let long = "x".repeat(10_000);
2944 for s in ["", " ", "div", "😀", "e\u{0301}", "\u{0}", long.as_str()] {
2945 let shared = NodeTypeTagParseError::Invalid(s);
2946 let owned = shared.to_contained();
2947 assert_eq!(owned, NodeTypeTagParseErrorOwned::Invalid(s.to_string().into()));
2948 assert_eq!(
2949 owned.to_shared(),
2950 shared,
2951 "to_shared(to_contained(x)) must equal x"
2952 );
2953 assert_eq!(format!("{}", owned.to_shared()), format!("{shared}"));
2955 }
2956 }
2957
2958 #[test]
2963 fn css_path_new_preserves_selectors_including_empty() {
2964 assert!(CssPath::new(Vec::new()).selectors.as_ref().is_empty());
2965 assert_eq!(CssPath::new(Vec::new()), CssPath::default());
2966
2967 let sels = vec![
2968 CssPathSelector::Type(NodeTypeTag::Div),
2969 CssPathSelector::DirectChildren,
2970 CssPathSelector::Class("c".to_string().into()),
2971 ];
2972 let p = CssPath::new(sels.clone());
2973 assert_eq!(p.selectors.as_ref(), &sels[..]);
2974 }
2975
2976 #[test]
2977 fn css_path_display_and_debug_agree_and_compose() {
2978 let p = CssPath::new(vec![
2979 CssPathSelector::Type(NodeTypeTag::Div),
2980 CssPathSelector::Id("id".to_string().into()),
2981 CssPathSelector::Class("cls".to_string().into()),
2982 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
2983 ]);
2984 assert_eq!(format!("{p}"), "div#id.cls:hover");
2985 assert_eq!(format!("{p:?}"), format!("{p}"), "Debug delegates to Display");
2986 assert_eq!(format!("{}", CssPath::default()), "");
2988 }
2989
2990 #[test]
2991 fn push_front_scope_scopes_a_bare_star_rule_to_the_node_only() {
2992 let mut p = CssPath::new(vec![CssPathSelector::Global]);
2994 p.push_front_scope(5, 9);
2995 assert_eq!(
2996 p.selectors.as_ref(),
2997 &[
2998 CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
2999 CssPathSelector::Global,
3000 ][..],
3001 "inline style must not leak past the owner node (#47)"
3002 );
3003 }
3004
3005 #[test]
3006 fn push_front_scope_scopes_a_real_selector_to_the_whole_subtree() {
3007 let mut p = CssPath::new(vec![CssPathSelector::Class("menu-item".to_string().into())]);
3008 p.push_front_scope(5, 9);
3009 assert_eq!(
3010 p.selectors.as_ref()[0],
3011 CssPathSelector::Root(CssScopeRange { start: 5, end: 9 })
3012 );
3013 assert_eq!(p.selectors.as_ref().len(), 2);
3014 }
3015
3016 #[test]
3017 fn push_front_scope_on_an_empty_path_uses_the_subtree_range() {
3018 let mut p = CssPath::default();
3020 p.push_front_scope(2, 8);
3021 assert_eq!(
3022 p.selectors.as_ref(),
3023 &[CssPathSelector::Root(CssScopeRange { start: 2, end: 8 })][..]
3024 );
3025 }
3026
3027 #[test]
3028 fn push_front_scope_at_numeric_boundaries_does_not_panic() {
3029 for (start, end) in [
3032 (0_usize, 0_usize),
3033 (0, usize::MAX),
3034 (usize::MAX, usize::MAX),
3035 (usize::MAX, 0), (9, 5), ] {
3038 let mut p = CssPath::new(vec![CssPathSelector::Class("c".to_string().into())]);
3039 p.push_front_scope(start, end);
3040 assert_eq!(
3041 p.selectors.as_ref()[0],
3042 CssPathSelector::Root(CssScopeRange { start, end })
3043 );
3044 }
3045 let mut g = CssPath::new(vec![CssPathSelector::Global]);
3047 g.push_front_scope(usize::MAX, 0);
3048 assert_eq!(
3049 g.selectors.as_ref()[0],
3050 CssPathSelector::Root(CssScopeRange {
3051 start: usize::MAX,
3052 end: usize::MAX
3053 })
3054 );
3055 }
3056
3057 #[test]
3058 fn push_front_scope_applied_twice_stacks_root_selectors() {
3059 let mut p = CssPath::new(vec![CssPathSelector::Global]);
3060 p.push_front_scope(5, 9);
3061 p.push_front_scope(1, 20);
3064 assert_eq!(
3065 p.selectors.as_ref(),
3066 &[
3067 CssPathSelector::Root(CssScopeRange { start: 1, end: 20 }),
3068 CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
3069 CssPathSelector::Global,
3070 ][..]
3071 );
3072 }
3073
3074 #[test]
3075 fn push_front_scope_on_a_long_path_preserves_order_and_length() {
3076 let sels: Vec<CssPathSelector> = (0..5_000)
3077 .map(|i| CssPathSelector::Class(format!("c{i}").into()))
3078 .collect();
3079 let mut p = CssPath::new(sels.clone());
3080 p.push_front_scope(3, 4);
3081 assert_eq!(p.selectors.as_ref().len(), 5_001);
3082 assert_eq!(
3083 p.selectors.as_ref()[0],
3084 CssPathSelector::Root(CssScopeRange { start: 3, end: 4 })
3085 );
3086 assert_eq!(&p.selectors.as_ref()[1..], &sels[..], "the tail must be untouched");
3087 }
3088
3089 #[test]
3090 fn scope_range_contains_at_zero_and_usize_max() {
3091 let zero = CssScopeRange { start: 0, end: 0 };
3092 assert!(zero.contains(0));
3093 assert!(!zero.contains(1));
3094 assert!(!zero.contains(usize::MAX));
3095
3096 let full = CssScopeRange {
3097 start: 0,
3098 end: usize::MAX,
3099 };
3100 assert!(full.contains(0));
3101 assert!(full.contains(usize::MAX));
3102 assert!(full.contains(usize::MAX / 2));
3103
3104 let top = CssScopeRange {
3105 start: usize::MAX,
3106 end: usize::MAX,
3107 };
3108 assert!(top.contains(usize::MAX));
3109 assert!(!top.contains(usize::MAX - 1));
3110 assert!(!top.contains(0));
3111 }
3112
3113 #[test]
3114 fn scope_range_inverted_contains_nothing() {
3115 let inverted = CssScopeRange { start: 9, end: 5 };
3117 for n in [0_usize, 4, 5, 7, 9, 10, usize::MAX] {
3118 assert!(!inverted.contains(n), "inverted range must never match {n}");
3119 }
3120 }
3121
3122 #[test]
3127 fn css_path_selector_display_covers_every_variant() {
3128 let cases: Vec<(CssPathSelector, String)> = vec![
3129 (CssPathSelector::Global, "*".to_string()),
3130 (
3131 CssPathSelector::Root(CssScopeRange { start: 2, end: 6 }),
3132 ":root(2..=6)".to_string(),
3133 ),
3134 (
3135 CssPathSelector::Type(NodeTypeTag::Div),
3136 "div".to_string(),
3137 ),
3138 (
3139 CssPathSelector::Class("c".to_string().into()),
3140 ".c".to_string(),
3141 ),
3142 (CssPathSelector::Id("i".to_string().into()), "#i".to_string()),
3143 (
3144 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Focus),
3145 ":focus".to_string(),
3146 ),
3147 (
3148 CssPathSelector::Attribute(CssAttributeSelector::default()),
3149 "[]".to_string(),
3150 ),
3151 (CssPathSelector::DirectChildren, ">".to_string()),
3152 (CssPathSelector::Children, " ".to_string()),
3153 (CssPathSelector::AdjacentSibling, "+".to_string()),
3154 (CssPathSelector::GeneralSibling, "~".to_string()),
3155 ];
3156 for (sel, expected) in cases {
3157 assert_eq!(format!("{sel}"), expected);
3158 }
3159 assert_eq!(CssPathSelector::default(), CssPathSelector::Global);
3160 }
3161
3162 #[test]
3163 fn css_path_selector_display_at_scope_range_boundaries() {
3164 let s = CssPathSelector::Root(CssScopeRange {
3165 start: 0,
3166 end: usize::MAX,
3167 });
3168 assert_eq!(format!("{s}"), format!(":root(0..={})", usize::MAX));
3169 }
3170
3171 #[test]
3172 fn css_path_selector_display_with_empty_and_unicode_names() {
3173 assert_eq!(
3175 format!("{}", CssPathSelector::Class(String::new().into())),
3176 "."
3177 );
3178 assert_eq!(format!("{}", CssPathSelector::Id(String::new().into())), "#");
3179 assert_eq!(
3180 format!("{}", CssPathSelector::Class("😀".to_string().into())),
3181 ".😀"
3182 );
3183 let long = "x".repeat(100_000);
3184 assert_eq!(
3185 format!("{}", CssPathSelector::Id(long.clone().into())).len(),
3186 long.len() + 1
3187 );
3188 }
3189
3190 #[test]
3191 fn attribute_match_op_symbol_prefix_matrix() {
3192 assert_eq!(AttributeMatchOp::Exists.symbol_prefix(), "");
3193 assert_eq!(AttributeMatchOp::Eq.symbol_prefix(), "");
3194 assert_eq!(AttributeMatchOp::Includes.symbol_prefix(), "~");
3195 assert_eq!(AttributeMatchOp::DashMatch.symbol_prefix(), "|");
3196 assert_eq!(AttributeMatchOp::Prefix.symbol_prefix(), "^");
3197 assert_eq!(AttributeMatchOp::Suffix.symbol_prefix(), "$");
3198 assert_eq!(AttributeMatchOp::Substring.symbol_prefix(), "*");
3199 assert_eq!(AttributeMatchOp::default(), AttributeMatchOp::Exists);
3200 }
3201
3202 #[test]
3203 fn css_attribute_selector_display_renders_each_operator() {
3204 let ops = [
3205 (AttributeMatchOp::Eq, "[a=\"v\"]"),
3206 (AttributeMatchOp::Includes, "[a~=\"v\"]"),
3207 (AttributeMatchOp::DashMatch, "[a|=\"v\"]"),
3208 (AttributeMatchOp::Prefix, "[a^=\"v\"]"),
3209 (AttributeMatchOp::Suffix, "[a$=\"v\"]"),
3210 (AttributeMatchOp::Substring, "[a*=\"v\"]"),
3211 ];
3212 for (op, expected) in ops {
3213 let sel = CssAttributeSelector {
3214 name: "a".to_string().into(),
3215 op,
3216 value: OptionString::Some("v".to_string().into()),
3217 };
3218 assert_eq!(format!("{sel}"), expected);
3219 }
3220 }
3221
3222 #[test]
3223 fn css_attribute_selector_exists_ignores_any_value() {
3224 let sel = CssAttributeSelector {
3226 name: "a".to_string().into(),
3227 op: AttributeMatchOp::Exists,
3228 value: OptionString::Some("ignored".to_string().into()),
3229 };
3230 assert_eq!(format!("{sel}"), "[a]");
3231 assert_eq!(format!("{}", CssAttributeSelector::default()), "[]");
3232 }
3233
3234 #[test]
3235 fn css_attribute_selector_missing_value_renders_an_empty_string_literal() {
3236 let sel = CssAttributeSelector {
3237 name: "a".to_string().into(),
3238 op: AttributeMatchOp::Eq,
3239 value: OptionString::None,
3240 };
3241 assert_eq!(format!("{sel}"), "[a=\"\"]");
3242 }
3243
3244 #[test]
3245 fn css_attribute_selector_display_with_hostile_names_and_values_does_not_panic() {
3246 for (name, value) in [
3247 ("", ""),
3248 ("😀", "😀"),
3249 ("a\u{0301}", "e\u{0301}"),
3250 ("a", "has \" quote"), ("a", "]"),
3252 ("a", "\u{0}"),
3253 ("a", "\n"),
3254 ] {
3255 let sel = CssAttributeSelector {
3256 name: name.to_string().into(),
3257 op: AttributeMatchOp::Eq,
3258 value: OptionString::Some(value.to_string().into()),
3259 };
3260 let out = format!("{sel}");
3261 assert!(out.starts_with('[') && out.ends_with(']'));
3262 }
3263 }
3264
3265 #[test]
3266 fn css_nth_child_selector_display_at_numeric_boundaries() {
3267 assert_eq!(format!("{}", CssNthChildSelector::Number(0)), "0");
3268 assert_eq!(
3269 format!("{}", CssNthChildSelector::Number(u32::MAX)),
3270 u32::MAX.to_string()
3271 );
3272 assert_eq!(format!("{}", CssNthChildSelector::Even), "even");
3273 assert_eq!(format!("{}", CssNthChildSelector::Odd), "odd");
3274 assert_eq!(
3275 format!(
3276 "{}",
3277 CssNthChildSelector::Pattern(CssNthChildPattern {
3278 pattern_repeat: 2,
3279 offset: 1,
3280 })
3281 ),
3282 "2n + 1"
3283 );
3284 assert_eq!(
3286 format!(
3287 "{}",
3288 CssNthChildSelector::Pattern(CssNthChildPattern {
3289 pattern_repeat: 0,
3290 offset: 0,
3291 })
3292 ),
3293 "0n + 0"
3294 );
3295 let maxed = CssNthChildSelector::Pattern(CssNthChildPattern {
3296 pattern_repeat: u32::MAX,
3297 offset: u32::MAX,
3298 });
3299 assert_eq!(
3300 format!("{maxed}"),
3301 format!("{}n + {}", u32::MAX, u32::MAX)
3302 );
3303 }
3304
3305 #[test]
3306 fn css_path_pseudo_selector_display_covers_every_variant() {
3307 let cases: Vec<(CssPathPseudoSelector, String)> = vec![
3308 (CssPathPseudoSelector::First, "first".to_string()),
3309 (CssPathPseudoSelector::Last, "last".to_string()),
3310 (
3311 CssPathPseudoSelector::NthChild(CssNthChildSelector::Even),
3312 "nth-child(even)".to_string(),
3313 ),
3314 (CssPathPseudoSelector::Hover, "hover".to_string()),
3315 (CssPathPseudoSelector::Active, "active".to_string()),
3316 (CssPathPseudoSelector::Focus, "focus".to_string()),
3317 (
3318 CssPathPseudoSelector::Lang("de-DE".to_string().into()),
3319 "lang(de-DE)".to_string(),
3320 ),
3321 (CssPathPseudoSelector::Backdrop, "backdrop".to_string()),
3322 (CssPathPseudoSelector::Dragging, "dragging".to_string()),
3323 (CssPathPseudoSelector::DragOver, "drag-over".to_string()),
3324 ];
3325 for (p, expected) in cases {
3326 assert_eq!(format!("{p}"), expected);
3327 }
3328 }
3329
3330 #[test]
3331 fn css_path_pseudo_selector_lang_with_hostile_payloads_does_not_panic() {
3332 let long = "l".repeat(100_000);
3333 for lang in ["", " ", "😀", "e\u{0301}", ")", "\u{0}", long.as_str()] {
3334 let p = CssPathPseudoSelector::Lang(lang.to_string().into());
3335 let out = format!("{p}");
3336 assert!(out.starts_with("lang(") && out.ends_with(')'));
3337 }
3338 }
3339
3340 #[test]
3345 fn get_specificity_of_an_empty_path_is_all_zero() {
3346 assert_eq!(get_specificity(&CssPath::default()), (0, 0, 0, 0));
3347 assert_eq!(get_specificity(&CssPath::new(Vec::new())), (0, 0, 0, 0));
3348 }
3349
3350 #[test]
3351 fn get_specificity_counts_ids_classes_and_types_separately() {
3352 let path = CssPath::new(vec![
3353 CssPathSelector::Id("a".to_string().into()),
3354 CssPathSelector::Id("b".to_string().into()),
3355 CssPathSelector::Class("c".to_string().into()),
3356 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
3357 CssPathSelector::Attribute(CssAttributeSelector::default()),
3358 CssPathSelector::Type(NodeTypeTag::Div),
3359 ]);
3360 assert_eq!(get_specificity(&path), (2, 3, 1, 6));
3362 }
3363
3364 #[test]
3365 fn get_specificity_ignores_combinators_and_root_except_in_the_total() {
3366 let path = CssPath::new(vec![
3367 CssPathSelector::Root(CssScopeRange { start: 0, end: 9 }),
3368 CssPathSelector::Global,
3369 CssPathSelector::Children,
3370 CssPathSelector::DirectChildren,
3371 CssPathSelector::AdjacentSibling,
3372 CssPathSelector::GeneralSibling,
3373 ]);
3374 let (ids, classes, types, total) = get_specificity(&path);
3375 assert_eq!((ids, classes, types), (0, 0, 0));
3376 assert_eq!(total, 6, "the 4th field is the raw selector count");
3377 }
3378
3379 #[test]
3380 fn get_specificity_orders_ids_above_classes_above_types() {
3381 let id = get_specificity(&CssPath::new(vec![CssPathSelector::Id("x".to_string().into())]));
3382 let class = get_specificity(&CssPath::new(vec![CssPathSelector::Class(
3383 "x".to_string().into(),
3384 )]));
3385 let ty = get_specificity(&CssPath::new(vec![CssPathSelector::Type(NodeTypeTag::Div)]));
3386 let star = get_specificity(&CssPath::new(vec![CssPathSelector::Global]));
3387 assert!(star < ty, "* must be the weakest");
3388 assert!(ty < class);
3389 assert!(class < id);
3390 }
3391
3392 #[test]
3393 fn get_specificity_on_a_huge_path_does_not_overflow_or_hang() {
3394 let sels: Vec<CssPathSelector> = (0..50_000)
3395 .map(|i| CssPathSelector::Id(format!("i{i}").into()))
3396 .collect();
3397 let path = CssPath::new(sels);
3398 assert_eq!(get_specificity(&path), (50_000, 0, 0, 50_000));
3399 }
3400
3401 #[cfg(feature = "parser")]
3406 fn parse(s: &str) -> Css {
3407 Css::from_string(s.to_string().into())
3408 }
3409
3410 #[cfg(feature = "parser")]
3411 #[test]
3412 fn from_string_on_empty_and_whitespace_only_input_yields_no_rules() {
3413 for input in ["", " ", " ", "\t\n\r\n ", "\u{FEFF}", "\u{00A0}"] {
3414 let css = parse(input);
3415 assert!(
3416 css.is_empty(),
3417 "{input:?} must produce zero rules, got {}",
3418 css.rules.as_ref().len()
3419 );
3420 }
3421 }
3422
3423 #[cfg(feature = "parser")]
3424 #[test]
3425 fn from_string_on_garbage_never_panics_and_is_deterministic() {
3426 let garbage = [
3427 "}}}}",
3428 "{{{{",
3429 "@@@@",
3430 ";;;;",
3431 "\u{0}\u{1}\u{2}",
3432 "div {",
3433 "div }",
3434 "} div {",
3435 "div { color",
3436 "div { color: }",
3437 "div { : red; }",
3438 "* * * * *",
3439 ":::::",
3440 "[[[[",
3441 "/* unterminated comment",
3442 "@media {",
3443 "url(",
3444 "\"unterminated",
3445 "div{color:red;}}}}",
3446 ];
3447 for input in garbage {
3448 let a = parse(input);
3449 let b = parse(input);
3450 assert_eq!(a, b, "parsing {input:?} must be deterministic");
3451 }
3452 }
3453
3454 #[cfg(feature = "parser")]
3455 #[test]
3456 fn from_string_valid_minimal_produces_one_author_rule() {
3457 let css = parse("div { width: 200px; }");
3458 assert_eq!(css.rules.as_ref().len(), 1);
3459 let rule = &css.rules.as_ref()[0];
3460 assert_eq!(
3461 rule.priority,
3462 rule_priority::AUTHOR,
3463 "parser output belongs to the author layer"
3464 );
3465 assert_eq!(
3466 rule.path.selectors.as_ref(),
3467 &[CssPathSelector::Type(NodeTypeTag::Div)][..]
3468 );
3469 let props: Vec<CssPropertyType> = css
3470 .iter_inline_properties()
3471 .map(|(p, _)| p.get_type())
3472 .collect();
3473 assert_eq!(props, vec![CssPropertyType::Width]);
3474 }
3475
3476 #[cfg(feature = "parser")]
3477 #[test]
3478 fn from_string_handles_leading_and_trailing_junk_deterministically() {
3479 assert_eq!(parse(" div { width: 1px; } "), parse("div { width: 1px; }"));
3481 let with_junk = parse("div { width: 1px; } @@@ garbage");
3483 assert!(
3484 with_junk.rules().any(|r| r.path.selectors.as_ref()
3485 == &[CssPathSelector::Type(NodeTypeTag::Div)][..]),
3486 "the leading valid rule must survive trailing junk"
3487 );
3488 }
3489
3490 #[cfg(feature = "parser")]
3491 #[test]
3492 fn from_string_on_boundary_numbers_does_not_panic() {
3493 let inputs = [
3494 "div { width: 0px; }",
3495 "div { width: -0px; }",
3496 "div { width: -1px; }",
3497 "div { width: 9223372036854775807px; }", "div { width: 340282350000000000000000000000000000000px; }", "div { width: 1e400px; }", "div { width: 1e-400px; }", "div { width: NaN; }",
3502 "div { width: inf; }",
3503 "div { width: -inf; }",
3504 "div { width: 99999999999999999999999999px; }",
3505 "div { opacity: 1e309; }",
3506 "div { z-index: -9223372036854775808; }", "div { width: .....; }",
3508 "div { width: --5px; }",
3509 ];
3510 for input in inputs {
3511 let css = parse(input);
3512 assert!(
3513 css.rules.as_ref().len() <= 1,
3514 "{input:?} must not explode into multiple rules"
3515 );
3516 }
3517 }
3518
3519 #[cfg(feature = "parser")]
3520 #[test]
3521 fn from_string_on_unicode_input_does_not_panic() {
3522 let inputs = [
3523 "div { content: \"😀\"; }",
3524 ".😀 { width: 1px; }",
3525 "#e\u{0301} { width: 1px; }",
3526 "div { font-family: \"日本語\"; }",
3527 "div\u{0301} { width: 1px; }",
3528 "div { width: 1px; } /* 🎉 */",
3529 "\u{202E}div { width: 1px; }", ];
3531 for input in inputs {
3532 let css = parse(input);
3533 assert_eq!(css, parse(input));
3535 }
3536 }
3537
3538 #[cfg(feature = "parser")]
3539 #[test]
3540 fn from_string_on_a_one_megabyte_input_terminates() {
3541 let body = "width:1px;".repeat(100_000);
3543 assert!(body.len() >= 1_000_000);
3544 let css = parse(&format!("div{{{body}}}"));
3545 assert_eq!(css.rules.as_ref().len(), 1);
3546 assert!(!css.rules.as_ref()[0].declarations.as_ref().is_empty());
3547
3548 let junk = "a".repeat(100_000);
3550 let _ = parse(&junk);
3551 }
3552
3553 #[cfg(feature = "parser")]
3554 #[test]
3555 fn from_string_on_deeply_nested_blocks_does_not_stack_overflow() {
3556 let depth = 10_000;
3559 let mut s = String::with_capacity(depth * 8);
3560 for _ in 0..depth {
3561 s.push_str("div{");
3562 }
3563 s.push_str("width:1px;");
3564 for _ in 0..depth {
3565 s.push('}');
3566 }
3567
3568 let handle = std::thread::Builder::new()
3569 .stack_size(64 * 1024 * 1024)
3570 .spawn(move || Css::from_string(s.into()).rules.as_ref().len())
3571 .expect("spawning the parser thread must succeed");
3572
3573 let rule_count = handle
3574 .join()
3575 .expect("10_000 nested blocks must not panic or overflow the stack");
3576 assert!(rule_count <= depth + 1, "rule count must stay bounded by the nesting depth");
3577 }
3578
3579 #[cfg(feature = "parser")]
3580 #[test]
3581 fn from_string_with_warnings_agrees_with_from_string() {
3582 for input in [
3583 "",
3584 " ",
3585 "div { width: 1px; }",
3586 "div { not-a-property: 1; }",
3587 "}}} garbage {{{",
3588 "div { width: NaN; }",
3589 ] {
3590 let (css, _warnings) = Css::from_string_with_warnings(input.to_string().into());
3591 assert_eq!(
3592 css,
3593 parse(input),
3594 "from_string_with_warnings must parse {input:?} identically to from_string"
3595 );
3596 }
3597 }
3598
3599 #[cfg(feature = "parser")]
3600 #[test]
3601 fn from_string_with_warnings_reports_an_unknown_property() {
3602 let (css, warnings) =
3603 Css::from_string_with_warnings("div { definitely-not-a-property: 1px; }".to_string().into());
3604 assert!(
3605 !warnings.is_empty(),
3606 "an unknown property must surface as a warning rather than being dropped silently"
3607 );
3608 assert!(css.rules.as_ref().len() <= 1);
3611 }
3612
3613 #[cfg(feature = "parser")]
3614 #[test]
3615 fn from_string_with_warnings_on_empty_input_has_no_rules() {
3616 let (css, warnings) = Css::from_string_with_warnings(String::new().into());
3617 assert!(css.is_empty());
3618 assert!(warnings.is_empty());
3619 }
3620
3621 #[cfg(feature = "parser")]
3622 #[test]
3623 fn parse_inline_marks_every_rule_as_the_inline_layer() {
3624 let css = Css::parse_inline("width: 200px; color: red;");
3625 assert!(!css.is_empty());
3626 for r in css.rules() {
3627 assert_eq!(
3628 r.priority,
3629 rule_priority::INLINE,
3630 "parse_inline must stamp every rule with the INLINE layer"
3631 );
3632 }
3633 let props: Vec<CssPropertyType> = css
3634 .iter_inline_properties()
3635 .map(|(p, _)| p.get_type())
3636 .collect();
3637 assert!(props.contains(&CssPropertyType::Width));
3638 assert!(props.contains(&CssPropertyType::TextColor));
3639 }
3640
3641 #[cfg(feature = "parser")]
3642 #[test]
3643 fn parse_inline_wraps_bare_declarations_in_a_star_rule() {
3644 let css = Css::parse_inline("width: 200px;");
3645 assert_eq!(css.rules.as_ref().len(), 1);
3646 assert_eq!(
3647 css.rules.as_ref()[0].path.selectors.as_ref(),
3648 &[CssPathSelector::Global][..],
3649 "the wrapper path must be exactly `*` — push_front_scope keys node-only \
3650 inline semantics off that shape"
3651 );
3652 }
3653
3654 #[cfg(feature = "parser")]
3655 #[test]
3656 fn parse_inline_on_empty_and_whitespace_input_does_not_panic() {
3657 for input in ["", " ", "\t\n", " \r\n "] {
3658 let css = Css::parse_inline(input);
3659 for r in css.rules() {
3660 assert_eq!(r.priority, rule_priority::INLINE);
3661 assert!(
3662 r.declarations.as_ref().is_empty(),
3663 "an empty inline style must not produce declarations"
3664 );
3665 }
3666 }
3667 }
3668
3669 #[cfg(feature = "parser")]
3670 #[test]
3671 fn parse_inline_on_garbage_never_panics_and_is_deterministic() {
3672 for input in [
3673 "}}}}",
3674 "{{{{",
3675 ";;;;",
3676 ":::",
3677 "color",
3678 "color:",
3679 ": red",
3680 "\u{0}\u{1}",
3681 "/* unterminated",
3682 "@@@",
3683 "width: 1px", ] {
3685 assert_eq!(
3686 Css::parse_inline(input),
3687 Css::parse_inline(input),
3688 "parse_inline({input:?}) must be deterministic"
3689 );
3690 }
3691 }
3692
3693 #[cfg(feature = "parser")]
3694 #[test]
3695 fn parse_inline_on_unicode_and_boundary_numbers_does_not_panic() {
3696 for input in [
3697 "content: \"😀\"",
3698 "font-family: \"日本語\"",
3699 "width: 0px",
3700 "width: -0px",
3701 "width: 9223372036854775807px",
3702 "width: 1e400px",
3703 "width: NaN",
3704 "width: inf",
3705 "opacity: 1e309",
3706 ] {
3707 let css = Css::parse_inline(input);
3708 for r in css.rules() {
3709 assert_eq!(r.priority, rule_priority::INLINE);
3710 }
3711 }
3712 }
3713
3714 #[cfg(feature = "parser")]
3715 #[test]
3716 fn parse_inline_on_a_one_megabyte_style_terminates() {
3717 let style = "width:1px;".repeat(100_000);
3718 assert!(style.len() >= 1_000_000);
3719 let css = Css::parse_inline(&style);
3720 assert!(!css.is_empty());
3721 for r in css.rules() {
3722 assert_eq!(r.priority, rule_priority::INLINE);
3723 }
3724 }
3725
3726 #[cfg(feature = "parser")]
3727 #[test]
3728 fn parse_inline_supports_nested_pseudo_blocks() {
3729 let css = Css::parse_inline(":hover { color: red; }");
3731 assert!(!css.is_empty(), "a nested pseudo block must produce a rule");
3732 for r in css.rules() {
3733 assert_eq!(r.priority, rule_priority::INLINE);
3734 }
3735 }
3736
3737 #[cfg(feature = "parser")]
3738 #[test]
3739 fn parse_inline_must_not_let_a_brace_escape_the_star_wrapper() {
3740 let css = Css::parse_inline("color: red; } div { background: green;");
3746 for r in css.rules() {
3747 let first = r.path.selectors.as_ref().first();
3748 assert!(
3749 matches!(first, None | Some(CssPathSelector::Global)),
3750 "a `}}` in the inline style escaped the `*` wrapper and produced the \
3751 free-standing rule `{}` (selector injection)",
3752 r.path
3753 );
3754 }
3755 }
3756}