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