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