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