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