Skip to main content

azul_css/
css.rs

1//! Types and methods used to describe the style of an application.
2//!
3//! This module defines the core CSS data model:
4//!
5//! - [`Css`] contains one or more [`Stylesheet`]s, each holding [`CssRuleBlock`]s.
6//! - A [`CssRuleBlock`] pairs a [`CssPath`] (selector) with [`CssDeclaration`]s (properties).
7//! - [`CssPropertyValue<T>`] wraps individual property values with CSS keywords
8//!   (`auto`, `inherit`, `initial`, etc.).
9//! - [`BoxOrStatic<T>`] is a smart-pointer enum for heap-allocated or static CSS values.
10//! - [`NodeTypeTag`] enumerates all recognized HTML/SVG element types for selector matching.
11use 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/// Css stylesheet - contains a parsed CSS stylesheet in "rule blocks",
22/// i.e. blocks of key-value pairs associated with a selector path.
23///
24/// Layer separation (UA / system / author / inline / runtime) is encoded
25/// per-rule via `CssRuleBlock::priority`; see [`rule_priority`] for the
26/// slot allocation. There is no separate `Stylesheet` wrapper — to merge
27/// two CSS sources, concatenate their `rules` and re-sort.
28#[derive(Debug, Default, PartialEq, Clone)]
29#[repr(C)]
30pub struct Css {
31    /// All rule blocks, in source order. Sort by `(priority, specificity)`
32    /// via `sort_by_specificity` to put them in cascade order.
33    pub rules: CssRuleBlockVec,
34    /// Every `@keyframes` block in this stylesheet, in source order. Web
35    /// compatibility sugar: internally each named track compiles to an
36    /// animation-function invocation (`animation` / `-azul-animation-in` /
37    /// `-azul-animation-out` reference these by name).
38    pub keyframes: KeyframesVec,
39}
40
41/// One stop of a `@keyframes` block (`from` = 0, `to` = 1000, `12.5%` = 125).
42///
43/// Permille rather than an f32 percentage so the type stays `Eq`-capable —
44/// `Css` must remain `Eq`/`Ord` (`NodeData` carries it), and 0.1% resolution is
45/// beyond anything a keyframe needs.
46#[derive(Debug, Default, PartialEq, Eq, Clone)]
47#[repr(C)]
48pub struct KeyframeStop {
49    /// 0..=1000 position of this stop.
50    pub permille: u16,
51    /// The property values at this stop.
52    pub props: crate::props::property::CssPropertyVec,
53}
54
55/// A named `@keyframes` block: `@keyframes flyOutRight { from {..} to {..} }`.
56#[derive(Debug, Default, PartialEq, Eq, Clone)]
57#[repr(C)]
58pub struct Keyframes {
59    pub name: AzString,
60    /// Sorted by `permille` ascending at parse time.
61    pub stops: KeyframeStopVec,
62}
63
64crate::impl_vec!(
65    KeyframeStop,
66    KeyframeStopVec,
67    KeyframeStopVecDestructor,
68    KeyframeStopVecDestructorType,
69    KeyframeStopVecSlice,
70    OptionKeyframeStop
71);
72crate::impl_vec_mut!(KeyframeStop, KeyframeStopVec);
73crate::impl_vec_debug!(KeyframeStop, KeyframeStopVec);
74crate::impl_vec_clone!(KeyframeStop, KeyframeStopVec, KeyframeStopVecDestructor);
75crate::impl_vec_partialeq!(KeyframeStop, KeyframeStopVec);
76crate::impl_vec_eq!(KeyframeStop, KeyframeStopVec);
77crate::impl_option!(
78    KeyframeStop,
79    OptionKeyframeStop,
80    copy = false,
81    [Debug, Clone, PartialEq, Eq]
82);
83
84crate::impl_vec!(
85    Keyframes,
86    KeyframesVec,
87    KeyframesVecDestructor,
88    KeyframesVecDestructorType,
89    KeyframesVecSlice,
90    OptionKeyframes
91);
92crate::impl_vec_mut!(Keyframes, KeyframesVec);
93crate::impl_vec_debug!(Keyframes, KeyframesVec);
94crate::impl_vec_clone!(Keyframes, KeyframesVec, KeyframesVecDestructor);
95crate::impl_vec_partialeq!(Keyframes, KeyframesVec);
96crate::impl_vec_eq!(Keyframes, KeyframesVec);
97crate::impl_option!(
98    Keyframes,
99    OptionKeyframes,
100    copy = false,
101    [Debug, Clone, PartialEq, Eq]
102);
103
104impl_option!(
105    Css,
106    OptionCss,
107    copy = false,
108    [Debug, Clone, PartialEq, Eq, PartialOrd]
109);
110
111impl_vec!(
112    Css,
113    CssVec,
114    CssVecDestructor,
115    CssVecDestructorType,
116    CssVecSlice,
117    OptionCss
118);
119impl_vec_mut!(Css, CssVec);
120impl_vec_debug!(Css, CssVec);
121impl_vec_partialord!(Css, CssVec);
122impl_vec_clone!(Css, CssVec, CssVecDestructor);
123impl_vec_partialeq!(Css, CssVec);
124
125impl_vec!(
126    CssRuleBlock,
127    CssRuleBlockVec,
128    CssRuleBlockVecDestructor,
129    CssRuleBlockVecDestructorType,
130    CssRuleBlockVecSlice,
131    OptionCssRuleBlock
132);
133impl_vec_mut!(CssRuleBlock, CssRuleBlockVec);
134impl_vec_debug!(CssRuleBlock, CssRuleBlockVec);
135impl_vec_partialord!(CssRuleBlock, CssRuleBlockVec);
136impl_vec_clone!(CssRuleBlock, CssRuleBlockVec, CssRuleBlockVecDestructor);
137impl_vec_partialeq!(CssRuleBlock, CssRuleBlockVec);
138
139impl Css {
140    /// The viewport-size thresholds (widths, heights, logical px) at which
141    /// any `@media (min-/max-width/height)` rule in this stylesheet can flip.
142    /// Sorted, deduplicated by bit pattern. Used by the engine's resize
143    /// decision instead of a hardcoded breakpoint list.
144    #[must_use]
145    pub fn viewport_breakpoints(&self) -> (Vec<f32>, Vec<f32>) {
146        let mut w = Vec::new();
147        let mut h = Vec::new();
148        for rule in self.rules.as_ref() {
149            crate::dynamic_selector::collect_viewport_thresholds(
150                rule.conditions.as_ref(),
151                &mut w,
152                &mut h,
153            );
154        }
155        w.sort_by_key(|v| v.to_bits());
156        w.dedup_by_key(|v| v.to_bits());
157        h.sort_by_key(|v| v.to_bits());
158        h.dedup_by_key(|v| v.to_bits());
159        (w, h)
160    }
161
162    #[must_use]
163    pub fn is_empty(&self) -> bool {
164        self.rules.as_ref().is_empty()
165    }
166
167    #[must_use]
168    pub fn new(rules: Vec<CssRuleBlock>) -> Self {
169        Self {
170            rules: rules.into(),
171            keyframes: KeyframesVec::from_const_slice(&[]),
172        }
173    }
174
175    #[cfg(feature = "parser")]
176    // takes the owned C-ABI `AzString` by value by FFI ownership-transfer convention,
177    // even though only a string slice is read here.
178    #[allow(clippy::needless_pass_by_value)]
179    #[must_use]
180    pub fn from_string(s: AzString) -> Self {
181        crate::parser2::new_from_str(s.as_str()).0
182    }
183
184    /// Parse inline-style CSS (bare properties, pseudo blocks, @-rule blocks)
185    /// and return a `Css` whose rules carry `rule_priority::INLINE`.
186    ///
187    /// Wraps the input in `* { ... }` so the main CSS parser can handle bare
188    /// properties at the top level. Pseudo and at-rule blocks like
189    /// `:hover { color: red; }` or `@os(linux) { font-size: 14px; }` work
190    /// directly via CSS nesting.
191    #[cfg(feature = "parser")]
192    #[must_use]
193    pub fn parse_inline(style: &str) -> Self {
194        use alloc::string::ToString;
195        let mut wrapped = String::with_capacity(style.len() + 6);
196        wrapped.push_str("* {\n");
197        wrapped.push_str(style);
198        wrapped.push_str("\n}");
199        let (mut css, _warnings) = crate::parser2::new_from_str(&wrapped);
200        // A `}` in `style` closes the `* {` wrapper early, so the remainder is parsed as
201        // a free-standing rule with a caller-controlled selector (selector injection).
202        // Every rule an inline style produces MUST stay rooted at the `*` wrapper, so
203        // drop any that isn't Global-rooted. Legitimate pseudo/at-rule nesting stays a
204        // child of `*` (still Global-rooted per push_front_scope), so it is kept.
205        css.rules.retain(|rule| {
206            matches!(
207                rule.path.selectors.as_ref().first(),
208                None | Some(CssPathSelector::Global)
209            )
210        });
211        for rule in css.rules.as_mut() {
212            rule.priority = rule_priority::INLINE;
213        }
214        css
215    }
216
217    #[cfg(feature = "parser")]
218    // takes the owned C-ABI `AzString` by value by FFI ownership-transfer convention,
219    // even though only a string slice is read here.
220    #[allow(clippy::needless_pass_by_value)]
221    #[must_use]
222    pub fn from_string_with_warnings(
223        s: AzString,
224    ) -> (Self, Vec<crate::parser2::CssParseWarnMsgOwned>) {
225        let (css, warnings) = crate::parser2::new_from_str(s.as_str());
226        (
227            css,
228            warnings
229                .into_iter()
230                .map(|w| crate::parser2::CssParseWarnMsgOwned {
231                    warning: w.warning.to_contained(),
232                    location: w.location,
233                })
234                .collect(),
235        )
236    }
237}
238
239impl From<Vec<CssRuleBlock>> for Css {
240    fn from(rules: Vec<CssRuleBlock>) -> Self {
241        Self {
242            rules: rules.into(),
243            keyframes: KeyframesVec::from_const_slice(&[]),
244        }
245    }
246}
247
248// NodeData derives Eq + Ord and carries `Css` as its inline style. Provide
249// length-based ordering so the derives keep working — the same pattern the
250// previous `CssPropertyWithConditionsVec` used.
251impl Eq for Css {}
252// PartialOrd delegates to the length-based Ord so the two agree (the derived
253// field-wise PartialOrd diverged from this manual Ord).
254impl PartialOrd for Css {
255    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
256        Some(self.cmp(other))
257    }
258}
259impl Ord for Css {
260    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
261        self.rules.as_ref().len().cmp(&other.rules.as_ref().len())
262    }
263}
264impl Eq for CssRuleBlock {}
265impl Ord for CssRuleBlock {
266    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
267        // Match the existing PartialOrd: path first, then declarations.
268        // Priority is intentionally not in the sort key — it's a layer label,
269        // not a comparison primitive for callers.
270        self.path
271            .cmp(&other.path)
272            .then_with(|| self.declarations.cmp(&other.declarations))
273    }
274}
275
276/// Convert a flat list of `CssPropertyWithConditions` (the legacy inline-CSS form)
277/// into a `Css`. Each property becomes a single-declaration `CssRuleBlock` with
278/// `priority = INLINE`, an empty path (implicitly `:scope` — applies to the node it
279/// lives on), and the original conditions intact.
280///
281/// This bridge lets widget code that built `&[CssPropertyWithConditions]` arrays
282/// keep working through `.into()` while the storage on `NodeData` is the unified
283/// `Css` type.
284impl From<crate::dynamic_selector::CssPropertyWithConditionsVec> for Css {
285    fn from(props: crate::dynamic_selector::CssPropertyWithConditionsVec) -> Self {
286        // Build via an explicit push loop rather than `.into_iter().map(|p| CssRuleBlock {
287        // declarations: vec![...], ... }).collect()`. On the web/remill lift, constructing a
288        // complex struct with nested Vecs *inside* a mapped+collected closure drops every
289        // element (AzButton's inline container style came back with 0 rules even though the
290        // source Vec had props), whereas the identical construction in a plain loop body lifts
291        // correctly — same pattern `NodeData::add_css_property` already relies on. Native
292        // behavior is byte-identical.
293        let owned = props.into_library_owned_vec();
294        let mut rules: Vec<CssRuleBlock> = Vec::with_capacity(owned.len());
295        for p in owned {
296            rules.push(CssRuleBlock {
297                path: CssPath {
298                    selectors: Vec::new().into(),
299                },
300                declarations: alloc::vec![CssDeclaration::Static(p.property)].into(),
301                conditions: p.apply_if,
302                priority: rule_priority::INLINE,
303            });
304        }
305        Self {
306            rules: rules.into(),
307            keyframes: KeyframesVec::from_const_slice(&[]),
308        }
309    }
310}
311
312/// Contains one parsed `key: value` pair, static or dynamic
313#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
314#[repr(C, u8)]
315pub enum CssDeclaration {
316    /// Static key-value pair, such as `width: 500px`
317    Static(CssProperty),
318    /// Dynamic key-value pair with default value, such as `width: [[ my_id | 500px ]]`
319    Dynamic(DynamicCssProperty),
320}
321
322impl_option!(
323    CssDeclaration,
324    OptionCssDeclaration,
325    copy = false,
326    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
327);
328
329impl CssDeclaration {
330    #[must_use]
331    pub const fn new_static(prop: CssProperty) -> Self {
332        Self::Static(prop)
333    }
334
335    #[must_use]
336    pub const fn new_dynamic(prop: DynamicCssProperty) -> Self {
337        Self::Dynamic(prop)
338    }
339
340    /// Returns the type of the property (i.e. the CSS key as a typed enum)
341    #[must_use]
342    pub const fn get_type(&self) -> CssPropertyType {
343        use self::CssDeclaration::{Dynamic, Static};
344        match self {
345            Static(s) => s.get_type(),
346            Dynamic(d) => d.default_value.get_type(),
347        }
348    }
349
350    /// Determines if the property will be inherited (applied to the children)
351    /// during the recursive application of the style on the DOM tree
352    #[must_use]
353    pub const fn is_inheritable(&self) -> bool {
354        use self::CssDeclaration::{Dynamic, Static};
355        match self {
356            Static(s) => s.get_type().is_inheritable(),
357            Dynamic(d) => d.is_inheritable(),
358        }
359    }
360
361    /// Returns whether this rule affects only styling properties or layout
362    /// properties (that could trigger a re-layout)
363    #[must_use]
364    pub const fn can_trigger_relayout(&self) -> bool {
365        use self::CssDeclaration::{Dynamic, Static};
366        match self {
367            Static(s) => s.get_type().can_trigger_relayout(),
368            Dynamic(d) => d.can_trigger_relayout(),
369        }
370    }
371
372    #[must_use]
373    pub fn to_str(&self) -> String {
374        use self::CssDeclaration::{Dynamic, Static};
375        match self {
376            Static(s) => format!("{s:?}"),
377            Dynamic(d) => match self.env_variable() {
378                Some(v) => format!("env({}, {:?})", v.as_css_name(), d.default_value),
379                None => format!("var(--{}, {:?})", d.dynamic_id, d.default_value),
380            },
381        }
382    }
383
384    /// The `env()` variable this declaration reads, if it is one.
385    ///
386    /// An `env()` declaration is a `Dynamic` whose `dynamic_id` carries the
387    /// [`ENV_DYNAMIC_ID_PREFIX`](crate::dynamic_selector::ENV_DYNAMIC_ID_PREFIX);
388    /// its `default_value` is the parsed fallback. `None` for `Static` and
389    /// for a plain `var()` reference.
390    #[must_use]
391    pub fn env_variable(&self) -> Option<crate::dynamic_selector::EnvVariable> {
392        match self {
393            Self::Static(_) => None,
394            Self::Dynamic(d) => {
395                crate::dynamic_selector::EnvVariable::from_dynamic_id(d.dynamic_id.as_str())
396            }
397        }
398    }
399
400    /// Whether the cascade can turn this declaration into a concrete property:
401    /// every `Static`, plus `env()` references. A `var()` `Dynamic` is not
402    /// (it is substituted at parse time and never reaches the cascade).
403    #[must_use]
404    pub fn is_cascade_resolvable(&self) -> bool {
405        matches!(self, Self::Static(_)) || self.env_variable().is_some()
406    }
407
408    /// Whether this declaration's value depends on the window's
409    /// [`DynamicSelectorContext`](crate::dynamic_selector::DynamicSelectorContext)
410    /// - i.e. it is an `env()` - so a context change must re-run the cascade
411    /// for it, exactly as it must for a rule with `@media`-style conditions.
412    #[must_use]
413    pub fn depends_on_dynamic_context(&self) -> bool {
414        self.env_variable().is_some()
415    }
416
417    /// The concrete property this declaration contributes to the cascade
418    /// under `ctx`.
419    ///
420    /// - `Static` - the property itself.
421    /// - `env()` - the variable's live value (an absolute length parsed as
422    ///   the declared property's own type, so `padding-bottom` gets a
423    ///   padding and `top` gets an inset), or the parsed fallback when the
424    ///   platform reports none for it, or when there is no context yet (a
425    ///   `StyledDom` no window has adopted - the same rule conditional
426    ///   rule blocks follow).
427    /// - a `var()` `Dynamic` - `None`, matching the previous behaviour of
428    ///   every cascade site (they filtered on `Static`).
429    #[must_use]
430    pub fn resolve_in_cascade(
431        &self,
432        ctx: Option<&crate::dynamic_selector::DynamicSelectorContext>,
433    ) -> Option<CssProperty> {
434        match self {
435            Self::Static(s) => Some(s.clone()),
436            Self::Dynamic(d) => {
437                let var = self.env_variable()?;
438                let Some(px) = ctx.and_then(|c| var.resolve(c)) else {
439                    return Some(d.default_value.clone());
440                };
441                Some(Self::env_length_as(&d.default_value, px))
442            }
443        }
444    }
445
446    /// `px` (logical, absolute) re-typed as `like`'s property. Goes through
447    /// the property's own parser rather than a hand-written match over every
448    /// length-taking variant, so a new length property is covered the day it
449    /// gets a parser. Without the parser feature `env()` cannot be parsed in
450    /// the first place, so the fallback is the only value that can exist.
451    #[allow(unused_variables)]
452    fn env_length_as(like: &CssProperty, px: f32) -> CssProperty {
453        #[cfg(feature = "parser")]
454        {
455            if let Ok(p) =
456                crate::props::property::parse_css_property(like.get_type(), &format!("{px}px"))
457            {
458                return p;
459            }
460        }
461        like.clone()
462    }
463}
464
465/// A `DynamicCssProperty` is a type of css property that can be changed on possibly
466/// every frame by the Rust code - for example to implement an `On::Hover` behaviour.
467///
468/// The syntax for such a property looks like this:
469///
470/// ```no_run,ignore
471/// #my_div {
472///    padding: var(--my_dynamic_property_id, 400px);
473/// }
474/// ```
475///
476/// Azul will register a dynamic property with the key "`my_dynamic_property_id`"
477/// and the default value of 400px. If the property gets overridden during one frame,
478/// the overridden property takes precedence.
479///
480/// At runtime the style is immutable (which is a performance optimization - if we
481/// can assume that the property never changes at runtime), we can do some optimizations on it.
482/// Dynamic style properties can also be used for animations and conditional styles
483/// (i.e. `hover`, `focus`, etc.), thereby leading to cleaner code, since all of these
484/// special cases now use one single API.
485#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
486#[repr(C)]
487pub struct DynamicCssProperty {
488    /// The stringified ID of this property, i.e. the `"my_id"` in `width: var(--my_id, 500px)`.
489    pub dynamic_id: AzString,
490    /// Default values for this properties - one single value can control multiple properties!
491    pub default_value: CssProperty,
492}
493
494/// A value that is either heap-allocated (parsed at runtime) or a compile-time
495/// static reference.
496///
497/// Used to reduce enum size for large CSS property payloads
498/// by storing them behind a pointer instead of inline.
499///
500/// - Size: 1 (tag) + 7 (padding) + 8 (pointer) = **16 bytes** on 64-bit
501/// - `Static` variant: no allocation, just a `*const T` pointer to static data
502/// - `Boxed` variant: heap-allocated via `Box::into_raw`, freed on Drop
503#[repr(C, u8)]
504pub enum BoxOrStatic<T> {
505    /// Heap-allocated (parsed at runtime). Owned — freed on Drop.
506    Boxed(*mut T),
507    /// Compile-time constant (e.g. from `const` CSS defaults). Not freed.
508    Static(*const T),
509}
510
511impl<T> BoxOrStatic<T> {
512    /// Allocate `value` on the heap and return a `Boxed` variant.
513    #[inline]
514    pub fn heap(value: T) -> Self {
515        Self::Boxed(Box::into_raw(Box::new(value)))
516    }
517
518    /// Return a reference to the inner value.
519    ///
520    /// # Safety invariant
521    /// The inner pointer must be non-null. This is guaranteed by [`heap`](Self::heap)
522    /// and the `Static` constructor (which should always point to valid data).
523    #[inline]
524    #[must_use]
525    pub fn as_ref(&self) -> &T {
526        match self {
527            Self::Boxed(ptr) => unsafe {
528                debug_assert!(
529                    !ptr.is_null(),
530                    "BoxOrStatic::Boxed contained a null pointer"
531                );
532                &**ptr
533            },
534            Self::Static(ptr) => unsafe {
535                debug_assert!(
536                    !ptr.is_null(),
537                    "BoxOrStatic::Static contained a null pointer"
538                );
539                &**ptr
540            },
541        }
542    }
543
544    /// Return a mutable reference to the inner value (only for Boxed).
545    ///
546    /// # Panics
547    ///
548    /// Panics if called on a `Static` variant: static values are immutable
549    /// and cannot hand out a `&mut`.
550    #[inline]
551    pub fn as_mut(&mut self) -> &mut T {
552        match self {
553            Self::Boxed(ptr) => unsafe { &mut **ptr },
554            Self::Static(_) => panic!("Cannot mutate a static BoxOrStatic value"),
555        }
556    }
557
558    /// Consume self and return the inner value.
559    #[inline]
560    #[must_use]
561    pub fn into_inner(self) -> T
562    where
563        T: Clone,
564    {
565        // Clone the inner value, then let `self` drop normally so `Drop` frees the
566        // heap box (for the Boxed variant). The old `mem::forget(self)` LEAKED that
567        // box on every call — the clone is an independent value, so there is no
568        // double-free to guard against.
569        self.as_ref().clone()
570    }
571}
572
573impl<T> Drop for BoxOrStatic<T> {
574    fn drop(&mut self) {
575        if let Self::Boxed(ptr) = self {
576            if !ptr.is_null() {
577                unsafe {
578                    drop(Box::from_raw(*ptr));
579                }
580                *ptr = core::ptr::null_mut();
581            }
582        }
583    }
584}
585
586impl<T: Clone> Clone for BoxOrStatic<T> {
587    fn clone(&self) -> Self {
588        match self {
589            Self::Boxed(ptr) => {
590                let val = unsafe { &**ptr }.clone();
591                Self::Boxed(Box::into_raw(Box::new(val)))
592            }
593            Self::Static(ptr) => Self::Static(*ptr),
594        }
595    }
596}
597
598impl<T: fmt::Debug> fmt::Debug for BoxOrStatic<T> {
599    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
600        self.as_ref().fmt(f)
601    }
602}
603
604impl<T: fmt::Display> fmt::Display for BoxOrStatic<T> {
605    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606        self.as_ref().fmt(f)
607    }
608}
609
610impl<T: PartialEq> PartialEq for BoxOrStatic<T> {
611    fn eq(&self, other: &Self) -> bool {
612        self.as_ref() == other.as_ref()
613    }
614}
615
616impl<T: Eq> Eq for BoxOrStatic<T> {}
617
618impl<T: core::hash::Hash> core::hash::Hash for BoxOrStatic<T> {
619    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
620        self.as_ref().hash(state);
621    }
622}
623
624impl<T: PartialOrd> PartialOrd for BoxOrStatic<T> {
625    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
626        self.as_ref().partial_cmp(other.as_ref())
627    }
628}
629
630impl<T: Ord> Ord for BoxOrStatic<T> {
631    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
632        self.as_ref().cmp(other.as_ref())
633    }
634}
635
636impl<T> core::ops::Deref for BoxOrStatic<T> {
637    type Target = T;
638    #[inline]
639    fn deref(&self) -> &T {
640        self.as_ref()
641    }
642}
643
644impl<T: Default> Default for BoxOrStatic<T> {
645    fn default() -> Self {
646        Self::heap(T::default())
647    }
648}
649
650impl<T: PrintAsCssValue> PrintAsCssValue for BoxOrStatic<T> {
651    fn print_as_css_value(&self) -> String {
652        self.as_ref().print_as_css_value()
653    }
654}
655
656// Safety: BoxOrStatic<T> is Send if T is Send
657unsafe impl<T: Send + 'static> Send for BoxOrStatic<T> {}
658// Safety: BoxOrStatic<T> is Sync if T is Sync
659unsafe impl<T: Sync + 'static> Sync for BoxOrStatic<T> {}
660
661/// Type alias: `BoxOrStatic<StyleBoxShadow>` — used by codegen for FFI monomorphization.
662pub type BoxOrStaticStyleBoxShadow = BoxOrStatic<crate::props::style::box_shadow::StyleBoxShadow>;
663
664/// Type alias: `BoxOrStatic<AzString>` — used by `NodeType::Text` and `NodeType::Icon`.
665pub type BoxOrStaticString = BoxOrStatic<AzString>;
666
667/// A CSS property value that may be an explicit value or a CSS-wide keyword
668/// (`auto`, `none`, `initial`, `inherit`, `revert`, `unset`).
669#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
670#[repr(C, u8)] // necessary for ABI stability
671pub enum CssPropertyValue<T> {
672    Auto,
673    None,
674    Initial,
675    Inherit,
676    Revert,
677    Unset,
678    Exact(T),
679}
680
681/// Trait for types that can format themselves as a CSS property value string.
682pub trait PrintAsCssValue {
683    fn print_as_css_value(&self) -> String;
684}
685
686impl<T: PrintAsCssValue> CssPropertyValue<T> {
687    pub fn get_css_value_fmt(&self) -> String {
688        match self {
689            Self::Auto => "auto".to_string(),
690            Self::None => "none".to_string(),
691            Self::Initial => "initial".to_string(),
692            Self::Inherit => "inherit".to_string(),
693            Self::Revert => "revert".to_string(),
694            Self::Unset => "unset".to_string(),
695            Self::Exact(e) => e.print_as_css_value(),
696        }
697    }
698}
699
700impl<T: fmt::Display> fmt::Display for CssPropertyValue<T> {
701    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
702        use self::CssPropertyValue::{Auto, Exact, Inherit, Initial, None, Revert, Unset};
703        match self {
704            Auto => write!(f, "auto"),
705            None => write!(f, "none"),
706            Initial => write!(f, "initial"),
707            Inherit => write!(f, "inherit"),
708            Revert => write!(f, "revert"),
709            Unset => write!(f, "unset"),
710            Exact(e) => write!(f, "{e}"),
711        }
712    }
713}
714
715impl<T> From<T> for CssPropertyValue<T> {
716    fn from(c: T) -> Self {
717        Self::Exact(c)
718    }
719}
720
721impl<T> CssPropertyValue<T> {
722    /// Transforms a `CssPropertyValue<T>` into a `CssPropertyValue<U>` by applying a mapping
723    /// function
724    #[inline]
725    pub fn map_property<F: Fn(T) -> U, U>(self, map_fn: F) -> CssPropertyValue<U> {
726        match self {
727            Self::Exact(c) => CssPropertyValue::Exact(map_fn(c)),
728            Self::Auto => CssPropertyValue::Auto,
729            Self::None => CssPropertyValue::None,
730            Self::Initial => CssPropertyValue::Initial,
731            Self::Inherit => CssPropertyValue::Inherit,
732            Self::Revert => CssPropertyValue::Revert,
733            Self::Unset => CssPropertyValue::Unset,
734        }
735    }
736
737    #[inline]
738    pub const fn get_property(&self) -> Option<&T> {
739        match self {
740            Self::Exact(c) => Some(c),
741            _ => None,
742        }
743    }
744
745    #[inline]
746    pub fn get_property_owned(self) -> Option<T> {
747        match self {
748            Self::Exact(c) => Some(c),
749            _ => None,
750        }
751    }
752
753    #[inline]
754    pub const fn is_auto(&self) -> bool {
755        matches!(self, Self::Auto)
756    }
757
758    #[inline]
759    pub const fn is_none(&self) -> bool {
760        matches!(self, Self::None)
761    }
762
763    #[inline]
764    pub const fn is_initial(&self) -> bool {
765        matches!(self, Self::Initial)
766    }
767
768    #[inline]
769    pub const fn is_inherit(&self) -> bool {
770        matches!(self, Self::Inherit)
771    }
772
773    #[inline]
774    pub const fn is_revert(&self) -> bool {
775        matches!(self, Self::Revert)
776    }
777
778    #[inline]
779    pub const fn is_unset(&self) -> bool {
780        matches!(self, Self::Unset)
781    }
782}
783
784impl<T: Default> CssPropertyValue<T> {
785    #[inline]
786    pub fn get_property_or_default(self) -> Option<T> {
787        match self {
788            Self::Auto | Self::Initial => Some(T::default()),
789            Self::Exact(c) => Some(c),
790            Self::None | Self::Inherit | Self::Revert | Self::Unset => None,
791        }
792    }
793}
794
795impl<T: Default> Default for CssPropertyValue<T> {
796    #[inline]
797    fn default() -> Self {
798        Self::Exact(T::default())
799    }
800}
801
802impl DynamicCssProperty {
803    #[must_use]
804    pub const fn is_inheritable(&self) -> bool {
805        // Dynamic style properties should not be inheritable,
806        // since that could lead to bugs - you set a property in Rust, suddenly
807        // the wrong UI component starts to react because it was inherited.
808        false
809    }
810
811    #[must_use]
812    pub const fn can_trigger_relayout(&self) -> bool {
813        self.default_value.get_type().can_trigger_relayout()
814    }
815}
816
817/// Layer priority for `CssRuleBlock`. Lower numbers cascade first;
818/// higher numbers override earlier layers at the same specificity.
819///
820/// `u8` leaves 256 slots, so a new layer can be inserted between any
821/// two existing slots without renumbering consumers. The gaps between
822/// named slots are intentional — fill them with custom intermediate
823/// layers if/when `@layer` lands.
824pub mod rule_priority {
825    /// User-Agent / framework defaults. Widget code that emits its
826    /// own default CSS uses this. Lowest priority — anything else
827    /// overrides it.
828    pub const UA: u8 = 0;
829
830    /// Stylesheets the host system reports (system fonts, theme CSS
831    /// derived from `SystemStyle`). One step above UA so they win
832    /// against framework defaults but lose against anything the app
833    /// author writes.
834    pub const SYSTEM: u8 = 10;
835
836    /// Default for parser-produced rules: the app author's CSS.
837    /// Everything coming out of `Css::from_string` lives here.
838    pub const AUTHOR: u8 = 20;
839
840    /// Inline `style="..."` / `NodeData::set_css(...)` rules — used
841    /// once the inline-vs-component unification (separate plan) folds
842    /// inline storage into the same Vec.
843    pub const INLINE: u8 = 30;
844
845    /// Reserved for direct-rule runtime overrides.
846    ///
847    /// Today the
848    /// `prop_cache` handles runtime overrides via
849    /// `user_overridden_properties`; this slot is reserved so a
850    /// future "push a `CssRuleBlock` at runtime" path stays above
851    /// inline. Used only when a callback writes a full rule, not a
852    /// single property.
853    pub const RUNTIME: u8 = 50;
854}
855
856/// One block of rules that applies a bunch of rules to a "path" in the style, i.e.
857/// `div#myid.myclass -> { ("justify-content", "center") }`
858///
859/// The `conditions` field contains @media/@lang/etc. conditions that must ALL be
860/// satisfied for this rule block to apply (from enclosing @-rule blocks).
861#[derive(Debug, Default, Clone, PartialEq)]
862#[repr(C)]
863pub struct CssRuleBlock {
864    /// The css path (full selector) of the style ruleset
865    pub path: CssPath,
866    /// `"justify-content: center"` =>
867    /// `CssDeclaration::Static(CssProperty::JustifyContent(LayoutJustifyContent::Center))`
868    pub declarations: CssDeclarationVec,
869    /// Conditions from enclosing @-rules (@media, @lang, etc.) that must ALL be
870    /// satisfied for this rule block to apply. Empty = unconditional.
871    pub conditions: DynamicSelectorVec,
872    /// Layer priority. See [`rule_priority`] for slot allocation.
873    /// `0` = UA / framework, `20` = author CSS (default), higher = wins.
874    /// Sort key combined with selector specificity in `sort_by_specificity`.
875    pub priority: u8,
876}
877
878impl_option!(
879    CssRuleBlock,
880    OptionCssRuleBlock,
881    copy = false,
882    [Debug, Clone, PartialEq, Eq, PartialOrd]
883);
884
885impl CssRuleBlock {
886    /// Whether this rule's contribution to the cascade depends on the
887    /// window's `DynamicSelectorContext`: it has `@media`/`@os`-style
888    /// conditions, or one of its declarations is an `env()`. The cascade is
889    /// re-run for such rules when the context changes
890    /// (`StyledDom::set_dynamic_selector_context`).
891    #[must_use]
892    pub fn depends_on_dynamic_context(&self) -> bool {
893        !self.conditions.as_ref().is_empty()
894            || self
895                .declarations
896                .as_ref()
897                .iter()
898                .any(CssDeclaration::depends_on_dynamic_context)
899    }
900}
901
902impl PartialOrd for CssRuleBlock {
903    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
904        // Compare by path and declarations only, conditions are not ordered
905        match self.path.partial_cmp(&other.path) {
906            Some(core::cmp::Ordering::Equal) => self.declarations.partial_cmp(&other.declarations),
907            ord => ord,
908        }
909    }
910}
911
912impl_vec!(
913    CssDeclaration,
914    CssDeclarationVec,
915    CssDeclarationVecDestructor,
916    CssDeclarationVecDestructorType,
917    CssDeclarationVecSlice,
918    OptionCssDeclaration
919);
920impl_vec_mut!(CssDeclaration, CssDeclarationVec);
921impl_vec_debug!(CssDeclaration, CssDeclarationVec);
922impl_vec_partialord!(CssDeclaration, CssDeclarationVec);
923impl_vec_ord!(CssDeclaration, CssDeclarationVec);
924impl_vec_clone!(
925    CssDeclaration,
926    CssDeclarationVec,
927    CssDeclarationVecDestructor
928);
929impl_vec_partialeq!(CssDeclaration, CssDeclarationVec);
930impl_vec_eq!(CssDeclaration, CssDeclarationVec);
931impl_vec_hash!(CssDeclaration, CssDeclarationVec);
932
933impl CssRuleBlock {
934    #[must_use]
935    pub fn new(path: CssPath, declarations: Vec<CssDeclaration>) -> Self {
936        Self {
937            path,
938            declarations: declarations.into(),
939            conditions: DynamicSelectorVec::from_const_slice(&[]),
940            priority: rule_priority::AUTHOR,
941        }
942    }
943
944    #[must_use]
945    pub fn with_conditions(
946        path: CssPath,
947        declarations: Vec<CssDeclaration>,
948        conditions: Vec<crate::dynamic_selector::DynamicSelector>,
949    ) -> Self {
950        Self {
951            path,
952            declarations: declarations.into(),
953            conditions: conditions.into(),
954            priority: rule_priority::AUTHOR,
955        }
956    }
957}
958
959/// A group of CSS path selectors, used during selector matching.
960pub type CssContentGroup<'a> = Vec<&'a CssPathSelector>;
961
962/// Signifies the type of a DOM node without carrying any associated data
963#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
964#[repr(C)]
965pub enum NodeTypeTag {
966    // Document structure
967    Html,
968    Head,
969    Body,
970
971    // Block-level elements
972    Div,
973    P,
974    Article,
975    Section,
976    Nav,
977    Aside,
978    Header,
979    Footer,
980    Main,
981    Figure,
982    FigCaption,
983
984    // Headings
985    H1,
986    H2,
987    H3,
988    H4,
989    H5,
990    H6,
991
992    // Inline text
993    Br,
994    Hr,
995    Pre,
996    BlockQuote,
997    Address,
998    Details,
999    Summary,
1000    Dialog,
1001
1002    // Lists
1003    Ul,
1004    Ol,
1005    Li,
1006    Dl,
1007    Dt,
1008    Dd,
1009    Menu,
1010    MenuItem,
1011    Dir,
1012
1013    // Tables
1014    Table,
1015    Caption,
1016    THead,
1017    TBody,
1018    TFoot,
1019    Tr,
1020    Th,
1021    Td,
1022    ColGroup,
1023    Col,
1024
1025    // Forms
1026    Form,
1027    FieldSet,
1028    Legend,
1029    Label,
1030    Input,
1031    Button,
1032    Select,
1033    OptGroup,
1034    SelectOption,
1035    TextArea,
1036    Output,
1037    Progress,
1038    Meter,
1039    DataList,
1040
1041    // Inline elements
1042    Span,
1043    A,
1044    Em,
1045    Strong,
1046    B,
1047    I,
1048    U,
1049    S,
1050    Mark,
1051    Del,
1052    Ins,
1053    Code,
1054    Samp,
1055    Kbd,
1056    Var,
1057    Cite,
1058    Dfn,
1059    Abbr,
1060    Acronym,
1061    Q,
1062    Time,
1063    Sub,
1064    Sup,
1065    Small,
1066    Big,
1067    Bdo,
1068    Bdi,
1069    Wbr,
1070    Ruby,
1071    Rt,
1072    Rtc,
1073    Rp,
1074    Data,
1075
1076    // Embedded content
1077    Canvas,
1078    Object,
1079    Param,
1080    Embed,
1081    Audio,
1082    Video,
1083    Source,
1084    Track,
1085    Map,
1086    Area,
1087    Svg,
1088    /// SVG `<path>` element.
1089    SvgPath,
1090    /// SVG `<circle>` element.
1091    SvgCircle,
1092    /// SVG `<rect>` element.
1093    SvgRect,
1094    /// SVG `<ellipse>` element.
1095    SvgEllipse,
1096    /// SVG `<line>` element.
1097    SvgLine,
1098    /// SVG `<polygon>` element.
1099    SvgPolygon,
1100    /// SVG `<polyline>` element.
1101    SvgPolyline,
1102    /// SVG `<g>` group element.
1103    SvgG,
1104
1105    // SVG container elements
1106    /// SVG `<defs>` element.
1107    SvgDefs,
1108    /// SVG `<symbol>` element.
1109    SvgSymbol,
1110    /// SVG `<use>` element.
1111    SvgUse,
1112    /// SVG `<switch>` element.
1113    SvgSwitch,
1114
1115    // SVG text elements
1116    /// SVG `<text>` element.
1117    SvgText,
1118    /// SVG `<tspan>` element.
1119    SvgTspan,
1120    /// SVG `<textPath>` element.
1121    SvgTextPath,
1122
1123    // SVG paint server elements
1124    /// SVG `<linearGradient>` element.
1125    SvgLinearGradient,
1126    /// SVG `<radialGradient>` element.
1127    SvgRadialGradient,
1128    /// SVG `<stop>` element.
1129    SvgStop,
1130    /// SVG `<pattern>` element.
1131    SvgPattern,
1132
1133    // SVG clipping/masking elements
1134    /// SVG `<clipPath>` element.
1135    SvgClipPathElement,
1136    /// SVG `<mask>` element.
1137    SvgMask,
1138
1139    // SVG filter elements
1140    /// SVG `<filter>` element.
1141    SvgFilter,
1142    /// SVG `<feBlend>` element.
1143    SvgFeBlend,
1144    /// SVG `<feColorMatrix>` element.
1145    SvgFeColorMatrix,
1146    /// SVG `<feComponentTransfer>` element.
1147    SvgFeComponentTransfer,
1148    /// SVG `<feComposite>` element.
1149    SvgFeComposite,
1150    /// SVG `<feConvolveMatrix>` element.
1151    SvgFeConvolveMatrix,
1152    /// SVG `<feDiffuseLighting>` element.
1153    SvgFeDiffuseLighting,
1154    /// SVG `<feDisplacementMap>` element.
1155    SvgFeDisplacementMap,
1156    /// SVG `<feDistantLight>` element.
1157    SvgFeDistantLight,
1158    /// SVG `<feDropShadow>` element.
1159    SvgFeDropShadow,
1160    /// SVG `<feFlood>` element.
1161    SvgFeFlood,
1162    /// SVG `<feFuncR>` element.
1163    SvgFeFuncR,
1164    /// SVG `<feFuncG>` element.
1165    SvgFeFuncG,
1166    /// SVG `<feFuncB>` element.
1167    SvgFeFuncB,
1168    /// SVG `<feFuncA>` element.
1169    SvgFeFuncA,
1170    /// SVG `<feGaussianBlur>` element.
1171    SvgFeGaussianBlur,
1172    /// SVG `<feImage>` element.
1173    SvgFeImage,
1174    /// SVG `<feMerge>` element.
1175    SvgFeMerge,
1176    /// SVG `<feMergeNode>` element.
1177    SvgFeMergeNode,
1178    /// SVG `<feMorphology>` element.
1179    SvgFeMorphology,
1180    /// SVG `<feOffset>` element.
1181    SvgFeOffset,
1182    /// SVG `<fePointLight>` element.
1183    SvgFePointLight,
1184    /// SVG `<feSpecularLighting>` element.
1185    SvgFeSpecularLighting,
1186    /// SVG `<feSpotLight>` element.
1187    SvgFeSpotLight,
1188    /// SVG `<feTile>` element.
1189    SvgFeTile,
1190    /// SVG `<feTurbulence>` element.
1191    SvgFeTurbulence,
1192
1193    // SVG marker/image elements
1194    /// SVG `<marker>` element.
1195    SvgMarker,
1196    /// SVG `<image>` element.
1197    SvgImage,
1198    /// SVG `<foreignObject>` element.
1199    SvgForeignObject,
1200
1201    // SVG descriptive elements
1202    /// SVG `<title>` element.
1203    SvgTitle,
1204    /// SVG `<desc>` element.
1205    SvgDesc,
1206    /// SVG `<metadata>` element.
1207    SvgMetadata,
1208    /// SVG `<a>` element.
1209    SvgA,
1210    /// SVG `<view>` element.
1211    SvgView,
1212    /// SVG `<style>` element.
1213    SvgStyle,
1214    /// SVG `<script>` element.
1215    SvgScript,
1216
1217    // SVG animation elements
1218    /// SVG `<animate>` element.
1219    SvgAnimate,
1220    /// SVG `<animateMotion>` element.
1221    SvgAnimateMotion,
1222    /// SVG `<animateTransform>` element.
1223    SvgAnimateTransform,
1224    /// SVG `<set>` element.
1225    SvgSet,
1226    /// SVG `<mpath>` element.
1227    SvgMpath,
1228
1229    // Metadata
1230    Title,
1231    Meta,
1232    Link,
1233    Script,
1234    Style,
1235    Base,
1236
1237    // Special
1238    Text,
1239    Img,
1240    VirtualView,
1241    /// `<transient-window>` — a popup that is a real OS window.
1242    TransientWindow,
1243    /// Icon element - resolved to actual content by `IconProvider`
1244    Icon,
1245    /// Invisible probe — `NodeType::GeolocationProbe`. Zero-size in
1246    /// layout, skipped in the display list. CSS tag: `geolocation-probe`.
1247    GeolocationProbe,
1248
1249    // Pseudo-elements
1250    Before,
1251    After,
1252    Marker,
1253    Placeholder,
1254
1255    /// THE canonical page-break element (`<pagebreak/>` /
1256    /// `Dom::create_page_break()`): an empty block with UA
1257    /// `break-before: page`. CSS tag: `pagebreak`.
1258    PageBreak,
1259}
1260
1261/// Error returned when a CSS tag name string cannot be mapped to a [`NodeTypeTag`].
1262#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1263pub enum NodeTypeTagParseError<'a> {
1264    Invalid(&'a str),
1265}
1266
1267impl fmt::Display for NodeTypeTagParseError<'_> {
1268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1269        match &self {
1270            NodeTypeTagParseError::Invalid(e) => write!(f, "Invalid node type: {e}"),
1271        }
1272    }
1273}
1274
1275/// Owned version of [`NodeTypeTagParseError`] for storage across lifetime boundaries.
1276#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1277#[repr(C, u8)]
1278pub enum NodeTypeTagParseErrorOwned {
1279    Invalid(AzString),
1280}
1281
1282impl NodeTypeTagParseError<'_> {
1283    #[must_use]
1284    pub fn to_contained(&self) -> NodeTypeTagParseErrorOwned {
1285        match self {
1286            NodeTypeTagParseError::Invalid(s) => {
1287                NodeTypeTagParseErrorOwned::Invalid((*s).to_string().into())
1288            }
1289        }
1290    }
1291}
1292
1293impl NodeTypeTagParseErrorOwned {
1294    #[must_use]
1295    pub fn to_shared(&self) -> NodeTypeTagParseError<'_> {
1296        match self {
1297            Self::Invalid(s) => NodeTypeTagParseError::Invalid(s),
1298        }
1299    }
1300}
1301
1302/// Parses the node type from a CSS string such as `"div"` => `NodeTypeTag::Div`
1303impl NodeTypeTag {
1304    #[allow(clippy::too_many_lines)]
1305    // large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
1306    /// # Errors
1307    ///
1308    /// Returns an error if `css_key` is not a recognized HTML node-type tag.
1309    pub fn from_str(css_key: &str) -> Result<Self, NodeTypeTagParseError<'_>> {
1310        match css_key {
1311            // Document structure
1312            "html" => Ok(Self::Html),
1313            "head" => Ok(Self::Head),
1314            "body" => Ok(Self::Body),
1315
1316            // Block-level elements
1317            "div" => Ok(Self::Div),
1318            "p" => Ok(Self::P),
1319            "article" => Ok(Self::Article),
1320            "section" => Ok(Self::Section),
1321            "nav" => Ok(Self::Nav),
1322            "aside" => Ok(Self::Aside),
1323            "header" => Ok(Self::Header),
1324            "footer" => Ok(Self::Footer),
1325            "main" => Ok(Self::Main),
1326            "figure" => Ok(Self::Figure),
1327            "figcaption" => Ok(Self::FigCaption),
1328
1329            // Headings
1330            "h1" => Ok(Self::H1),
1331            "h2" => Ok(Self::H2),
1332            "h3" => Ok(Self::H3),
1333            "h4" => Ok(Self::H4),
1334            "h5" => Ok(Self::H5),
1335            "h6" => Ok(Self::H6),
1336
1337            // Inline text
1338            "br" => Ok(Self::Br),
1339            "hr" => Ok(Self::Hr),
1340            "pre" => Ok(Self::Pre),
1341            "blockquote" => Ok(Self::BlockQuote),
1342            "address" => Ok(Self::Address),
1343            "details" => Ok(Self::Details),
1344            "summary" => Ok(Self::Summary),
1345            "dialog" => Ok(Self::Dialog),
1346
1347            // Lists
1348            "ul" => Ok(Self::Ul),
1349            "ol" => Ok(Self::Ol),
1350            "li" => Ok(Self::Li),
1351            "dl" => Ok(Self::Dl),
1352            "dt" => Ok(Self::Dt),
1353            "dd" => Ok(Self::Dd),
1354            "menu" => Ok(Self::Menu),
1355            "menuitem" => Ok(Self::MenuItem),
1356            "dir" => Ok(Self::Dir),
1357
1358            // Tables
1359            "table" => Ok(Self::Table),
1360            "caption" => Ok(Self::Caption),
1361            "thead" => Ok(Self::THead),
1362            "tbody" => Ok(Self::TBody),
1363            "tfoot" => Ok(Self::TFoot),
1364            "tr" => Ok(Self::Tr),
1365            "th" => Ok(Self::Th),
1366            "td" => Ok(Self::Td),
1367            "colgroup" => Ok(Self::ColGroup),
1368            "col" => Ok(Self::Col),
1369
1370            // Forms
1371            "form" => Ok(Self::Form),
1372            "fieldset" => Ok(Self::FieldSet),
1373            "legend" => Ok(Self::Legend),
1374            "label" => Ok(Self::Label),
1375            "input" => Ok(Self::Input),
1376            "button" => Ok(Self::Button),
1377            "select" => Ok(Self::Select),
1378            "optgroup" => Ok(Self::OptGroup),
1379            "option" => Ok(Self::SelectOption),
1380            "textarea" => Ok(Self::TextArea),
1381            "output" => Ok(Self::Output),
1382            "progress" => Ok(Self::Progress),
1383            "meter" => Ok(Self::Meter),
1384            "datalist" => Ok(Self::DataList),
1385
1386            // Inline elements
1387            "span" => Ok(Self::Span),
1388            "a" => Ok(Self::A),
1389            "em" => Ok(Self::Em),
1390            "strong" => Ok(Self::Strong),
1391            "b" => Ok(Self::B),
1392            "i" => Ok(Self::I),
1393            "u" => Ok(Self::U),
1394            "s" => Ok(Self::S),
1395            "mark" => Ok(Self::Mark),
1396            "del" => Ok(Self::Del),
1397            "ins" => Ok(Self::Ins),
1398            "code" => Ok(Self::Code),
1399            "samp" => Ok(Self::Samp),
1400            "kbd" => Ok(Self::Kbd),
1401            "var" => Ok(Self::Var),
1402            "cite" => Ok(Self::Cite),
1403            "dfn" => Ok(Self::Dfn),
1404            "abbr" => Ok(Self::Abbr),
1405            "acronym" => Ok(Self::Acronym),
1406            "q" => Ok(Self::Q),
1407            "time" => Ok(Self::Time),
1408            "sub" => Ok(Self::Sub),
1409            "sup" => Ok(Self::Sup),
1410            "small" => Ok(Self::Small),
1411            "big" => Ok(Self::Big),
1412            "bdo" => Ok(Self::Bdo),
1413            "bdi" => Ok(Self::Bdi),
1414            "wbr" => Ok(Self::Wbr),
1415            "ruby" => Ok(Self::Ruby),
1416            "rt" => Ok(Self::Rt),
1417            "rtc" => Ok(Self::Rtc),
1418            "rp" => Ok(Self::Rp),
1419            "data" => Ok(Self::Data),
1420
1421            // Embedded content
1422            "canvas" => Ok(Self::Canvas),
1423            "object" => Ok(Self::Object),
1424            "param" => Ok(Self::Param),
1425            "embed" => Ok(Self::Embed),
1426            "audio" => Ok(Self::Audio),
1427            "video" => Ok(Self::Video),
1428            "source" => Ok(Self::Source),
1429            "track" => Ok(Self::Track),
1430            "map" => Ok(Self::Map),
1431            "area" => Ok(Self::Area),
1432            "svg" => Ok(Self::Svg),
1433
1434            // SVG shape elements
1435            "path" => Ok(Self::SvgPath),
1436            "circle" => Ok(Self::SvgCircle),
1437            "rect" => Ok(Self::SvgRect),
1438            "ellipse" => Ok(Self::SvgEllipse),
1439            "line" => Ok(Self::SvgLine),
1440            "polygon" => Ok(Self::SvgPolygon),
1441            "polyline" => Ok(Self::SvgPolyline),
1442            "g" => Ok(Self::SvgG),
1443
1444            // SVG container elements
1445            "defs" => Ok(Self::SvgDefs),
1446            "symbol" => Ok(Self::SvgSymbol),
1447            "use" => Ok(Self::SvgUse),
1448            "switch" => Ok(Self::SvgSwitch),
1449
1450            // SVG text elements
1451            "svg:text" => Ok(Self::SvgText),
1452            "tspan" => Ok(Self::SvgTspan),
1453            "textpath" => Ok(Self::SvgTextPath),
1454
1455            // SVG paint server elements
1456            "lineargradient" => Ok(Self::SvgLinearGradient),
1457            "radialgradient" => Ok(Self::SvgRadialGradient),
1458            "stop" => Ok(Self::SvgStop),
1459            "pattern" => Ok(Self::SvgPattern),
1460
1461            // SVG clipping/masking elements
1462            "clippath" => Ok(Self::SvgClipPathElement),
1463            "mask" => Ok(Self::SvgMask),
1464
1465            // SVG filter elements
1466            "filter" => Ok(Self::SvgFilter),
1467            "feblend" => Ok(Self::SvgFeBlend),
1468            "fecolormatrix" => Ok(Self::SvgFeColorMatrix),
1469            "fecomponenttransfer" => Ok(Self::SvgFeComponentTransfer),
1470            "fecomposite" => Ok(Self::SvgFeComposite),
1471            "feconvolvematrix" => Ok(Self::SvgFeConvolveMatrix),
1472            "fediffuselighting" => Ok(Self::SvgFeDiffuseLighting),
1473            "fedisplacementmap" => Ok(Self::SvgFeDisplacementMap),
1474            "fedistantlight" => Ok(Self::SvgFeDistantLight),
1475            "fedropshadow" => Ok(Self::SvgFeDropShadow),
1476            "feflood" => Ok(Self::SvgFeFlood),
1477            "fefuncr" => Ok(Self::SvgFeFuncR),
1478            "fefuncg" => Ok(Self::SvgFeFuncG),
1479            "fefuncb" => Ok(Self::SvgFeFuncB),
1480            "fefunca" => Ok(Self::SvgFeFuncA),
1481            "fegaussianblur" => Ok(Self::SvgFeGaussianBlur),
1482            "feimage" => Ok(Self::SvgFeImage),
1483            "femerge" => Ok(Self::SvgFeMerge),
1484            "femergenode" => Ok(Self::SvgFeMergeNode),
1485            "femorphology" => Ok(Self::SvgFeMorphology),
1486            "feoffset" => Ok(Self::SvgFeOffset),
1487            "fepointlight" => Ok(Self::SvgFePointLight),
1488            "fespecularlighting" => Ok(Self::SvgFeSpecularLighting),
1489            "fespotlight" => Ok(Self::SvgFeSpotLight),
1490            "fetile" => Ok(Self::SvgFeTile),
1491            "feturbulence" => Ok(Self::SvgFeTurbulence),
1492
1493            // SVG marker/image elements
1494            "image" | "svg:image" => Ok(Self::SvgImage),
1495            "svg:marker" => Ok(Self::SvgMarker),
1496            "foreignobject" => Ok(Self::SvgForeignObject),
1497
1498            // SVG descriptive elements
1499            "svg:title" => Ok(Self::SvgTitle),
1500            "svg:a" => Ok(Self::SvgA),
1501            "svg:style" => Ok(Self::SvgStyle),
1502            "svg:script" => Ok(Self::SvgScript),
1503            "desc" => Ok(Self::SvgDesc),
1504            "metadata" => Ok(Self::SvgMetadata),
1505            "view" => Ok(Self::SvgView),
1506
1507            // SVG animation elements
1508            "animate" => Ok(Self::SvgAnimate),
1509            "animatemotion" => Ok(Self::SvgAnimateMotion),
1510            "animatetransform" => Ok(Self::SvgAnimateTransform),
1511            "set" => Ok(Self::SvgSet),
1512            "mpath" => Ok(Self::SvgMpath),
1513
1514            // Metadata
1515            "title" => Ok(Self::Title),
1516            "meta" => Ok(Self::Meta),
1517            "link" => Ok(Self::Link),
1518            "script" => Ok(Self::Script),
1519            "style" => Ok(Self::Style),
1520            "base" => Ok(Self::Base),
1521
1522            // Special
1523            "text" => Ok(Self::Text), // Display emits "text"; from_str must accept it back
1524            "img" => Ok(Self::Img),
1525            "virtual-view" | "iframe" => Ok(Self::VirtualView),
1526            "transient-window" => Ok(Self::TransientWindow),
1527            "icon" => Ok(Self::Icon),
1528            "geolocation-probe" => Ok(Self::GeolocationProbe),
1529            "pagebreak" => Ok(Self::PageBreak),
1530
1531            // Pseudo-elements (usually prefixed with ::)
1532            "before" | "::before" => Ok(Self::Before),
1533            "after" | "::after" => Ok(Self::After),
1534            "marker" | "::marker" => Ok(Self::Marker),
1535            "placeholder" | "::placeholder" => Ok(Self::Placeholder),
1536
1537            other => Err(NodeTypeTagParseError::Invalid(other)),
1538        }
1539    }
1540}
1541
1542impl fmt::Display for NodeTypeTag {
1543    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
1544    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1545        match self {
1546            // Document structure
1547            Self::Html => write!(f, "html"),
1548            Self::Head => write!(f, "head"),
1549            Self::Body => write!(f, "body"),
1550
1551            // Block elements
1552            Self::Div => write!(f, "div"),
1553            Self::P => write!(f, "p"),
1554            Self::Article => write!(f, "article"),
1555            Self::Section => write!(f, "section"),
1556            Self::Nav => write!(f, "nav"),
1557            Self::Aside => write!(f, "aside"),
1558            Self::Header => write!(f, "header"),
1559            Self::Footer => write!(f, "footer"),
1560            Self::Main => write!(f, "main"),
1561            Self::Figure => write!(f, "figure"),
1562            Self::FigCaption => write!(f, "figcaption"),
1563
1564            // Headings
1565            Self::H1 => write!(f, "h1"),
1566            Self::H2 => write!(f, "h2"),
1567            Self::H3 => write!(f, "h3"),
1568            Self::H4 => write!(f, "h4"),
1569            Self::H5 => write!(f, "h5"),
1570            Self::H6 => write!(f, "h6"),
1571
1572            // Text formatting
1573            Self::Br => write!(f, "br"),
1574            Self::Hr => write!(f, "hr"),
1575            Self::Pre => write!(f, "pre"),
1576            Self::BlockQuote => write!(f, "blockquote"),
1577            Self::Address => write!(f, "address"),
1578            Self::Details => write!(f, "details"),
1579            Self::Summary => write!(f, "summary"),
1580            Self::Dialog => write!(f, "dialog"),
1581
1582            // List elements
1583            Self::Ul => write!(f, "ul"),
1584            Self::Ol => write!(f, "ol"),
1585            Self::Li => write!(f, "li"),
1586            Self::Dl => write!(f, "dl"),
1587            Self::Dt => write!(f, "dt"),
1588            Self::Dd => write!(f, "dd"),
1589            Self::Menu => write!(f, "menu"),
1590            Self::MenuItem => write!(f, "menuitem"),
1591            Self::Dir => write!(f, "dir"),
1592
1593            // Table elements
1594            Self::Table => write!(f, "table"),
1595            Self::Caption => write!(f, "caption"),
1596            Self::THead => write!(f, "thead"),
1597            Self::TBody => write!(f, "tbody"),
1598            Self::TFoot => write!(f, "tfoot"),
1599            Self::Tr => write!(f, "tr"),
1600            Self::Th => write!(f, "th"),
1601            Self::Td => write!(f, "td"),
1602            Self::ColGroup => write!(f, "colgroup"),
1603            Self::Col => write!(f, "col"),
1604
1605            // Form elements
1606            Self::Form => write!(f, "form"),
1607            Self::FieldSet => write!(f, "fieldset"),
1608            Self::Legend => write!(f, "legend"),
1609            Self::Label => write!(f, "label"),
1610            Self::Input => write!(f, "input"),
1611            Self::Button => write!(f, "button"),
1612            Self::Select => write!(f, "select"),
1613            Self::OptGroup => write!(f, "optgroup"),
1614            Self::SelectOption => write!(f, "option"),
1615            Self::TextArea => write!(f, "textarea"),
1616            Self::Output => write!(f, "output"),
1617            Self::Progress => write!(f, "progress"),
1618            Self::Meter => write!(f, "meter"),
1619            Self::DataList => write!(f, "datalist"),
1620
1621            // Inline elements
1622            Self::Span => write!(f, "span"),
1623            Self::A => write!(f, "a"),
1624            Self::Em => write!(f, "em"),
1625            Self::Strong => write!(f, "strong"),
1626            Self::B => write!(f, "b"),
1627            Self::I => write!(f, "i"),
1628            Self::U => write!(f, "u"),
1629            Self::S => write!(f, "s"),
1630            Self::Mark => write!(f, "mark"),
1631            Self::Del => write!(f, "del"),
1632            Self::Ins => write!(f, "ins"),
1633            Self::Code => write!(f, "code"),
1634            Self::Samp => write!(f, "samp"),
1635            Self::Kbd => write!(f, "kbd"),
1636            Self::Var => write!(f, "var"),
1637            Self::Cite => write!(f, "cite"),
1638            Self::Dfn => write!(f, "dfn"),
1639            Self::Abbr => write!(f, "abbr"),
1640            Self::Acronym => write!(f, "acronym"),
1641            Self::Q => write!(f, "q"),
1642            Self::Time => write!(f, "time"),
1643            Self::Sub => write!(f, "sub"),
1644            Self::Sup => write!(f, "sup"),
1645            Self::Small => write!(f, "small"),
1646            Self::Big => write!(f, "big"),
1647            Self::Bdo => write!(f, "bdo"),
1648            Self::Bdi => write!(f, "bdi"),
1649            Self::Wbr => write!(f, "wbr"),
1650            Self::Ruby => write!(f, "ruby"),
1651            Self::Rt => write!(f, "rt"),
1652            Self::Rtc => write!(f, "rtc"),
1653            Self::Rp => write!(f, "rp"),
1654            Self::Data => write!(f, "data"),
1655
1656            // Embedded content
1657            Self::Canvas => write!(f, "canvas"),
1658            Self::Object => write!(f, "object"),
1659            Self::Param => write!(f, "param"),
1660            Self::Embed => write!(f, "embed"),
1661            Self::Audio => write!(f, "audio"),
1662            Self::Video => write!(f, "video"),
1663            Self::Source => write!(f, "source"),
1664            Self::Track => write!(f, "track"),
1665            Self::Map => write!(f, "map"),
1666            Self::Area => write!(f, "area"),
1667            Self::Svg => write!(f, "svg"),
1668            Self::SvgPath => write!(f, "path"),
1669            Self::SvgCircle => write!(f, "circle"),
1670            Self::SvgRect => write!(f, "rect"),
1671            Self::SvgEllipse => write!(f, "ellipse"),
1672            Self::SvgLine => write!(f, "line"),
1673            Self::SvgPolygon => write!(f, "polygon"),
1674            Self::SvgPolyline => write!(f, "polyline"),
1675            Self::SvgG => write!(f, "g"),
1676
1677            // SVG container elements
1678            Self::SvgDefs => write!(f, "defs"),
1679            Self::SvgSymbol => write!(f, "symbol"),
1680            Self::SvgUse => write!(f, "use"),
1681            Self::SvgSwitch => write!(f, "switch"),
1682
1683            // SVG text elements
1684            Self::SvgText => write!(f, "svg:text"),
1685            Self::SvgTspan => write!(f, "tspan"),
1686            Self::SvgTextPath => write!(f, "textpath"),
1687
1688            // SVG paint server elements
1689            Self::SvgLinearGradient => write!(f, "lineargradient"),
1690            Self::SvgRadialGradient => write!(f, "radialgradient"),
1691            Self::SvgStop => write!(f, "stop"),
1692            Self::SvgPattern => write!(f, "pattern"),
1693
1694            // SVG clipping/masking elements
1695            Self::SvgClipPathElement => write!(f, "clippath"),
1696            Self::SvgMask => write!(f, "mask"),
1697
1698            // SVG filter elements
1699            Self::SvgFilter => write!(f, "filter"),
1700            Self::SvgFeBlend => write!(f, "feblend"),
1701            Self::SvgFeColorMatrix => write!(f, "fecolormatrix"),
1702            Self::SvgFeComponentTransfer => write!(f, "fecomponenttransfer"),
1703            Self::SvgFeComposite => write!(f, "fecomposite"),
1704            Self::SvgFeConvolveMatrix => write!(f, "feconvolvematrix"),
1705            Self::SvgFeDiffuseLighting => write!(f, "fediffuselighting"),
1706            Self::SvgFeDisplacementMap => write!(f, "fedisplacementmap"),
1707            Self::SvgFeDistantLight => write!(f, "fedistantlight"),
1708            Self::SvgFeDropShadow => write!(f, "fedropshadow"),
1709            Self::SvgFeFlood => write!(f, "feflood"),
1710            Self::SvgFeFuncR => write!(f, "fefuncr"),
1711            Self::SvgFeFuncG => write!(f, "fefuncg"),
1712            Self::SvgFeFuncB => write!(f, "fefuncb"),
1713            Self::SvgFeFuncA => write!(f, "fefunca"),
1714            Self::SvgFeGaussianBlur => write!(f, "fegaussianblur"),
1715            Self::SvgFeImage => write!(f, "feimage"),
1716            Self::SvgFeMerge => write!(f, "femerge"),
1717            Self::SvgFeMergeNode => write!(f, "femergenode"),
1718            Self::SvgFeMorphology => write!(f, "femorphology"),
1719            Self::SvgFeOffset => write!(f, "feoffset"),
1720            Self::SvgFePointLight => write!(f, "fepointlight"),
1721            Self::SvgFeSpecularLighting => write!(f, "fespecularlighting"),
1722            Self::SvgFeSpotLight => write!(f, "fespotlight"),
1723            Self::SvgFeTile => write!(f, "fetile"),
1724            Self::SvgFeTurbulence => write!(f, "feturbulence"),
1725
1726            // SVG marker/image elements
1727            Self::SvgMarker => write!(f, "svg:marker"),
1728            Self::SvgImage => write!(f, "svg:image"),
1729            Self::SvgForeignObject => write!(f, "foreignobject"),
1730
1731            // SVG descriptive elements
1732            Self::SvgTitle => write!(f, "svg:title"),
1733            Self::SvgDesc => write!(f, "desc"),
1734            Self::SvgMetadata => write!(f, "metadata"),
1735            Self::SvgA => write!(f, "svg:a"),
1736            Self::SvgView => write!(f, "view"),
1737            Self::SvgStyle => write!(f, "svg:style"),
1738            Self::SvgScript => write!(f, "svg:script"),
1739
1740            // SVG animation elements
1741            Self::SvgAnimate => write!(f, "animate"),
1742            Self::SvgAnimateMotion => write!(f, "animatemotion"),
1743            Self::SvgAnimateTransform => write!(f, "animatetransform"),
1744            Self::SvgSet => write!(f, "set"),
1745            Self::SvgMpath => write!(f, "mpath"),
1746
1747            // Metadata
1748            Self::Title => write!(f, "title"),
1749            Self::Meta => write!(f, "meta"),
1750            Self::Link => write!(f, "link"),
1751            Self::Script => write!(f, "script"),
1752            Self::Style => write!(f, "style"),
1753            Self::Base => write!(f, "base"),
1754
1755            // Content elements
1756            Self::Text => write!(f, "text"),
1757            Self::Img => write!(f, "img"),
1758            Self::VirtualView => write!(f, "virtual-view"),
1759            Self::TransientWindow => write!(f, "transient-window"),
1760            Self::Icon => write!(f, "icon"),
1761            Self::GeolocationProbe => write!(f, "geolocation-probe"),
1762            Self::PageBreak => write!(f, "pagebreak"),
1763
1764            // Pseudo-elements
1765            Self::Before => write!(f, "::before"),
1766            Self::After => write!(f, "::after"),
1767            Self::Marker => write!(f, "::marker"),
1768            Self::Placeholder => write!(f, "::placeholder"),
1769        }
1770    }
1771}
1772
1773/// Represents a full CSS path (i.e. the "div#id.class" selector belonging to
1774///  a CSS "content group" (the following key-value block)).
1775///
1776/// ```no_run,ignore
1777/// "#div > .my_class:focus" ==
1778/// [
1779///   CssPathSelector::Type(NodeTypeTag::Div),
1780///   CssPathSelector::PseudoSelector(CssPathPseudoSelector::LimitChildren),
1781///   CssPathSelector::Class("my_class"),
1782///   CssPathSelector::PseudoSelector(CssPathPseudoSelector::Focus),
1783/// ]
1784#[derive(Clone, Hash, Default, PartialEq, Eq, PartialOrd, Ord)]
1785#[repr(C)]
1786pub struct CssPath {
1787    pub selectors: CssPathSelectorVec,
1788}
1789
1790impl_vec!(
1791    CssPathSelector,
1792    CssPathSelectorVec,
1793    CssPathSelectorVecDestructor,
1794    CssPathSelectorVecDestructorType,
1795    CssPathSelectorVecSlice,
1796    OptionCssPathSelector
1797);
1798impl_vec_debug!(CssPathSelector, CssPathSelectorVec);
1799impl_vec_partialord!(CssPathSelector, CssPathSelectorVec);
1800impl_vec_ord!(CssPathSelector, CssPathSelectorVec);
1801impl_vec_clone!(
1802    CssPathSelector,
1803    CssPathSelectorVec,
1804    CssPathSelectorVecDestructor
1805);
1806impl_vec_partialeq!(CssPathSelector, CssPathSelectorVec);
1807impl_vec_eq!(CssPathSelector, CssPathSelectorVec);
1808impl_vec_hash!(CssPathSelector, CssPathSelectorVec);
1809
1810impl CssPath {
1811    #[must_use]
1812    pub fn new(selectors: Vec<CssPathSelector>) -> Self {
1813        Self {
1814            selectors: selectors.into(),
1815        }
1816    }
1817
1818    /// Prepend a `Root` scope selector (`push_front`) confining this rule to the owner
1819    /// node `start` (whose subtree spans the inclusive flat ids `[start, end]`).
1820    /// Two cases (#47 leak fix + descendant-selector support):
1821    ///
1822    /// - A **bare `*` rule** (the `parse_inline` wrapper for a `with_css`/`set_css`
1823    ///   bare-declaration string) is scoped **node-only** (`[start, start]`):
1824    ///   inline-style semantics — it applies to the OWNER only and must not leak to
1825    ///   descendants or siblings. `[Root([s,s]), Global]` matches `s` only.
1826    /// - A rule with a **real selector** (`.menu-item`, `div`, a descendant chain —
1827    ///   from `add_component_css` / a component stylesheet) is scoped to the whole
1828    ///   **subtree** (`[start, end]`), so its selectors match within the owner's
1829    ///   subtree (e.g. a menu container's `.menu-item` children). `[Root([s,e]),
1830    ///   Class(x)]` matches any node in `[s,e]` that also matches `.x`.
1831    pub fn push_front_scope(&mut self, start: usize, end: usize) {
1832        self.push_front_scope_for(start, end, true);
1833    }
1834
1835    /// Like [`Self::push_front_scope`], but the CALLER decides whether a bare
1836    /// `[Global]` path is a bare-declaration wrapper (scope it NODE-ONLY,
1837    /// `[start, start]`) or a real stylesheet `* { ... }` rule (scope it to
1838    /// the whole subtree).
1839    ///
1840    /// The two are indistinguishable by path shape - `parse_inline` wraps
1841    /// selector-less declarations in `*`, and an author stylesheet's
1842    /// universal rule IS `*` - but they differ by RULE PRIORITY
1843    /// (`rule_priority::INLINE` vs `AUTHOR`), which only the caller holding
1844    /// the `CssRuleBlock` can see. Treating every bare global as a wrapper
1845    /// scoped the classic `* { margin: 0 }` reset of a mounted document to
1846    /// the mount root alone: the UA body margin survived on every child and
1847    /// each reftest page rendered shifted by 8px against the browser.
1848    pub fn push_front_scope_for(&mut self, start: usize, end: usize, node_only_bare_global: bool) {
1849        let is_bare_global = self.selectors.as_ref().len() == 1
1850            && matches!(
1851                self.selectors.as_ref().first(),
1852                Some(CssPathSelector::Global)
1853            );
1854        let range = if is_bare_global && node_only_bare_global {
1855            CssScopeRange { start, end: start }
1856        } else {
1857            CssScopeRange { start, end }
1858        };
1859        let mut selectors = Vec::with_capacity(self.selectors.as_ref().len() + 1);
1860        selectors.push(CssPathSelector::Root(range));
1861        selectors.extend(self.selectors.as_ref().iter().cloned());
1862        self.selectors = selectors.into();
1863    }
1864}
1865
1866impl fmt::Display for CssPath {
1867    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1868        for selector in self.selectors.as_ref() {
1869            write!(f, "{selector}")?;
1870        }
1871        Ok(())
1872    }
1873}
1874
1875impl fmt::Debug for CssPath {
1876    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1877        write!(f, "{self}")
1878    }
1879}
1880
1881/// Inclusive range of flat `NodeId`s describing a node's subtree `[start, end]`
1882/// (`end = start + estimated_total_children`, since the flat arena lays subtrees
1883/// out contiguously).
1884///
1885/// Carried by [`CssPathSelector::Root`] to scope inline css to
1886/// a subtree, and is the unit of future parallel per-subtree cascading.
1887/// `repr(C)` for FFI / api.json codegen.
1888#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1889#[repr(C)]
1890pub struct CssScopeRange {
1891    /// First flat `NodeId` of the subtree (the owning node itself).
1892    pub start: usize,
1893    /// Last flat `NodeId` of the subtree, inclusive (`start` for a leaf).
1894    pub end: usize,
1895}
1896
1897impl CssScopeRange {
1898    /// True if `node` (a flat `NodeId` index) is inside this subtree range.
1899    #[inline]
1900    #[must_use]
1901    pub const fn contains(&self, node: usize) -> bool {
1902        self.start <= node && node <= self.end
1903    }
1904}
1905
1906#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1907#[repr(C, u8)]
1908#[derive(Default)]
1909pub enum CssPathSelector {
1910    /// Represents the `*` selector
1911    #[default]
1912    Global,
1913    /// Scope marker carrying a node's **subtree range** `[start, end]` (inclusive
1914    /// flat `NodeId`s; `end = start + estimated_total_children`). Matches a node
1915    /// iff `start <= node <= end`. Synthesized at flatten time and `push_front`-ed
1916    /// onto every inline (`with_css`/`set_css`) rule's path, so the rule compounds
1917    /// with the `parse_inline` `*` wrapper (`[Root(s,e), Global, …]`) and is scoped
1918    /// to that node's subtree instead of leaking to the whole tree (#47). Because
1919    /// the flat arena lays subtrees out contiguously, this range is also the unit
1920    /// of future parallel per-subtree cascading.
1921    Root(CssScopeRange),
1922    /// `div`, `p`, etc.
1923    Type(NodeTypeTag),
1924    /// `.something`
1925    Class(AzString),
1926    /// `#something`
1927    Id(AzString),
1928    /// `:something`
1929    PseudoSelector(CssPathPseudoSelector),
1930    /// `[attr]`, `[attr="value"]`, `[attr~="value"]`, etc.
1931    Attribute(CssAttributeSelector),
1932    /// Represents the `>` selector (direct child)
1933    DirectChildren,
1934    /// Represents the ` ` selector (descendant)
1935    Children,
1936    /// Represents the `+` selector (adjacent sibling)
1937    AdjacentSibling,
1938    /// Represents the `~` selector (general sibling)
1939    GeneralSibling,
1940}
1941
1942/// Attribute selector (`[attr]`, `[attr="value"]`, ...).
1943#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1944#[repr(C)]
1945pub struct CssAttributeSelector {
1946    pub name: AzString,
1947    pub op: AttributeMatchOp,
1948    pub value: OptionString,
1949}
1950
1951impl Default for CssAttributeSelector {
1952    fn default() -> Self {
1953        Self {
1954            name: AzString::default(),
1955            op: AttributeMatchOp::Exists,
1956            value: OptionString::None,
1957        }
1958    }
1959}
1960
1961/// Operator that compares an attribute value against a target string.
1962#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1963#[repr(C)]
1964#[derive(Default)]
1965pub enum AttributeMatchOp {
1966    /// `[attr]` — attribute is present (any value).
1967    #[default]
1968    Exists,
1969    /// `[attr="value"]` — attribute equals value exactly.
1970    Eq,
1971    /// `[attr~="value"]` — value is one of the whitespace-separated words.
1972    Includes,
1973    /// `[attr|="value"]` — value equals exactly OR begins with value followed by `-`.
1974    DashMatch,
1975    /// `[attr^="value"]` — value starts with the given prefix.
1976    Prefix,
1977    /// `[attr$="value"]` — value ends with the given suffix.
1978    Suffix,
1979    /// `[attr*="value"]` — value contains the given substring.
1980    Substring,
1981}
1982
1983impl_option!(
1984    CssPathSelector,
1985    OptionCssPathSelector,
1986    copy = false,
1987    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1988);
1989
1990impl fmt::Display for CssPathSelector {
1991    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1992        use self::CssPathSelector::{
1993            AdjacentSibling, Attribute, Children, Class, DirectChildren, GeneralSibling, Global,
1994            Id, PseudoSelector, Root, Type,
1995        };
1996        match &self {
1997            Global => write!(f, "*"),
1998            Root(r) => write!(f, ":root({}..={})", r.start, r.end),
1999            Type(n) => write!(f, "{n}"),
2000            Class(c) => write!(f, ".{c}"),
2001            Id(i) => write!(f, "#{i}"),
2002            PseudoSelector(p) => write!(f, ":{p}"),
2003            Attribute(a) => write!(f, "{a}"),
2004            DirectChildren => write!(f, ">"),
2005            Children => write!(f, " "),
2006            AdjacentSibling => write!(f, "+"),
2007            GeneralSibling => write!(f, "~"),
2008        }
2009    }
2010}
2011
2012impl fmt::Display for CssAttributeSelector {
2013    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2014        match (&self.op, self.value.as_ref()) {
2015            (AttributeMatchOp::Exists, _) => write!(f, "[{}]", self.name),
2016            (op, Some(v)) => write!(f, "[{}{}=\"{}\"]", self.name, op.symbol_prefix(), v),
2017            (op, None) => write!(f, "[{}{}=\"\"]", self.name, op.symbol_prefix()),
2018        }
2019    }
2020}
2021
2022impl AttributeMatchOp {
2023    /// Returns the prefix character for the `=` operator (e.g. `~` for `~=`).
2024    /// `Eq` returns `""`, `Exists` is unused (no `=` printed at all).
2025    #[must_use]
2026    pub const fn symbol_prefix(&self) -> &'static str {
2027        match self {
2028            Self::Exists | Self::Eq => "",
2029            Self::Includes => "~",
2030            Self::DashMatch => "|",
2031            Self::Prefix => "^",
2032            Self::Suffix => "$",
2033            Self::Substring => "*",
2034        }
2035    }
2036}
2037
2038#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2039#[repr(C, u8)]
2040pub enum CssPathPseudoSelector {
2041    /// `:first`
2042    First,
2043    /// `:last`
2044    Last,
2045    /// `:nth-child`
2046    NthChild(CssNthChildSelector),
2047    /// `:hover` - mouse is over element
2048    Hover,
2049    /// `:active` - mouse is pressed and over element
2050    Active,
2051    /// `:focus` - element has received focus
2052    Focus,
2053    /// `:seat-focus` - a NON-primary pointer seat focuses the element
2054    /// (9b-ii-a-i-d-iii-a); the primary's focus is `:focus`.
2055    SeatFocus,
2056    /// `:lang(de)` - element matches language
2057    Lang(AzString),
2058    /// `:backdrop` - window is not focused (GTK compatibility)
2059    Backdrop,
2060    /// `:dragging` - element is currently being dragged
2061    Dragging,
2062    /// `:drag-over` - a dragged element is over this drop target
2063    DragOver,
2064    /// `:root` - matches the document root element (equivalent to `html`,
2065    /// but with pseudo-class specificity). Structural (non-interactive).
2066    Root,
2067    /// `::placeholder` - the prompt the ENGINE paints inside an empty
2068    /// editable. A pseudo-ELEMENT: it styles painted glyphs, not a node, so
2069    /// it never matches the element itself.
2070    Placeholder,
2071}
2072
2073/// Selector for the `:nth-child()` CSS pseudo-class.
2074#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2075#[repr(C, u8)]
2076pub enum CssNthChildSelector {
2077    Number(u32),
2078    Even,
2079    Odd,
2080    Pattern(CssNthChildPattern),
2081}
2082
2083/// Pattern for `:nth-child(An+B)` selectors, where `pattern_repeat` is A and `offset` is B.
2084#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2085#[repr(C)]
2086pub struct CssNthChildPattern {
2087    pub pattern_repeat: u32,
2088    pub offset: u32,
2089}
2090
2091impl fmt::Display for CssNthChildSelector {
2092    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2093        use self::CssNthChildSelector::{Even, Number, Odd, Pattern};
2094        match &self {
2095            Number(u) => write!(f, "{u}"),
2096            Even => write!(f, "even"),
2097            Odd => write!(f, "odd"),
2098            Pattern(p) => write!(f, "{}n + {}", p.pattern_repeat, p.offset),
2099        }
2100    }
2101}
2102
2103impl fmt::Display for CssPathPseudoSelector {
2104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2105        use self::CssPathPseudoSelector::{
2106            Active, Backdrop, DragOver, Dragging, First, Focus, Hover, Lang, Last, NthChild,
2107            Placeholder, Root, SeatFocus,
2108        };
2109        match &self {
2110            First => write!(f, "first"),
2111            Last => write!(f, "last"),
2112            NthChild(u) => write!(f, "nth-child({u})"),
2113            Hover => write!(f, "hover"),
2114            Active => write!(f, "active"),
2115            Focus => write!(f, "focus"),
2116            SeatFocus => write!(f, "seat-focus"),
2117            Lang(lang) => write!(f, "lang({})", lang.as_str()),
2118            Backdrop => write!(f, "backdrop"),
2119            Dragging => write!(f, "dragging"),
2120            DragOver => write!(f, "drag-over"),
2121            Root => write!(f, "root"),
2122            // The extra colon is part of the NAME: `:{p}` at the call site
2123            // plus this leading one spells the pseudo-ELEMENT `::placeholder`.
2124            Placeholder => write!(f, ":placeholder"),
2125        }
2126    }
2127}
2128
2129impl Css {
2130    /// Creates a new, empty CSS.
2131    #[must_use]
2132    pub fn empty() -> Self {
2133        Self::default()
2134    }
2135
2136    /// Sort the rules by `(priority, specificity)` so they apply in cascade order.
2137    /// Lower-priority rules sort first; ties break by selector specificity.
2138    /// This preserves layer identity (UA / SYSTEM / AUTHOR / INLINE / RUNTIME)
2139    /// without needing a separate `Stylesheet` boundary.
2140    pub fn sort_by_specificity(&mut self) {
2141        self.rules.as_mut().sort_by(|a, b| {
2142            a.priority
2143                .cmp(&b.priority)
2144                .then_with(|| get_specificity(&a.path).cmp(&get_specificity(&b.path)))
2145        });
2146    }
2147
2148    pub fn rules(&self) -> core::slice::Iter<'_, CssRuleBlock> {
2149        self.rules.as_ref().iter()
2150    }
2151
2152    /// Iterate `(property, conditions)` pairs as if this were a flat list of
2153    /// `CssPropertyWithConditions`. Each `Static` declaration yields one item,
2154    /// sharing the conditions of its enclosing rule. `Dynamic` declarations
2155    /// are skipped (matching the previous inline-CSS behaviour).
2156    ///
2157    /// Used by cascade and diff code that walks per-property to keep the
2158    /// flat-iteration shape after the inline-vs-component unification.
2159    pub fn iter_inline_properties(
2160        &self,
2161    ) -> impl Iterator<Item = (&CssProperty, &DynamicSelectorVec)> + '_ {
2162        self.rules.as_ref().iter().flat_map(|r| {
2163            r.declarations.as_ref().iter().filter_map(move |d| match d {
2164                CssDeclaration::Static(p) => Some((p, &r.conditions)),
2165                CssDeclaration::Dynamic(_) => None,
2166            })
2167        })
2168    }
2169}
2170
2171#[cfg(test)]
2172mod root_scope_tests {
2173    use super::*;
2174
2175    #[test]
2176    fn scope_range_contains() {
2177        let r = CssScopeRange { start: 3, end: 7 };
2178        assert!(r.contains(3) && r.contains(5) && r.contains(7));
2179        assert!(!r.contains(2) && !r.contains(8));
2180        // leaf: start == end matches only itself
2181        let leaf = CssScopeRange { start: 4, end: 4 };
2182        assert!(leaf.contains(4));
2183        assert!(!leaf.contains(3) && !leaf.contains(5));
2184    }
2185
2186    #[test]
2187    fn push_front_scope_compounds_with_wrapper() {
2188        // a bare-decl `set_css` path is `[Global]` (the parse_inline `*` wrapper) and
2189        // is scoped NODE-ONLY ([start, start]) so it applies to the owner only.
2190        let mut p = CssPath::new(vec![CssPathSelector::Global]);
2191        p.push_front_scope(5, 9);
2192        assert_eq!(
2193            p.selectors.as_ref(),
2194            &[
2195                CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
2196                CssPathSelector::Global
2197            ][..]
2198        );
2199        // a path with a real selector is SUBTREE-scoped ([start, end]).
2200        let subtree = CssScopeRange { start: 5, end: 9 };
2201        let mut p2 = CssPath::new(vec![
2202            CssPathSelector::Global,
2203            CssPathSelector::Children,
2204            CssPathSelector::Class("foo".to_string().into()),
2205        ]);
2206        p2.push_front_scope(5, 9);
2207        assert_eq!(p2.selectors.as_ref()[0], CssPathSelector::Root(subtree));
2208        assert_eq!(p2.selectors.as_ref().len(), 4);
2209    }
2210
2211    #[test]
2212    fn root_display_roundtrips() {
2213        let s = CssPathSelector::Root(CssScopeRange { start: 2, end: 6 });
2214        assert_eq!(format!("{s}"), ":root(2..=6)");
2215    }
2216
2217    #[test]
2218    fn parse_inline_keeps_layout_and_style_decls() {
2219        // set_css("width: 200px; height: 100px; background: red") must keep all
2220        // three declarations (layout + style) as Static props in the parsed rule.
2221        let css = Css::parse_inline("width: 200px; height: 100px; background: red");
2222        let mut types = Vec::new();
2223        for r in css.rules.as_ref() {
2224            for d in r.declarations.as_ref() {
2225                if let CssDeclaration::Static(p) = d {
2226                    types.push(alloc::format!("{:?}", p.get_type()));
2227                }
2228            }
2229        }
2230        println!("INLINE PROP TYPES: {types:?}");
2231        assert!(
2232            types.iter().any(|t| t.contains("width")),
2233            "width must survive parse_inline as a Static decl; got {types:?}"
2234        );
2235        assert!(
2236            types.iter().any(|t| t.contains("height")),
2237            "height must survive parse_inline; got {types:?}"
2238        );
2239    }
2240}
2241
2242#[cfg(test)]
2243mod priority_sort_tests {
2244    use super::*;
2245    use crate::css::rule_priority;
2246
2247    fn rule_with(priority: u8, selectors: Vec<CssPathSelector>) -> CssRuleBlock {
2248        CssRuleBlock {
2249            path: CssPath {
2250                selectors: selectors.into(),
2251            },
2252            declarations: Vec::new().into(),
2253            conditions: DynamicSelectorVec::from_const_slice(&[]),
2254            priority,
2255        }
2256    }
2257
2258    /// Pin the (priority, specificity) sort order. Lower priority sorts first;
2259    /// ties break by specificity.
2260    #[test]
2261    fn sort_by_priority_then_specificity() {
2262        let mut css = Css::new(vec![
2263            // Author rule, no specificity.
2264            rule_with(rule_priority::AUTHOR, vec![CssPathSelector::Global]),
2265            // UA rule with high specificity — must still come BEFORE any author rule.
2266            rule_with(
2267                rule_priority::UA,
2268                vec![
2269                    CssPathSelector::Id("ua-id".to_string().into()),
2270                    CssPathSelector::Class("ua-class".to_string().into()),
2271                ],
2272            ),
2273            // Author rule with high specificity.
2274            rule_with(
2275                rule_priority::AUTHOR,
2276                vec![CssPathSelector::Id("a-id".to_string().into())],
2277            ),
2278            // System rule with no specificity — must sit between UA and author.
2279            rule_with(rule_priority::SYSTEM, vec![CssPathSelector::Global]),
2280        ]);
2281        css.sort_by_specificity();
2282        let priorities: Vec<u8> = css.rules.as_ref().iter().map(|r| r.priority).collect();
2283        assert_eq!(
2284            priorities,
2285            vec![
2286                rule_priority::UA,
2287                rule_priority::SYSTEM,
2288                rule_priority::AUTHOR,
2289                rule_priority::AUTHOR
2290            ],
2291            "rules must sort by layer first; specificity only breaks ties within a layer"
2292        );
2293        // Within author, the high-specificity #a-id comes after the * rule.
2294        let last_two_specificity: Vec<_> = css
2295            .rules
2296            .as_ref()
2297            .iter()
2298            .filter(|r| r.priority == rule_priority::AUTHOR)
2299            .map(|r| get_specificity(&r.path))
2300            .collect();
2301        assert!(last_two_specificity[0] < last_two_specificity[1]);
2302    }
2303}
2304
2305/// Returns specificity of the given css path. Further information can be found on
2306/// [the w3 website](http://www.w3.org/TR/selectors/#specificity).
2307#[must_use]
2308pub fn get_specificity(path: &CssPath) -> (usize, usize, usize, usize) {
2309    let id_count = path
2310        .selectors
2311        .iter()
2312        .filter(|x| matches!(x, CssPathSelector::Id(_)))
2313        .count();
2314    let class_count = path
2315        .selectors
2316        .iter()
2317        .filter(|x| {
2318            matches!(
2319                x,
2320                CssPathSelector::Class(_)
2321                    | CssPathSelector::PseudoSelector(_)
2322                    | CssPathSelector::Attribute(_)
2323            )
2324        })
2325        .count();
2326    let div_count = path
2327        .selectors
2328        .iter()
2329        .filter(|x| matches!(x, CssPathSelector::Type(_)))
2330        .count();
2331    (id_count, class_count, div_count, path.selectors.len())
2332}
2333
2334#[cfg(test)]
2335#[allow(clippy::pedantic, clippy::nursery)]
2336mod autotest_generated {
2337    use core::hash::{Hash, Hasher};
2338    use std::collections::hash_map::DefaultHasher;
2339
2340    use super::*;
2341    use crate::{
2342        dynamic_selector::DynamicSelector,
2343        props::{
2344            basic::{color::ColorU, pixel::PixelValue},
2345            layout::dimensions::LayoutWidth,
2346            style::text::StyleTextColor,
2347        },
2348    };
2349
2350    // ---------------------------------------------------------------------
2351    // helpers
2352    // ---------------------------------------------------------------------
2353
2354    /// `width` — NOT inheritable, DOES trigger relayout.
2355    fn prop_width(px: f32) -> CssProperty {
2356        CssProperty::width(LayoutWidth::Px(PixelValue::px(px)))
2357    }
2358
2359    /// `color` — IS inheritable, does NOT trigger relayout.
2360    fn prop_text_color(r: u8) -> CssProperty {
2361        CssProperty::const_text_color(StyleTextColor {
2362            inner: ColorU::new(r, 0, 0, 255),
2363        })
2364    }
2365
2366    fn dyn_prop(id: &str, default_value: CssProperty) -> DynamicCssProperty {
2367        DynamicCssProperty {
2368            dynamic_id: id.to_string().into(),
2369            default_value,
2370        }
2371    }
2372
2373    fn rule_at(priority: u8, selectors: Vec<CssPathSelector>) -> CssRuleBlock {
2374        let mut r = CssRuleBlock::new(
2375            CssPath::new(selectors),
2376            vec![CssDeclaration::Static(prop_width(1.0))],
2377        );
2378        r.priority = priority;
2379        r
2380    }
2381
2382    fn hash_of<T: Hash>(t: &T) -> u64 {
2383        let mut h = DefaultHasher::new();
2384        t.hash(&mut h);
2385        h.finish()
2386    }
2387
2388    /// A stand-in property payload so the generic `CssPropertyValue<T>` surface can be
2389    /// driven with hostile floats (NaN / ±inf) that no real CSS type would hand us.
2390    #[derive(Debug, Copy, Clone, PartialEq, Default)]
2391    struct TestVal(f32);
2392
2393    impl PrintAsCssValue for TestVal {
2394        fn print_as_css_value(&self) -> String {
2395            format!("{}", self.0)
2396        }
2397    }
2398
2399    impl fmt::Display for TestVal {
2400        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2401            write!(f, "{}", self.0)
2402        }
2403    }
2404
2405    /// The six CSS-wide keyword variants (everything that is not `Exact`).
2406    fn keyword_values() -> Vec<CssPropertyValue<TestVal>> {
2407        vec![
2408            CssPropertyValue::Auto,
2409            CssPropertyValue::None,
2410            CssPropertyValue::Initial,
2411            CssPropertyValue::Inherit,
2412            CssPropertyValue::Revert,
2413            CssPropertyValue::Unset,
2414        ]
2415    }
2416
2417    // =====================================================================
2418    // Css — constructors, predicates, getters
2419    // =====================================================================
2420
2421    #[test]
2422    fn css_empty_is_the_neutral_element() {
2423        let e = Css::empty();
2424        assert!(e.is_empty());
2425        assert_eq!(e.rules().count(), 0);
2426        assert_eq!(e.iter_inline_properties().count(), 0);
2427        // empty() / default() / new(vec![]) must all agree.
2428        assert_eq!(e, Css::default());
2429        assert_eq!(e, Css::new(Vec::new()));
2430        assert_eq!(e, Css::from(Vec::<CssRuleBlock>::new()));
2431    }
2432
2433    #[test]
2434    fn css_new_preserves_length_and_order() {
2435        let rules = vec![
2436            rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
2437            rule_at(
2438                rule_priority::AUTHOR,
2439                vec![CssPathSelector::Id("a".to_string().into())],
2440            ),
2441            rule_at(rule_priority::INLINE, vec![CssPathSelector::Children]),
2442        ];
2443        let css = Css::new(rules.clone());
2444        assert!(!css.is_empty());
2445        assert_eq!(css.rules().count(), 3);
2446        assert_eq!(css.rules.as_ref(), &rules[..]);
2447        // `rules()` and the raw vec must not disagree.
2448        assert_eq!(css.rules().count(), css.rules.as_ref().len());
2449    }
2450
2451    #[test]
2452    fn css_new_with_many_rules_does_not_panic() {
2453        let rules: Vec<CssRuleBlock> = (0..10_000)
2454            .map(|i| rule_at((i % 256) as u8, vec![CssPathSelector::Global]))
2455            .collect();
2456        let css = Css::new(rules);
2457        assert_eq!(css.rules.as_ref().len(), 10_000);
2458        assert!(!css.is_empty());
2459        // Every rule carries one Static decl → one inline property each.
2460        assert_eq!(css.iter_inline_properties().count(), 10_000);
2461    }
2462
2463    #[test]
2464    fn css_sort_by_specificity_on_empty_and_singleton_is_a_noop() {
2465        let mut empty = Css::empty();
2466        empty.sort_by_specificity();
2467        assert!(empty.is_empty());
2468
2469        let one = rule_at(rule_priority::AUTHOR, vec![CssPathSelector::Global]);
2470        let mut css = Css::new(vec![one.clone()]);
2471        css.sort_by_specificity();
2472        assert_eq!(css.rules.as_ref(), &[one][..]);
2473    }
2474
2475    #[test]
2476    fn css_sort_by_specificity_is_idempotent() {
2477        let mut css = Css::new(vec![
2478            rule_at(rule_priority::RUNTIME, vec![CssPathSelector::Global]),
2479            rule_at(
2480                rule_priority::UA,
2481                vec![
2482                    CssPathSelector::Id("x".to_string().into()),
2483                    CssPathSelector::Class("y".to_string().into()),
2484                ],
2485            ),
2486            rule_at(rule_priority::INLINE, vec![CssPathSelector::Global]),
2487            rule_at(
2488                rule_priority::AUTHOR,
2489                vec![CssPathSelector::Type(NodeTypeTag::Div)],
2490            ),
2491            rule_at(rule_priority::SYSTEM, vec![CssPathSelector::Global]),
2492        ]);
2493        css.sort_by_specificity();
2494        let once = css.clone();
2495        css.sort_by_specificity();
2496        assert_eq!(css, once, "sort_by_specificity must be idempotent");
2497
2498        // Layer order is the primary key, and it is monotonically non-decreasing.
2499        let priorities: Vec<u8> = css.rules().map(|r| r.priority).collect();
2500        assert_eq!(
2501            priorities,
2502            vec![
2503                rule_priority::UA,
2504                rule_priority::SYSTEM,
2505                rule_priority::AUTHOR,
2506                rule_priority::INLINE,
2507                rule_priority::RUNTIME,
2508            ]
2509        );
2510    }
2511
2512    #[test]
2513    fn css_sort_by_specificity_keeps_equal_keys_in_source_order() {
2514        // sort_by is stable — two rules with the same (priority, specificity) must not swap.
2515        let a = CssRuleBlock::new(
2516            CssPath::new(vec![CssPathSelector::Global]),
2517            vec![CssDeclaration::Static(prop_width(1.0))],
2518        );
2519        let b = CssRuleBlock::new(
2520            CssPath::new(vec![CssPathSelector::Global]),
2521            vec![CssDeclaration::Static(prop_width(2.0))],
2522        );
2523        let mut css = Css::new(vec![a.clone(), b.clone()]);
2524        css.sort_by_specificity();
2525        assert_eq!(
2526            css.rules.as_ref(),
2527            &[a, b][..],
2528            "ties must keep source order (last-wins cascade depends on it)"
2529        );
2530    }
2531
2532    #[test]
2533    fn css_iter_inline_properties_skips_dynamic_declarations() {
2534        let css = Css::new(vec![CssRuleBlock::with_conditions(
2535            CssPath::new(vec![CssPathSelector::Global]),
2536            vec![
2537                CssDeclaration::Static(prop_width(10.0)),
2538                CssDeclaration::Dynamic(dyn_prop("d", prop_text_color(1))),
2539                CssDeclaration::Static(prop_text_color(2)),
2540            ],
2541            vec![DynamicSelector::ContainerName("c".to_string().into())],
2542        )]);
2543
2544        let collected: Vec<_> = css.iter_inline_properties().collect();
2545        assert_eq!(collected.len(), 2, "Dynamic declarations must be skipped");
2546        assert_eq!(collected[0].0.get_type(), CssPropertyType::Width);
2547        assert_eq!(collected[1].0.get_type(), CssPropertyType::TextColor);
2548        // Every yielded property shares the conditions of its enclosing rule.
2549        for (_, conds) in &collected {
2550            assert_eq!(conds.as_ref().len(), 1);
2551        }
2552    }
2553
2554    #[test]
2555    fn css_iter_inline_properties_on_rule_without_declarations() {
2556        let css = Css::new(vec![CssRuleBlock::new(
2557            CssPath::new(vec![CssPathSelector::Global]),
2558            Vec::new(),
2559        )]);
2560        assert!(
2561            !css.is_empty(),
2562            "a rule with 0 declarations is still a rule"
2563        );
2564        assert_eq!(css.iter_inline_properties().count(), 0);
2565    }
2566
2567    #[test]
2568    fn css_ord_is_length_based_by_design() {
2569        // Documented deviation: `Ord for Css` compares rule COUNT only, so two
2570        // structurally different stylesheets of equal length compare Equal while
2571        // PartialEq reports them as different. Pinned here so the deviation is a
2572        // deliberate choice, not an accident.
2573        let a = Css::new(vec![rule_at(
2574            rule_priority::UA,
2575            vec![CssPathSelector::Global],
2576        )]);
2577        let b = Css::new(vec![rule_at(
2578            rule_priority::RUNTIME,
2579            vec![CssPathSelector::Type(NodeTypeTag::Div)],
2580        )]);
2581        assert_ne!(a, b);
2582        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
2583        assert_eq!(a.partial_cmp(&b), Some(core::cmp::Ordering::Equal));
2584        // …and the length ordering itself is right.
2585        let longer = Css::new(vec![
2586            rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
2587            rule_at(rule_priority::UA, vec![CssPathSelector::Global]),
2588        ]);
2589        assert_eq!(a.cmp(&longer), core::cmp::Ordering::Less);
2590        assert_eq!(Css::empty().cmp(&a), core::cmp::Ordering::Less);
2591    }
2592
2593    // =====================================================================
2594    // CssDeclaration
2595    // =====================================================================
2596
2597    #[test]
2598    fn css_declaration_new_static_matches_the_wrapped_property() {
2599        let p = prop_width(42.0);
2600        let d = CssDeclaration::new_static(p.clone());
2601        assert_eq!(d, CssDeclaration::Static(p.clone()));
2602        assert_eq!(d.get_type(), p.get_type());
2603        assert_eq!(d.get_type(), CssPropertyType::Width);
2604    }
2605
2606    #[test]
2607    fn css_declaration_new_dynamic_takes_its_type_from_the_default_value() {
2608        let dp = dyn_prop("my_id", prop_text_color(7));
2609        let d = CssDeclaration::new_dynamic(dp.clone());
2610        assert_eq!(d, CssDeclaration::Dynamic(dp));
2611        assert_eq!(
2612            d.get_type(),
2613            CssPropertyType::TextColor,
2614            "a Dynamic declaration's type is its default value's type"
2615        );
2616    }
2617
2618    #[test]
2619    fn css_declaration_is_inheritable_matrix() {
2620        // color inherits, width does not — that is the CSS spec.
2621        assert!(CssDeclaration::new_static(prop_text_color(1)).is_inheritable());
2622        assert!(!CssDeclaration::new_static(prop_width(1.0)).is_inheritable());
2623        // A Dynamic declaration is NEVER inheritable, even when its default value
2624        // is an inheritable property. Guards the documented anti-surprise rule.
2625        assert!(
2626            !CssDeclaration::new_dynamic(dyn_prop("c", prop_text_color(1))).is_inheritable(),
2627            "Dynamic declarations must never inherit, even wrapping an inheritable prop"
2628        );
2629        assert!(!CssDeclaration::new_dynamic(dyn_prop("w", prop_width(1.0))).is_inheritable());
2630    }
2631
2632    #[test]
2633    fn css_declaration_can_trigger_relayout_matrix() {
2634        assert!(CssDeclaration::new_static(prop_width(1.0)).can_trigger_relayout());
2635        assert!(!CssDeclaration::new_static(prop_text_color(1)).can_trigger_relayout());
2636        // Dynamic delegates to the default value's type (unlike is_inheritable).
2637        assert!(CssDeclaration::new_dynamic(dyn_prop("w", prop_width(1.0))).can_trigger_relayout());
2638        assert!(
2639            !CssDeclaration::new_dynamic(dyn_prop("c", prop_text_color(1))).can_trigger_relayout()
2640        );
2641    }
2642
2643    #[test]
2644    fn css_declaration_to_str_static_is_non_empty_for_edge_floats() {
2645        for px in [
2646            0.0_f32,
2647            -0.0,
2648            f32::MIN,
2649            f32::MAX,
2650            f32::NAN,
2651            f32::INFINITY,
2652            f32::NEG_INFINITY,
2653            f32::EPSILON,
2654        ] {
2655            let s = CssDeclaration::new_static(prop_width(px)).to_str();
2656            assert!(
2657                !s.is_empty(),
2658                "to_str() must render something for width: {px:?}"
2659            );
2660        }
2661    }
2662
2663    #[test]
2664    fn css_declaration_to_str_dynamic_renders_var_syntax() {
2665        let s = CssDeclaration::new_dynamic(dyn_prop("my_id", prop_width(5.0))).to_str();
2666        assert!(
2667            s.starts_with("var(--my_id, "),
2668            "dynamic to_str must render CSS var() syntax, got {s:?}"
2669        );
2670        assert!(s.ends_with(')'));
2671    }
2672
2673    #[test]
2674    fn css_declaration_to_str_dynamic_with_hostile_ids_does_not_panic() {
2675        let long = "x".repeat(100_000);
2676        for id in [
2677            "",
2678            "   ",
2679            "😀",
2680            "a\u{0301}\u{0301}",
2681            "--)",
2682            "\u{0}",
2683            "a\nb",
2684            long.as_str(),
2685        ] {
2686            let s = CssDeclaration::new_dynamic(dyn_prop(id, prop_width(1.0))).to_str();
2687            assert!(s.starts_with("var(--"));
2688        }
2689    }
2690
2691    // =====================================================================
2692    // DynamicCssProperty
2693    // =====================================================================
2694
2695    #[test]
2696    fn dynamic_css_property_is_never_inheritable() {
2697        for p in [
2698            prop_text_color(0),
2699            prop_width(0.0),
2700            prop_width(f32::NAN),
2701            CssProperty::const_none(CssPropertyType::FontSize),
2702            CssProperty::const_inherit(CssPropertyType::TextColor),
2703        ] {
2704            assert!(
2705                !dyn_prop("id", p).is_inheritable(),
2706                "DynamicCssProperty::is_inheritable is unconditionally false"
2707            );
2708        }
2709    }
2710
2711    #[test]
2712    fn dynamic_css_property_relayout_follows_the_default_value_type() {
2713        assert!(dyn_prop("a", prop_width(1.0)).can_trigger_relayout());
2714        assert!(!dyn_prop("a", prop_text_color(1)).can_trigger_relayout());
2715        // Keyword-valued defaults keep their property type, so the answer is unchanged.
2716        assert!(
2717            dyn_prop("a", CssProperty::const_auto(CssPropertyType::Width)).can_trigger_relayout()
2718        );
2719        assert!(
2720            !dyn_prop("a", CssProperty::const_none(CssPropertyType::TextColor))
2721                .can_trigger_relayout()
2722        );
2723    }
2724
2725    // =====================================================================
2726    // BoxOrStatic
2727    // =====================================================================
2728
2729    static STATIC_U32: u32 = 0xDEAD_BEEF;
2730
2731    #[test]
2732    fn box_or_static_heap_round_trips_through_as_ref_and_deref() {
2733        let b = BoxOrStatic::heap(123_u32);
2734        assert_eq!(*b.as_ref(), 123);
2735        assert_eq!(*b, 123, "Deref must agree with as_ref");
2736        assert_eq!(*BoxOrStatic::heap(u32::MAX).as_ref(), u32::MAX);
2737        assert_eq!(*BoxOrStatic::heap(0_u32).as_ref(), 0);
2738        // Zero-sized payload: exercise as_ref + Deref on the ZST heap path (no
2739        // value to compare; Miri is the UB oracle here).
2740        let z = BoxOrStatic::heap(());
2741        let _: &() = z.as_ref();
2742        let () = *z;
2743        let big = BoxOrStatic::heap(vec![0_u8; 1_000_000]);
2744        assert_eq!(big.as_ref().len(), 1_000_000);
2745    }
2746
2747    #[test]
2748    fn box_or_static_static_variant_reads_through_as_ref() {
2749        let b: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2750        assert_eq!(*b.as_ref(), 0xDEAD_BEEF);
2751        assert_eq!(*b, 0xDEAD_BEEF);
2752    }
2753
2754    #[test]
2755    fn box_or_static_as_mut_mutates_the_boxed_value() {
2756        let mut b = BoxOrStatic::heap(1_u32);
2757        *b.as_mut() = 9;
2758        assert_eq!(*b.as_ref(), 9);
2759    }
2760
2761    #[test]
2762    #[should_panic(expected = "Cannot mutate a static BoxOrStatic value")]
2763    fn box_or_static_as_mut_on_static_panics_as_documented() {
2764        let mut b: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2765        let _ = b.as_mut();
2766    }
2767
2768    #[test]
2769    fn box_or_static_clone_is_deep_for_boxed() {
2770        let a = BoxOrStatic::heap(5_u32);
2771        let mut b = a.clone();
2772        *b.as_mut() = 6;
2773        assert_eq!(*a.as_ref(), 5, "cloning a Boxed value must not alias it");
2774        assert_eq!(*b.as_ref(), 6);
2775        assert_ne!(a, b);
2776    }
2777
2778    #[test]
2779    fn box_or_static_eq_ord_hash_all_delegate_to_the_inner_value() {
2780        let heap = BoxOrStatic::heap(0xDEAD_BEEF_u32);
2781        let stat: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2782        // Same inner value, different variant → still equal / equal-hashing / equal-ordering.
2783        assert_eq!(heap, stat);
2784        assert_eq!(hash_of(&heap), hash_of(&stat));
2785        assert_eq!(heap.cmp(&stat), core::cmp::Ordering::Equal);
2786        assert_eq!(heap.partial_cmp(&stat), Some(core::cmp::Ordering::Equal));
2787
2788        let smaller = BoxOrStatic::heap(1_u32);
2789        assert!(smaller < heap);
2790    }
2791
2792    #[test]
2793    fn box_or_static_debug_and_display_render_the_inner_value() {
2794        let b = BoxOrStatic::heap(42_u32);
2795        assert_eq!(format!("{b:?}"), "42");
2796        assert_eq!(format!("{b}"), "42");
2797        let s: BoxOrStaticString = BoxOrStatic::heap(String::new().into());
2798        assert!(
2799            !format!("{s:?}").is_empty(),
2800            "Debug of an empty string payload is still well-formed"
2801        );
2802        // Hostile float payloads must not panic while formatting.
2803        for f in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, f64::MIN] {
2804            let bf = BoxOrStatic::heap(f);
2805            assert!(!format!("{bf:?}").is_empty());
2806            assert!(!format!("{bf}").is_empty());
2807        }
2808    }
2809
2810    #[test]
2811    fn box_or_static_default_is_a_heap_allocated_default() {
2812        let b: BoxOrStatic<u32> = BoxOrStatic::default();
2813        assert_eq!(*b.as_ref(), 0);
2814        assert!(matches!(b, BoxOrStatic::Boxed(_)));
2815        let s: BoxOrStaticString = BoxOrStatic::default();
2816        assert_eq!(s.as_ref().as_str(), "");
2817    }
2818
2819    #[test]
2820    fn box_or_static_into_inner_returns_the_payload() {
2821        assert_eq!(BoxOrStatic::heap(7_u32).into_inner(), 7);
2822        let stat: BoxOrStatic<u32> = BoxOrStatic::Static(&STATIC_U32 as *const u32);
2823        assert_eq!(stat.into_inner(), 0xDEAD_BEEF);
2824        let s: BoxOrStaticString = BoxOrStatic::heap("hello".to_string().into());
2825        assert_eq!(s.into_inner().as_str(), "hello");
2826    }
2827
2828    #[test]
2829    fn box_or_static_into_inner_must_not_leak_the_box() {
2830        use core::sync::atomic::{AtomicIsize, Ordering};
2831
2832        // Counts LIVE instances: +1 on construction, +1 on clone, -1 on drop.
2833        // Never touched by any other test, so parallel execution is safe.
2834        static LIVE: AtomicIsize = AtomicIsize::new(0);
2835
2836        struct Tracked(u32);
2837        impl Tracked {
2838            fn new(v: u32) -> Self {
2839                LIVE.fetch_add(1, Ordering::SeqCst);
2840                Self(v)
2841            }
2842        }
2843        impl Clone for Tracked {
2844            fn clone(&self) -> Self {
2845                LIVE.fetch_add(1, Ordering::SeqCst);
2846                Self(self.0)
2847            }
2848        }
2849        impl Drop for Tracked {
2850            fn drop(&mut self) {
2851                LIVE.fetch_sub(1, Ordering::SeqCst);
2852            }
2853        }
2854
2855        let boxed = BoxOrStatic::heap(Tracked::new(7));
2856        assert_eq!(LIVE.load(Ordering::SeqCst), 1);
2857
2858        let inner = boxed.into_inner();
2859        assert_eq!(inner.0, 7);
2860        drop(inner);
2861
2862        assert_eq!(
2863            LIVE.load(Ordering::SeqCst),
2864            0,
2865            "into_inner() on a Boxed variant leaks: it clones the payload and then \
2866             mem::forget(self), so `Drop for BoxOrStatic` never runs and the Box \
2867             (plus the T inside it) is never freed"
2868        );
2869    }
2870
2871    #[test]
2872    #[cfg(target_pointer_width = "64")]
2873    fn box_or_static_is_the_documented_16_bytes() {
2874        assert_eq!(size_of::<BoxOrStatic<u32>>(), 16);
2875        assert_eq!(size_of::<BoxOrStaticString>(), 16);
2876    }
2877
2878    // =====================================================================
2879    // CssPropertyValue
2880    // =====================================================================
2881
2882    #[test]
2883    fn css_property_value_predicates_are_mutually_exclusive() {
2884        for v in keyword_values() {
2885            let flags = [
2886                v.is_auto(),
2887                v.is_none(),
2888                v.is_initial(),
2889                v.is_inherit(),
2890                v.is_revert(),
2891                v.is_unset(),
2892            ];
2893            assert_eq!(
2894                flags.iter().filter(|b| **b).count(),
2895                1,
2896                "exactly one predicate must fire for {v:?}"
2897            );
2898            assert!(
2899                v.get_property().is_none(),
2900                "a keyword variant has no property"
2901            );
2902        }
2903
2904        let exact = CssPropertyValue::Exact(TestVal(1.0));
2905        assert!(
2906            !exact.is_auto()
2907                && !exact.is_none()
2908                && !exact.is_initial()
2909                && !exact.is_inherit()
2910                && !exact.is_revert()
2911                && !exact.is_unset(),
2912            "Exact must answer false to every keyword predicate"
2913        );
2914    }
2915
2916    #[test]
2917    fn css_property_value_predicates_pick_the_right_variant() {
2918        assert!(CssPropertyValue::<TestVal>::Auto.is_auto());
2919        assert!(CssPropertyValue::<TestVal>::None.is_none());
2920        assert!(CssPropertyValue::<TestVal>::Initial.is_initial());
2921        assert!(CssPropertyValue::<TestVal>::Inherit.is_inherit());
2922        assert!(CssPropertyValue::<TestVal>::Revert.is_revert());
2923        assert!(CssPropertyValue::<TestVal>::Unset.is_unset());
2924    }
2925
2926    #[test]
2927    fn css_property_value_get_property_and_get_property_owned_agree() {
2928        let exact = CssPropertyValue::Exact(TestVal(3.5));
2929        assert_eq!(exact.get_property(), Some(&TestVal(3.5)));
2930        assert_eq!(exact.get_property_owned(), Some(TestVal(3.5)));
2931        for v in keyword_values() {
2932            assert_eq!(v.get_property(), None);
2933            assert_eq!(v.get_property_owned(), None);
2934        }
2935    }
2936
2937    #[test]
2938    fn css_property_value_get_property_or_default_substitutes_only_auto_and_initial() {
2939        // Documented mapping: Auto/Initial fall back to T::default(); the remaining
2940        // keywords stay None (the cascade resolves them elsewhere).
2941        assert_eq!(
2942            CssPropertyValue::<TestVal>::Auto.get_property_or_default(),
2943            Some(TestVal::default())
2944        );
2945        assert_eq!(
2946            CssPropertyValue::<TestVal>::Initial.get_property_or_default(),
2947            Some(TestVal::default())
2948        );
2949        assert_eq!(
2950            CssPropertyValue::<TestVal>::None.get_property_or_default(),
2951            None
2952        );
2953        assert_eq!(
2954            CssPropertyValue::<TestVal>::Inherit.get_property_or_default(),
2955            None
2956        );
2957        assert_eq!(
2958            CssPropertyValue::<TestVal>::Revert.get_property_or_default(),
2959            None
2960        );
2961        assert_eq!(
2962            CssPropertyValue::<TestVal>::Unset.get_property_or_default(),
2963            None
2964        );
2965        assert_eq!(
2966            CssPropertyValue::Exact(TestVal(9.0)).get_property_or_default(),
2967            Some(TestVal(9.0))
2968        );
2969    }
2970
2971    #[test]
2972    fn css_property_value_default_is_exact_default() {
2973        assert_eq!(
2974            CssPropertyValue::<TestVal>::default(),
2975            CssPropertyValue::Exact(TestVal::default())
2976        );
2977        assert!(!CssPropertyValue::<TestVal>::default().is_auto());
2978    }
2979
2980    #[test]
2981    fn css_property_value_from_wraps_into_exact() {
2982        let v: CssPropertyValue<TestVal> = TestVal(2.0).into();
2983        assert_eq!(v, CssPropertyValue::Exact(TestVal(2.0)));
2984    }
2985
2986    #[test]
2987    fn css_property_value_keyword_serialization_is_the_css_keyword() {
2988        let cases: [(CssPropertyValue<TestVal>, &str); 6] = [
2989            (CssPropertyValue::Auto, "auto"),
2990            (CssPropertyValue::None, "none"),
2991            (CssPropertyValue::Initial, "initial"),
2992            (CssPropertyValue::Inherit, "inherit"),
2993            (CssPropertyValue::Revert, "revert"),
2994            (CssPropertyValue::Unset, "unset"),
2995        ];
2996        for (v, expected) in cases {
2997            assert_eq!(v.get_css_value_fmt(), expected);
2998            assert_eq!(
2999                format!("{v}"),
3000                expected,
3001                "Display and get_css_value_fmt must not diverge for keywords"
3002            );
3003        }
3004        let exact = CssPropertyValue::Exact(TestVal(1.5));
3005        assert_eq!(exact.get_css_value_fmt(), "1.5");
3006        assert_eq!(format!("{exact}"), "1.5");
3007    }
3008
3009    #[test]
3010    fn css_property_value_serializes_hostile_floats_without_panicking() {
3011        for f in [
3012            f32::NAN,
3013            f32::INFINITY,
3014            f32::NEG_INFINITY,
3015            f32::MIN,
3016            f32::MAX,
3017            -0.0,
3018            f32::MIN_POSITIVE,
3019        ] {
3020            let v = CssPropertyValue::Exact(TestVal(f));
3021            assert!(!v.get_css_value_fmt().is_empty());
3022            assert!(!format!("{v}").is_empty());
3023        }
3024    }
3025
3026    #[test]
3027    fn css_property_value_map_property_preserves_keyword_variants() {
3028        // The mapper must never run for a keyword variant…
3029        for v in keyword_values() {
3030            let before = format!("{v}");
3031            let mapped: CssPropertyValue<u32> =
3032                v.map_property(|_| panic!("map_fn must not run on a keyword variant"));
3033            assert_eq!(
3034                format!("{mapped}"),
3035                before,
3036                "the keyword must survive the map"
3037            );
3038        }
3039        // …and must run exactly once for Exact.
3040        let mapped = CssPropertyValue::Exact(TestVal(2.0)).map_property(|t| t.0 as u32);
3041        assert_eq!(mapped, CssPropertyValue::Exact(2_u32));
3042    }
3043
3044    #[test]
3045    fn css_property_value_map_property_handles_nan_and_type_changing_maps() {
3046        let mapped = CssPropertyValue::Exact(TestVal(f32::NAN)).map_property(|t| t.0.is_nan());
3047        assert_eq!(mapped, CssPropertyValue::Exact(true));
3048        // f32 -> i32 saturating cast on infinity must not trap.
3049        let mapped = CssPropertyValue::Exact(TestVal(f32::INFINITY)).map_property(|t| t.0 as i64);
3050        assert_eq!(mapped, CssPropertyValue::Exact(i64::MAX));
3051    }
3052
3053    // =====================================================================
3054    // CssRuleBlock
3055    // =====================================================================
3056
3057    #[test]
3058    fn css_rule_block_new_defaults_to_author_priority_and_no_conditions() {
3059        let decls = vec![
3060            CssDeclaration::Static(prop_width(1.0)),
3061            CssDeclaration::Static(prop_text_color(2)),
3062        ];
3063        let r = CssRuleBlock::new(
3064            CssPath::new(vec![CssPathSelector::Type(NodeTypeTag::Div)]),
3065            decls.clone(),
3066        );
3067        assert_eq!(r.priority, rule_priority::AUTHOR);
3068        assert!(r.conditions.as_ref().is_empty());
3069        assert_eq!(r.declarations.as_ref(), &decls[..]);
3070        assert_eq!(r.path.selectors.as_ref().len(), 1);
3071    }
3072
3073    #[test]
3074    fn css_rule_block_with_conditions_keeps_every_condition() {
3075        let conds: Vec<DynamicSelector> = (0..1_000)
3076            .map(|i| DynamicSelector::ContainerName(format!("c{i}").into()))
3077            .collect();
3078        let r = CssRuleBlock::with_conditions(CssPath::default(), Vec::new(), conds.clone());
3079        assert_eq!(r.conditions.as_ref().len(), 1_000);
3080        assert_eq!(r.conditions.as_ref(), &conds[..]);
3081        assert_eq!(
3082            r.priority,
3083            rule_priority::AUTHOR,
3084            "with_conditions must not change the layer"
3085        );
3086        assert!(r.declarations.as_ref().is_empty());
3087        assert!(r.path.selectors.as_ref().is_empty());
3088    }
3089
3090    #[test]
3091    fn css_rule_block_default_is_empty_and_ua_priority() {
3092        let r = CssRuleBlock::default();
3093        assert!(r.path.selectors.as_ref().is_empty());
3094        assert!(r.declarations.as_ref().is_empty());
3095        assert!(r.conditions.as_ref().is_empty());
3096        assert_eq!(r.priority, rule_priority::UA, "u8::default() == 0 == UA");
3097    }
3098
3099    #[test]
3100    fn rule_priority_slots_are_strictly_ordered() {
3101        const _: () = assert!(rule_priority::UA < rule_priority::SYSTEM);
3102        const _: () = assert!(rule_priority::SYSTEM < rule_priority::AUTHOR);
3103        const _: () = assert!(rule_priority::AUTHOR < rule_priority::INLINE);
3104        const _: () = assert!(rule_priority::INLINE < rule_priority::RUNTIME);
3105    }
3106
3107    // =====================================================================
3108    // NodeTypeTag — parse / serialize round trip
3109    // =====================================================================
3110
3111    const ALL_TAGS: &[NodeTypeTag] = {
3112        use NodeTypeTag::*;
3113        &[
3114            Html,
3115            Head,
3116            Body,
3117            Div,
3118            P,
3119            Article,
3120            Section,
3121            Nav,
3122            Aside,
3123            Header,
3124            Footer,
3125            Main,
3126            Figure,
3127            FigCaption,
3128            H1,
3129            H2,
3130            H3,
3131            H4,
3132            H5,
3133            H6,
3134            Br,
3135            Hr,
3136            Pre,
3137            BlockQuote,
3138            Address,
3139            Details,
3140            Summary,
3141            Dialog,
3142            Ul,
3143            Ol,
3144            Li,
3145            Dl,
3146            Dt,
3147            Dd,
3148            Menu,
3149            MenuItem,
3150            Dir,
3151            Table,
3152            Caption,
3153            THead,
3154            TBody,
3155            TFoot,
3156            Tr,
3157            Th,
3158            Td,
3159            ColGroup,
3160            Col,
3161            Form,
3162            FieldSet,
3163            Legend,
3164            Label,
3165            Input,
3166            Button,
3167            Select,
3168            OptGroup,
3169            SelectOption,
3170            TextArea,
3171            Output,
3172            Progress,
3173            Meter,
3174            DataList,
3175            Span,
3176            A,
3177            Em,
3178            Strong,
3179            B,
3180            I,
3181            U,
3182            S,
3183            Mark,
3184            Del,
3185            Ins,
3186            Code,
3187            Samp,
3188            Kbd,
3189            Var,
3190            Cite,
3191            Dfn,
3192            Abbr,
3193            Acronym,
3194            Q,
3195            Time,
3196            Sub,
3197            Sup,
3198            Small,
3199            Big,
3200            Bdo,
3201            Bdi,
3202            Wbr,
3203            Ruby,
3204            Rt,
3205            Rtc,
3206            Rp,
3207            Data,
3208            Canvas,
3209            Object,
3210            Param,
3211            Embed,
3212            Audio,
3213            Video,
3214            Source,
3215            Track,
3216            Map,
3217            Area,
3218            Svg,
3219            SvgPath,
3220            SvgCircle,
3221            SvgRect,
3222            SvgEllipse,
3223            SvgLine,
3224            SvgPolygon,
3225            SvgPolyline,
3226            SvgG,
3227            SvgDefs,
3228            SvgSymbol,
3229            SvgUse,
3230            SvgSwitch,
3231            SvgText,
3232            SvgTspan,
3233            SvgTextPath,
3234            SvgLinearGradient,
3235            SvgRadialGradient,
3236            SvgStop,
3237            SvgPattern,
3238            SvgClipPathElement,
3239            SvgMask,
3240            SvgFilter,
3241            SvgFeBlend,
3242            SvgFeColorMatrix,
3243            SvgFeComponentTransfer,
3244            SvgFeComposite,
3245            SvgFeConvolveMatrix,
3246            SvgFeDiffuseLighting,
3247            SvgFeDisplacementMap,
3248            SvgFeDistantLight,
3249            SvgFeDropShadow,
3250            SvgFeFlood,
3251            SvgFeFuncR,
3252            SvgFeFuncG,
3253            SvgFeFuncB,
3254            SvgFeFuncA,
3255            SvgFeGaussianBlur,
3256            SvgFeImage,
3257            SvgFeMerge,
3258            SvgFeMergeNode,
3259            SvgFeMorphology,
3260            SvgFeOffset,
3261            SvgFePointLight,
3262            SvgFeSpecularLighting,
3263            SvgFeSpotLight,
3264            SvgFeTile,
3265            SvgFeTurbulence,
3266            SvgMarker,
3267            SvgImage,
3268            SvgForeignObject,
3269            SvgTitle,
3270            SvgDesc,
3271            SvgMetadata,
3272            SvgA,
3273            SvgView,
3274            SvgStyle,
3275            SvgScript,
3276            SvgAnimate,
3277            SvgAnimateMotion,
3278            SvgAnimateTransform,
3279            SvgSet,
3280            SvgMpath,
3281            Title,
3282            Meta,
3283            Link,
3284            Script,
3285            Style,
3286            Base,
3287            Text,
3288            Img,
3289            VirtualView,
3290            TransientWindow,
3291            Icon,
3292            GeolocationProbe,
3293            Before,
3294            After,
3295            Marker,
3296            Placeholder,
3297        ]
3298    };
3299
3300    #[test]
3301    fn node_type_tag_variant_list_is_complete_and_unique() {
3302        // Guard rail for the round-trip tests below: if a variant is added to the enum
3303        // without being added here, this count check fails and points at the omission.
3304        assert_eq!(
3305            ALL_TAGS.len(),
3306            183, // +TransientWindow (2026-08-22)
3307            "ALL_TAGS is out of sync with the NodeTypeTag enum"
3308        );
3309        let mut seen: Vec<NodeTypeTag> = Vec::new();
3310        for t in ALL_TAGS {
3311            assert!(!seen.contains(t), "duplicate entry in ALL_TAGS: {t:?}");
3312            seen.push(*t);
3313        }
3314    }
3315
3316    #[test]
3317    fn node_type_tag_display_names_are_all_distinct() {
3318        let mut names: Vec<String> = ALL_TAGS.iter().map(ToString::to_string).collect();
3319        names.sort();
3320        let before = names.len();
3321        names.dedup();
3322        assert_eq!(
3323            names.len(),
3324            before,
3325            "two NodeTypeTag variants serialize to the same CSS tag name — \
3326             the string is then ambiguous on the way back in"
3327        );
3328    }
3329
3330    #[test]
3331    fn node_type_tag_display_then_from_str_round_trips_every_variant() {
3332        let mut broken: Vec<(NodeTypeTag, String)> = Vec::new();
3333        for tag in ALL_TAGS {
3334            let serialized = tag.to_string();
3335            // Resolve to a bool first: the Result borrows `serialized`, so `serialized`
3336            // cannot be moved into `broken` while that borrow is still live.
3337            let round_trips = matches!(NodeTypeTag::from_str(&serialized), Ok(t) if t == *tag);
3338            if !round_trips {
3339                broken.push((*tag, serialized));
3340            }
3341        }
3342        assert!(
3343            broken.is_empty(),
3344            "from_str(Display(tag)) must yield tag back, but these variants do not \
3345             round-trip: {broken:?}"
3346        );
3347    }
3348
3349    #[test]
3350    fn node_type_tag_serialize_parse_serialize_is_stable() {
3351        // Idempotent normalization: for every variant that parses back at all, a second
3352        // serialize must produce a byte-identical string.
3353        for tag in ALL_TAGS {
3354            let once = tag.to_string();
3355            if let Ok(parsed) = NodeTypeTag::from_str(&once) {
3356                assert_eq!(
3357                    parsed.to_string(),
3358                    once,
3359                    "serialize(parse(serialize({tag:?}))) drifted"
3360                );
3361            }
3362        }
3363    }
3364
3365    #[test]
3366    fn node_type_tag_from_str_valid_minimal() {
3367        assert_eq!(NodeTypeTag::from_str("div"), Ok(NodeTypeTag::Div));
3368        assert_eq!(NodeTypeTag::from_str("p"), Ok(NodeTypeTag::P));
3369        assert_eq!(NodeTypeTag::from_str("a"), Ok(NodeTypeTag::A));
3370    }
3371
3372    #[test]
3373    fn node_type_tag_from_str_accepts_documented_aliases() {
3374        // Two spellings, one variant — and the canonical spelling is what comes back out.
3375        assert_eq!(NodeTypeTag::from_str("image"), Ok(NodeTypeTag::SvgImage));
3376        assert_eq!(
3377            NodeTypeTag::from_str("svg:image"),
3378            Ok(NodeTypeTag::SvgImage)
3379        );
3380        assert_eq!(NodeTypeTag::SvgImage.to_string(), "svg:image");
3381
3382        assert_eq!(
3383            NodeTypeTag::from_str("iframe"),
3384            Ok(NodeTypeTag::VirtualView)
3385        );
3386        assert_eq!(
3387            NodeTypeTag::from_str("virtual-view"),
3388            Ok(NodeTypeTag::VirtualView)
3389        );
3390
3391        for (bare, prefixed, tag) in [
3392            ("before", "::before", NodeTypeTag::Before),
3393            ("after", "::after", NodeTypeTag::After),
3394            ("marker", "::marker", NodeTypeTag::Marker),
3395            ("placeholder", "::placeholder", NodeTypeTag::Placeholder),
3396        ] {
3397            assert_eq!(NodeTypeTag::from_str(bare), Ok(tag));
3398            assert_eq!(NodeTypeTag::from_str(prefixed), Ok(tag));
3399            assert_eq!(
3400                tag.to_string(),
3401                prefixed,
3402                "pseudo-elements must serialize in their `::` form"
3403            );
3404        }
3405    }
3406
3407    #[test]
3408    fn node_type_tag_from_str_rejects_hostile_input_without_panicking() {
3409        let long = "a".repeat(1_000_000);
3410        let nested = "<".repeat(10_000);
3411        let hostile = [
3412            "",            // empty
3413            " ",           // whitespace only
3414            "   \t\n\r  ", // whitespace only, mixed
3415            " div",        // leading junk — no trimming
3416            "div ",        // trailing junk
3417            "  div  ",     // both
3418            "div;garbage", // trailing garbage
3419            "DIV",         // wrong case — CSS tag matching here is case-sensitive
3420            "Div",
3421            "0",
3422            "-0",
3423            "9223372036854775807", // i64::MAX
3424            "1e400",
3425            "NaN",
3426            "inf",
3427            "-inf",
3428            "\u{1F600}",   // emoji
3429            "e\u{0301}",   // combining mark
3430            "\u{0}",       // NUL
3431            "\u{FEFF}div", // BOM prefix
3432            "div\u{0}",
3433            "*",
3434            "::",
3435            ":::before",
3436            "<script>",
3437            long.as_str(),
3438            nested.as_str(),
3439        ];
3440        for input in hostile {
3441            match NodeTypeTag::from_str(input) {
3442                Ok(t) => panic!("{input:?} must not parse, but produced {t:?}"),
3443                Err(NodeTypeTagParseError::Invalid(echoed)) => {
3444                    assert_eq!(echoed, input, "the error must echo the input verbatim");
3445                }
3446            }
3447        }
3448    }
3449
3450    #[test]
3451    fn node_type_tag_parse_error_display_names_the_offending_input() {
3452        let e = NodeTypeTagParseError::Invalid("wat");
3453        assert_eq!(format!("{e}"), "Invalid node type: wat");
3454        // Empty / unicode payloads must still format cleanly.
3455        assert_eq!(
3456            format!("{}", NodeTypeTagParseError::Invalid("")),
3457            "Invalid node type: "
3458        );
3459        assert!(format!("{}", NodeTypeTagParseError::Invalid("😀")).contains('😀'));
3460    }
3461
3462    #[test]
3463    fn node_type_tag_parse_error_to_contained_to_shared_round_trips() {
3464        let long = "x".repeat(10_000);
3465        for s in ["", "   ", "div", "😀", "e\u{0301}", "\u{0}", long.as_str()] {
3466            let shared = NodeTypeTagParseError::Invalid(s);
3467            let owned = shared.to_contained();
3468            assert_eq!(
3469                owned,
3470                NodeTypeTagParseErrorOwned::Invalid(s.to_string().into())
3471            );
3472            assert_eq!(
3473                owned.to_shared(),
3474                shared,
3475                "to_shared(to_contained(x)) must equal x"
3476            );
3477            // …and the owned form still renders the same message.
3478            assert_eq!(format!("{}", owned.to_shared()), format!("{shared}"));
3479        }
3480    }
3481
3482    // =====================================================================
3483    // CssPath / CssScopeRange
3484    // =====================================================================
3485
3486    #[test]
3487    fn css_path_new_preserves_selectors_including_empty() {
3488        assert!(CssPath::new(Vec::new()).selectors.as_ref().is_empty());
3489        assert_eq!(CssPath::new(Vec::new()), CssPath::default());
3490
3491        let sels = vec![
3492            CssPathSelector::Type(NodeTypeTag::Div),
3493            CssPathSelector::DirectChildren,
3494            CssPathSelector::Class("c".to_string().into()),
3495        ];
3496        let p = CssPath::new(sels.clone());
3497        assert_eq!(p.selectors.as_ref(), &sels[..]);
3498    }
3499
3500    #[test]
3501    fn css_path_display_and_debug_agree_and_compose() {
3502        let p = CssPath::new(vec![
3503            CssPathSelector::Type(NodeTypeTag::Div),
3504            CssPathSelector::Id("id".to_string().into()),
3505            CssPathSelector::Class("cls".to_string().into()),
3506            CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
3507        ]);
3508        assert_eq!(format!("{p}"), "div#id.cls:hover");
3509        assert_eq!(
3510            format!("{p:?}"),
3511            format!("{p}"),
3512            "Debug delegates to Display"
3513        );
3514        // An empty path renders as the empty string — deterministic, no panic.
3515        assert_eq!(format!("{}", CssPath::default()), "");
3516    }
3517
3518    #[test]
3519    fn push_front_scope_scopes_a_bare_star_rule_to_the_node_only() {
3520        // A bare `*` path is the parse_inline wrapper for inline styles → node-only.
3521        let mut p = CssPath::new(vec![CssPathSelector::Global]);
3522        p.push_front_scope(5, 9);
3523        assert_eq!(
3524            p.selectors.as_ref(),
3525            &[
3526                CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
3527                CssPathSelector::Global,
3528            ][..],
3529            "inline style must not leak past the owner node (#47)"
3530        );
3531    }
3532
3533    #[test]
3534    fn push_front_scope_scopes_a_real_selector_to_the_whole_subtree() {
3535        let mut p = CssPath::new(vec![CssPathSelector::Class("menu-item".to_string().into())]);
3536        p.push_front_scope(5, 9);
3537        assert_eq!(
3538            p.selectors.as_ref()[0],
3539            CssPathSelector::Root(CssScopeRange { start: 5, end: 9 })
3540        );
3541        assert_eq!(p.selectors.as_ref().len(), 2);
3542    }
3543
3544    #[test]
3545    fn push_front_scope_on_an_empty_path_uses_the_subtree_range() {
3546        // len() != 1 → not the bare-`*` case → subtree scope.
3547        let mut p = CssPath::default();
3548        p.push_front_scope(2, 8);
3549        assert_eq!(
3550            p.selectors.as_ref(),
3551            &[CssPathSelector::Root(CssScopeRange { start: 2, end: 8 })][..]
3552        );
3553    }
3554
3555    #[test]
3556    fn push_front_scope_at_numeric_boundaries_does_not_panic() {
3557        // 0, usize::MAX, and an INVERTED range (start > end) — the function does no
3558        // arithmetic, so all three must be stored verbatim without overflow.
3559        for (start, end) in [
3560            (0_usize, 0_usize),
3561            (0, usize::MAX),
3562            (usize::MAX, usize::MAX),
3563            (usize::MAX, 0), // inverted: end < start
3564            (9, 5),          // inverted
3565        ] {
3566            let mut p = CssPath::new(vec![CssPathSelector::Class("c".to_string().into())]);
3567            p.push_front_scope(start, end);
3568            assert_eq!(
3569                p.selectors.as_ref()[0],
3570                CssPathSelector::Root(CssScopeRange { start, end })
3571            );
3572        }
3573        // The bare-`*` branch clamps `end` to `start`, so it can never be inverted.
3574        let mut g = CssPath::new(vec![CssPathSelector::Global]);
3575        g.push_front_scope(usize::MAX, 0);
3576        assert_eq!(
3577            g.selectors.as_ref()[0],
3578            CssPathSelector::Root(CssScopeRange {
3579                start: usize::MAX,
3580                end: usize::MAX
3581            })
3582        );
3583    }
3584
3585    #[test]
3586    fn push_front_scope_applied_twice_stacks_root_selectors() {
3587        let mut p = CssPath::new(vec![CssPathSelector::Global]);
3588        p.push_front_scope(5, 9);
3589        // Now the path is [Root, Global] (len 2) → no longer the bare-`*` case, so the
3590        // second call scopes to the full subtree rather than node-only.
3591        p.push_front_scope(1, 20);
3592        assert_eq!(
3593            p.selectors.as_ref(),
3594            &[
3595                CssPathSelector::Root(CssScopeRange { start: 1, end: 20 }),
3596                CssPathSelector::Root(CssScopeRange { start: 5, end: 5 }),
3597                CssPathSelector::Global,
3598            ][..]
3599        );
3600    }
3601
3602    #[test]
3603    fn push_front_scope_on_a_long_path_preserves_order_and_length() {
3604        let sels: Vec<CssPathSelector> = (0..5_000)
3605            .map(|i| CssPathSelector::Class(format!("c{i}").into()))
3606            .collect();
3607        let mut p = CssPath::new(sels.clone());
3608        p.push_front_scope(3, 4);
3609        assert_eq!(p.selectors.as_ref().len(), 5_001);
3610        assert_eq!(
3611            p.selectors.as_ref()[0],
3612            CssPathSelector::Root(CssScopeRange { start: 3, end: 4 })
3613        );
3614        assert_eq!(
3615            &p.selectors.as_ref()[1..],
3616            &sels[..],
3617            "the tail must be untouched"
3618        );
3619    }
3620
3621    #[test]
3622    fn scope_range_contains_at_zero_and_usize_max() {
3623        let zero = CssScopeRange { start: 0, end: 0 };
3624        assert!(zero.contains(0));
3625        assert!(!zero.contains(1));
3626        assert!(!zero.contains(usize::MAX));
3627
3628        let full = CssScopeRange {
3629            start: 0,
3630            end: usize::MAX,
3631        };
3632        assert!(full.contains(0));
3633        assert!(full.contains(usize::MAX));
3634        assert!(full.contains(usize::MAX / 2));
3635
3636        let top = CssScopeRange {
3637            start: usize::MAX,
3638            end: usize::MAX,
3639        };
3640        assert!(top.contains(usize::MAX));
3641        assert!(!top.contains(usize::MAX - 1));
3642        assert!(!top.contains(0));
3643    }
3644
3645    #[test]
3646    fn scope_range_inverted_contains_nothing() {
3647        // start > end is nonsense but must degrade to "matches nothing", not panic.
3648        let inverted = CssScopeRange { start: 9, end: 5 };
3649        for n in [0_usize, 4, 5, 7, 9, 10, usize::MAX] {
3650            assert!(!inverted.contains(n), "inverted range must never match {n}");
3651        }
3652    }
3653
3654    // =====================================================================
3655    // Selector serializers
3656    // =====================================================================
3657
3658    #[test]
3659    fn css_path_selector_display_covers_every_variant() {
3660        let cases: Vec<(CssPathSelector, String)> = vec![
3661            (CssPathSelector::Global, "*".to_string()),
3662            (
3663                CssPathSelector::Root(CssScopeRange { start: 2, end: 6 }),
3664                ":root(2..=6)".to_string(),
3665            ),
3666            (CssPathSelector::Type(NodeTypeTag::Div), "div".to_string()),
3667            (
3668                CssPathSelector::Class("c".to_string().into()),
3669                ".c".to_string(),
3670            ),
3671            (
3672                CssPathSelector::Id("i".to_string().into()),
3673                "#i".to_string(),
3674            ),
3675            (
3676                CssPathSelector::PseudoSelector(CssPathPseudoSelector::Focus),
3677                ":focus".to_string(),
3678            ),
3679            (
3680                CssPathSelector::Attribute(CssAttributeSelector::default()),
3681                "[]".to_string(),
3682            ),
3683            (CssPathSelector::DirectChildren, ">".to_string()),
3684            (CssPathSelector::Children, " ".to_string()),
3685            (CssPathSelector::AdjacentSibling, "+".to_string()),
3686            (CssPathSelector::GeneralSibling, "~".to_string()),
3687        ];
3688        for (sel, expected) in cases {
3689            assert_eq!(format!("{sel}"), expected);
3690        }
3691        assert_eq!(CssPathSelector::default(), CssPathSelector::Global);
3692    }
3693
3694    #[test]
3695    fn css_path_selector_display_at_scope_range_boundaries() {
3696        let s = CssPathSelector::Root(CssScopeRange {
3697            start: 0,
3698            end: usize::MAX,
3699        });
3700        assert_eq!(format!("{s}"), format!(":root(0..={})", usize::MAX));
3701    }
3702
3703    #[test]
3704    fn css_path_selector_display_with_empty_and_unicode_names() {
3705        // Empty class/id names produce a bare `.` / `#` — degenerate but deterministic.
3706        assert_eq!(
3707            format!("{}", CssPathSelector::Class(String::new().into())),
3708            "."
3709        );
3710        assert_eq!(
3711            format!("{}", CssPathSelector::Id(String::new().into())),
3712            "#"
3713        );
3714        assert_eq!(
3715            format!("{}", CssPathSelector::Class("😀".to_string().into())),
3716            ".😀"
3717        );
3718        let long = "x".repeat(100_000);
3719        assert_eq!(
3720            format!("{}", CssPathSelector::Id(long.clone().into())).len(),
3721            long.len() + 1
3722        );
3723    }
3724
3725    #[test]
3726    fn attribute_match_op_symbol_prefix_matrix() {
3727        assert_eq!(AttributeMatchOp::Exists.symbol_prefix(), "");
3728        assert_eq!(AttributeMatchOp::Eq.symbol_prefix(), "");
3729        assert_eq!(AttributeMatchOp::Includes.symbol_prefix(), "~");
3730        assert_eq!(AttributeMatchOp::DashMatch.symbol_prefix(), "|");
3731        assert_eq!(AttributeMatchOp::Prefix.symbol_prefix(), "^");
3732        assert_eq!(AttributeMatchOp::Suffix.symbol_prefix(), "$");
3733        assert_eq!(AttributeMatchOp::Substring.symbol_prefix(), "*");
3734        assert_eq!(AttributeMatchOp::default(), AttributeMatchOp::Exists);
3735    }
3736
3737    #[test]
3738    fn css_attribute_selector_display_renders_each_operator() {
3739        let ops = [
3740            (AttributeMatchOp::Eq, "[a=\"v\"]"),
3741            (AttributeMatchOp::Includes, "[a~=\"v\"]"),
3742            (AttributeMatchOp::DashMatch, "[a|=\"v\"]"),
3743            (AttributeMatchOp::Prefix, "[a^=\"v\"]"),
3744            (AttributeMatchOp::Suffix, "[a$=\"v\"]"),
3745            (AttributeMatchOp::Substring, "[a*=\"v\"]"),
3746        ];
3747        for (op, expected) in ops {
3748            let sel = CssAttributeSelector {
3749                name: "a".to_string().into(),
3750                op,
3751                value: OptionString::Some("v".to_string().into()),
3752            };
3753            assert_eq!(format!("{sel}"), expected);
3754        }
3755    }
3756
3757    #[test]
3758    fn css_attribute_selector_exists_ignores_any_value() {
3759        // `[attr]` has no right-hand side, so a stray value must be dropped, not printed.
3760        let sel = CssAttributeSelector {
3761            name: "a".to_string().into(),
3762            op: AttributeMatchOp::Exists,
3763            value: OptionString::Some("ignored".to_string().into()),
3764        };
3765        assert_eq!(format!("{sel}"), "[a]");
3766        assert_eq!(format!("{}", CssAttributeSelector::default()), "[]");
3767    }
3768
3769    #[test]
3770    fn css_attribute_selector_missing_value_renders_an_empty_string_literal() {
3771        let sel = CssAttributeSelector {
3772            name: "a".to_string().into(),
3773            op: AttributeMatchOp::Eq,
3774            value: OptionString::None,
3775        };
3776        assert_eq!(format!("{sel}"), "[a=\"\"]");
3777    }
3778
3779    #[test]
3780    fn css_attribute_selector_display_with_hostile_names_and_values_does_not_panic() {
3781        for (name, value) in [
3782            ("", ""),
3783            ("😀", "😀"),
3784            ("a\u{0301}", "e\u{0301}"),
3785            ("a", "has \" quote"), // NOTE: not escaped — see report
3786            ("a", "]"),
3787            ("a", "\u{0}"),
3788            ("a", "\n"),
3789        ] {
3790            let sel = CssAttributeSelector {
3791                name: name.to_string().into(),
3792                op: AttributeMatchOp::Eq,
3793                value: OptionString::Some(value.to_string().into()),
3794            };
3795            let out = format!("{sel}");
3796            assert!(out.starts_with('[') && out.ends_with(']'));
3797        }
3798    }
3799
3800    #[test]
3801    fn css_nth_child_selector_display_at_numeric_boundaries() {
3802        assert_eq!(format!("{}", CssNthChildSelector::Number(0)), "0");
3803        assert_eq!(
3804            format!("{}", CssNthChildSelector::Number(u32::MAX)),
3805            u32::MAX.to_string()
3806        );
3807        assert_eq!(format!("{}", CssNthChildSelector::Even), "even");
3808        assert_eq!(format!("{}", CssNthChildSelector::Odd), "odd");
3809        assert_eq!(
3810            format!(
3811                "{}",
3812                CssNthChildSelector::Pattern(CssNthChildPattern {
3813                    pattern_repeat: 2,
3814                    offset: 1,
3815                })
3816            ),
3817            "2n + 1"
3818        );
3819        // A=0 / A=B=u32::MAX must not overflow or panic while formatting.
3820        assert_eq!(
3821            format!(
3822                "{}",
3823                CssNthChildSelector::Pattern(CssNthChildPattern {
3824                    pattern_repeat: 0,
3825                    offset: 0,
3826                })
3827            ),
3828            "0n + 0"
3829        );
3830        let maxed = CssNthChildSelector::Pattern(CssNthChildPattern {
3831            pattern_repeat: u32::MAX,
3832            offset: u32::MAX,
3833        });
3834        assert_eq!(format!("{maxed}"), format!("{}n + {}", u32::MAX, u32::MAX));
3835    }
3836
3837    #[test]
3838    fn css_path_pseudo_selector_display_covers_every_variant() {
3839        let cases: Vec<(CssPathPseudoSelector, String)> = vec![
3840            (CssPathPseudoSelector::First, "first".to_string()),
3841            (CssPathPseudoSelector::Last, "last".to_string()),
3842            (
3843                CssPathPseudoSelector::NthChild(CssNthChildSelector::Even),
3844                "nth-child(even)".to_string(),
3845            ),
3846            (CssPathPseudoSelector::Hover, "hover".to_string()),
3847            (CssPathPseudoSelector::Active, "active".to_string()),
3848            (CssPathPseudoSelector::Focus, "focus".to_string()),
3849            (CssPathPseudoSelector::SeatFocus, "seat-focus".to_string()),
3850            (
3851                CssPathPseudoSelector::Lang("de-DE".to_string().into()),
3852                "lang(de-DE)".to_string(),
3853            ),
3854            (CssPathPseudoSelector::Backdrop, "backdrop".to_string()),
3855            (CssPathPseudoSelector::Dragging, "dragging".to_string()),
3856            (CssPathPseudoSelector::DragOver, "drag-over".to_string()),
3857        ];
3858        for (p, expected) in cases {
3859            assert_eq!(format!("{p}"), expected);
3860        }
3861    }
3862
3863    #[test]
3864    fn css_path_pseudo_selector_lang_with_hostile_payloads_does_not_panic() {
3865        let long = "l".repeat(100_000);
3866        for lang in ["", "   ", "😀", "e\u{0301}", ")", "\u{0}", long.as_str()] {
3867            let p = CssPathPseudoSelector::Lang(lang.to_string().into());
3868            let out = format!("{p}");
3869            assert!(out.starts_with("lang(") && out.ends_with(')'));
3870        }
3871    }
3872
3873    // =====================================================================
3874    // get_specificity
3875    // =====================================================================
3876
3877    #[test]
3878    fn get_specificity_of_an_empty_path_is_all_zero() {
3879        assert_eq!(get_specificity(&CssPath::default()), (0, 0, 0, 0));
3880        assert_eq!(get_specificity(&CssPath::new(Vec::new())), (0, 0, 0, 0));
3881    }
3882
3883    #[test]
3884    fn get_specificity_counts_ids_classes_and_types_separately() {
3885        let path = CssPath::new(vec![
3886            CssPathSelector::Id("a".to_string().into()),
3887            CssPathSelector::Id("b".to_string().into()),
3888            CssPathSelector::Class("c".to_string().into()),
3889            CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
3890            CssPathSelector::Attribute(CssAttributeSelector::default()),
3891            CssPathSelector::Type(NodeTypeTag::Div),
3892        ]);
3893        // pseudo-classes and attribute selectors count in the CLASS column (per W3C).
3894        assert_eq!(get_specificity(&path), (2, 3, 1, 6));
3895    }
3896
3897    #[test]
3898    fn get_specificity_ignores_combinators_and_root_except_in_the_total() {
3899        let path = CssPath::new(vec![
3900            CssPathSelector::Root(CssScopeRange { start: 0, end: 9 }),
3901            CssPathSelector::Global,
3902            CssPathSelector::Children,
3903            CssPathSelector::DirectChildren,
3904            CssPathSelector::AdjacentSibling,
3905            CssPathSelector::GeneralSibling,
3906        ]);
3907        let (ids, classes, types, total) = get_specificity(&path);
3908        assert_eq!((ids, classes, types), (0, 0, 0));
3909        assert_eq!(total, 6, "the 4th field is the raw selector count");
3910    }
3911
3912    #[test]
3913    fn get_specificity_orders_ids_above_classes_above_types() {
3914        let id = get_specificity(&CssPath::new(vec![CssPathSelector::Id(
3915            "x".to_string().into(),
3916        )]));
3917        let class = get_specificity(&CssPath::new(vec![CssPathSelector::Class(
3918            "x".to_string().into(),
3919        )]));
3920        let ty = get_specificity(&CssPath::new(vec![CssPathSelector::Type(NodeTypeTag::Div)]));
3921        let star = get_specificity(&CssPath::new(vec![CssPathSelector::Global]));
3922        assert!(star < ty, "* must be the weakest");
3923        assert!(ty < class);
3924        assert!(class < id);
3925    }
3926
3927    #[test]
3928    fn get_specificity_on_a_huge_path_does_not_overflow_or_hang() {
3929        let sels: Vec<CssPathSelector> = (0..50_000)
3930            .map(|i| CssPathSelector::Id(format!("i{i}").into()))
3931            .collect();
3932        let path = CssPath::new(sels);
3933        assert_eq!(get_specificity(&path), (50_000, 0, 0, 50_000));
3934    }
3935
3936    // =====================================================================
3937    // Parsers (feature = "parser")
3938    // =====================================================================
3939
3940    #[cfg(feature = "parser")]
3941    fn parse(s: &str) -> Css {
3942        Css::from_string(s.to_string().into())
3943    }
3944
3945    #[cfg(feature = "parser")]
3946    #[test]
3947    fn viewport_breakpoints_harvests_media_bounds() {
3948        let css = Css::from_string(
3949            "@media (max-width: 400px) { .a { width: 10px; } }\n\
3950             @media (min-width: 800px) { .b { width: 10px; } }\n\
3951             @media (max-height: 300px) { .c { width: 10px; } }\n\
3952             .d { width: 10px; }"
3953                .into(),
3954        );
3955        let (w, h) = css.viewport_breakpoints();
3956        assert_eq!(w, vec![400.0, 800.0]);
3957        assert_eq!(h, vec![300.0]);
3958    }
3959
3960    #[test]
3961    fn from_string_on_empty_and_whitespace_only_input_yields_no_rules() {
3962        for input in ["", " ", "   ", "\t\n\r\n ", "\u{FEFF}", "\u{00A0}"] {
3963            let css = parse(input);
3964            assert!(
3965                css.is_empty(),
3966                "{input:?} must produce zero rules, got {}",
3967                css.rules.as_ref().len()
3968            );
3969        }
3970    }
3971
3972    #[cfg(feature = "parser")]
3973    #[test]
3974    fn from_string_on_garbage_never_panics_and_is_deterministic() {
3975        let garbage = [
3976            "}}}}",
3977            "{{{{",
3978            "@@@@",
3979            ";;;;",
3980            "\u{0}\u{1}\u{2}",
3981            "div {",
3982            "div }",
3983            "} div {",
3984            "div { color",
3985            "div { color: }",
3986            "div { : red; }",
3987            "* * * * *",
3988            ":::::",
3989            "[[[[",
3990            "/* unterminated comment",
3991            "@media {",
3992            "url(",
3993            "\"unterminated",
3994            "div{color:red;}}}}",
3995        ];
3996        for input in garbage {
3997            let a = parse(input);
3998            let b = parse(input);
3999            assert_eq!(a, b, "parsing {input:?} must be deterministic");
4000        }
4001    }
4002
4003    #[cfg(feature = "parser")]
4004    #[test]
4005    fn from_string_valid_minimal_produces_one_author_rule() {
4006        let css = parse("div { width: 200px; }");
4007        assert_eq!(css.rules.as_ref().len(), 1);
4008        let rule = &css.rules.as_ref()[0];
4009        assert_eq!(
4010            rule.priority,
4011            rule_priority::AUTHOR,
4012            "parser output belongs to the author layer"
4013        );
4014        assert_eq!(
4015            rule.path.selectors.as_ref(),
4016            &[CssPathSelector::Type(NodeTypeTag::Div)][..]
4017        );
4018        let props: Vec<CssPropertyType> = css
4019            .iter_inline_properties()
4020            .map(|(p, _)| p.get_type())
4021            .collect();
4022        assert_eq!(props, vec![CssPropertyType::Width]);
4023    }
4024
4025    #[cfg(feature = "parser")]
4026    #[test]
4027    fn from_string_handles_leading_and_trailing_junk_deterministically() {
4028        // Surrounding whitespace must be irrelevant.
4029        assert_eq!(
4030            parse("  div { width: 1px; }  "),
4031            parse("div { width: 1px; }")
4032        );
4033        // Trailing garbage must not eat the valid rule that precedes it.
4034        let with_junk = parse("div { width: 1px; } @@@ garbage");
4035        assert!(
4036            with_junk.rules().any(
4037                |r| r.path.selectors.as_ref() == &[CssPathSelector::Type(NodeTypeTag::Div)][..]
4038            ),
4039            "the leading valid rule must survive trailing junk"
4040        );
4041    }
4042
4043    #[cfg(feature = "parser")]
4044    #[test]
4045    fn from_string_on_boundary_numbers_does_not_panic() {
4046        let inputs = [
4047            "div { width: 0px; }",
4048            "div { width: -0px; }",
4049            "div { width: -1px; }",
4050            "div { width: 9223372036854775807px; }", // i64::MAX
4051            "div { width: 340282350000000000000000000000000000000px; }", // ~f32::MAX
4052            "div { width: 1e400px; }",               // f64 overflow → inf
4053            "div { width: 1e-400px; }",              // f64 underflow → 0
4054            "div { width: NaN; }",
4055            "div { width: inf; }",
4056            "div { width: -inf; }",
4057            "div { width: 99999999999999999999999999px; }",
4058            "div { opacity: 1e309; }",
4059            "div { z-index: -9223372036854775808; }", // i64::MIN
4060            "div { width: .....; }",
4061            "div { width: --5px; }",
4062        ];
4063        for input in inputs {
4064            let css = parse(input);
4065            assert!(
4066                css.rules.as_ref().len() <= 1,
4067                "{input:?} must not explode into multiple rules"
4068            );
4069        }
4070    }
4071
4072    #[cfg(feature = "parser")]
4073    #[test]
4074    fn from_string_on_unicode_input_does_not_panic() {
4075        let inputs = [
4076            "div { content: \"😀\"; }",
4077            ".😀 { width: 1px; }",
4078            "#e\u{0301} { width: 1px; }",
4079            "div { font-family: \"日本語\"; }",
4080            "div\u{0301} { width: 1px; }",
4081            "div { width: 1px; } /* 🎉 */",
4082            "\u{202E}div { width: 1px; }", // right-to-left override
4083        ];
4084        for input in inputs {
4085            let css = parse(input);
4086            // The invariant is "no panic + stable output", not any particular rule count.
4087            assert_eq!(css, parse(input));
4088        }
4089    }
4090
4091    #[cfg(feature = "parser")]
4092    #[test]
4093    fn from_string_on_a_one_megabyte_input_terminates() {
4094        // 100k declarations inside a single rule ≈ 1_000_000 chars.
4095        let body = "width:1px;".repeat(100_000);
4096        assert!(body.len() >= 1_000_000);
4097        let css = parse(&format!("div{{{body}}}"));
4098        assert_eq!(css.rules.as_ref().len(), 1);
4099        assert!(!css.rules.as_ref()[0].declarations.as_ref().is_empty());
4100
4101        // …and a single 100k-char junk token must not hang either.
4102        let junk = "a".repeat(100_000);
4103        let _ = parse(&junk);
4104    }
4105
4106    #[cfg(feature = "parser")]
4107    #[test]
4108    fn from_string_on_deeply_nested_blocks_does_not_stack_overflow() {
4109        // Run on a dedicated 64 MiB stack: a recursive-descent parser would blow the
4110        // default 2 MiB test stack and take the whole test binary down with it.
4111        let depth = 10_000;
4112        let mut s = String::with_capacity(depth * 8);
4113        for _ in 0..depth {
4114            s.push_str("div{");
4115        }
4116        s.push_str("width:1px;");
4117        for _ in 0..depth {
4118            s.push('}');
4119        }
4120
4121        let handle = std::thread::Builder::new()
4122            .stack_size(64 * 1024 * 1024)
4123            .spawn(move || Css::from_string(s.into()).rules.as_ref().len())
4124            .expect("spawning the parser thread must succeed");
4125
4126        let rule_count = handle
4127            .join()
4128            .expect("10_000 nested blocks must not panic or overflow the stack");
4129        assert!(
4130            rule_count <= depth + 1,
4131            "rule count must stay bounded by the nesting depth"
4132        );
4133    }
4134
4135    #[cfg(feature = "parser")]
4136    #[test]
4137    fn from_string_with_warnings_agrees_with_from_string() {
4138        for input in [
4139            "",
4140            "   ",
4141            "div { width: 1px; }",
4142            "div { not-a-property: 1; }",
4143            "}}} garbage {{{",
4144            "div { width: NaN; }",
4145        ] {
4146            let (css, _warnings) = Css::from_string_with_warnings(input.to_string().into());
4147            assert_eq!(
4148                css,
4149                parse(input),
4150                "from_string_with_warnings must parse {input:?} identically to from_string"
4151            );
4152        }
4153    }
4154
4155    #[cfg(feature = "parser")]
4156    #[test]
4157    fn from_string_with_warnings_reports_an_unknown_property() {
4158        let (css, warnings) = Css::from_string_with_warnings(
4159            "div { definitely-not-a-property: 1px; }".to_string().into(),
4160        );
4161        assert!(
4162            !warnings.is_empty(),
4163            "an unknown property must surface as a warning rather than being dropped silently"
4164        );
4165        // The unknown property is a RECOVERABLE error: the parse keeps going, so the
4166        // stylesheet must not be torn down around it.
4167        assert!(css.rules.as_ref().len() <= 1);
4168    }
4169
4170    #[cfg(feature = "parser")]
4171    #[test]
4172    fn from_string_with_warnings_on_empty_input_has_no_rules() {
4173        let (css, warnings) = Css::from_string_with_warnings(String::new().into());
4174        assert!(css.is_empty());
4175        assert!(warnings.is_empty());
4176    }
4177
4178    #[cfg(feature = "parser")]
4179    #[test]
4180    fn parse_inline_marks_every_rule_as_the_inline_layer() {
4181        let css = Css::parse_inline("width: 200px; color: red;");
4182        assert!(!css.is_empty());
4183        for r in css.rules() {
4184            assert_eq!(
4185                r.priority,
4186                rule_priority::INLINE,
4187                "parse_inline must stamp every rule with the INLINE layer"
4188            );
4189        }
4190        let props: Vec<CssPropertyType> = css
4191            .iter_inline_properties()
4192            .map(|(p, _)| p.get_type())
4193            .collect();
4194        assert!(props.contains(&CssPropertyType::Width));
4195        assert!(props.contains(&CssPropertyType::TextColor));
4196    }
4197
4198    #[cfg(feature = "parser")]
4199    #[test]
4200    fn parse_inline_wraps_bare_declarations_in_a_star_rule() {
4201        let css = Css::parse_inline("width: 200px;");
4202        assert_eq!(css.rules.as_ref().len(), 1);
4203        assert_eq!(
4204            css.rules.as_ref()[0].path.selectors.as_ref(),
4205            &[CssPathSelector::Global][..],
4206            "the wrapper path must be exactly `*` — push_front_scope keys node-only \
4207             inline semantics off that shape"
4208        );
4209    }
4210
4211    #[cfg(feature = "parser")]
4212    #[test]
4213    fn parse_inline_on_empty_and_whitespace_input_does_not_panic() {
4214        for input in ["", " ", "\t\n", "   \r\n  "] {
4215            let css = Css::parse_inline(input);
4216            for r in css.rules() {
4217                assert_eq!(r.priority, rule_priority::INLINE);
4218                assert!(
4219                    r.declarations.as_ref().is_empty(),
4220                    "an empty inline style must not produce declarations"
4221                );
4222            }
4223        }
4224    }
4225
4226    #[cfg(feature = "parser")]
4227    #[test]
4228    fn parse_inline_on_garbage_never_panics_and_is_deterministic() {
4229        for input in [
4230            "}}}}",
4231            "{{{{",
4232            ";;;;",
4233            ":::",
4234            "color",
4235            "color:",
4236            ": red",
4237            "\u{0}\u{1}",
4238            "/* unterminated",
4239            "@@@",
4240            "width: 1px", // no trailing semicolon
4241        ] {
4242            assert_eq!(
4243                Css::parse_inline(input),
4244                Css::parse_inline(input),
4245                "parse_inline({input:?}) must be deterministic"
4246            );
4247        }
4248    }
4249
4250    #[cfg(feature = "parser")]
4251    #[test]
4252    fn parse_inline_on_unicode_and_boundary_numbers_does_not_panic() {
4253        for input in [
4254            "content: \"😀\"",
4255            "font-family: \"日本語\"",
4256            "width: 0px",
4257            "width: -0px",
4258            "width: 9223372036854775807px",
4259            "width: 1e400px",
4260            "width: NaN",
4261            "width: inf",
4262            "opacity: 1e309",
4263        ] {
4264            let css = Css::parse_inline(input);
4265            for r in css.rules() {
4266                assert_eq!(r.priority, rule_priority::INLINE);
4267            }
4268        }
4269    }
4270
4271    #[cfg(feature = "parser")]
4272    #[test]
4273    fn parse_inline_on_a_one_megabyte_style_terminates() {
4274        let style = "width:1px;".repeat(100_000);
4275        assert!(style.len() >= 1_000_000);
4276        let css = Css::parse_inline(&style);
4277        assert!(!css.is_empty());
4278        for r in css.rules() {
4279            assert_eq!(r.priority, rule_priority::INLINE);
4280        }
4281    }
4282
4283    #[cfg(feature = "parser")]
4284    #[test]
4285    fn parse_inline_supports_nested_pseudo_blocks() {
4286        // Documented feature: `:hover { ... }` works inside an inline style via CSS nesting.
4287        let css = Css::parse_inline(":hover { color: red; }");
4288        assert!(!css.is_empty(), "a nested pseudo block must produce a rule");
4289        for r in css.rules() {
4290            assert_eq!(r.priority, rule_priority::INLINE);
4291        }
4292    }
4293
4294    #[cfg(feature = "parser")]
4295    #[test]
4296    fn parse_inline_must_not_let_a_brace_escape_the_star_wrapper() {
4297        // parse_inline builds `* {\n<input>\n}`. A `}` inside the input closes that
4298        // wrapper early, so whatever follows is parsed as a TOP-LEVEL rule with an
4299        // attacker-chosen selector. An inline style string must never be able to define
4300        // rules that target other elements — every rule it produces has to stay rooted
4301        // at the `*` wrapper (push_front_scope also keys node-only scoping off that).
4302        let css = Css::parse_inline("color: red; } div { background: green;");
4303        for r in css.rules() {
4304            let first = r.path.selectors.as_ref().first();
4305            assert!(
4306                matches!(first, None | Some(CssPathSelector::Global)),
4307                "a `}}` in the inline style escaped the `*` wrapper and produced the \
4308                 free-standing rule `{}` (selector injection)",
4309                r.path
4310            );
4311        }
4312    }
4313}