Skip to main content

euv_core/vdom/attribute/
impl.rs

1use super::*;
2
3/// SAFETY: `InjectedClassesCell` is only used in single-threaded WASM contexts.
4unsafe impl Sync for InjectedClassesCell {}
5
6/// Implementation of injected class tracking for CSS deduplication.
7impl InjectedClassesCell {
8    /// Returns a shared reference to the injected classes set.
9    ///
10    /// # Returns
11    ///
12    /// - `&'static HashSet<String>` - A shared reference to the global set of injected class names.
13    pub(crate) fn get_injected_classes() -> &'static HashSet<String> {
14        unsafe {
15            &*(*std::ptr::addr_of!(INJECTED_CLASSES))
16                .deref()
17                .get_0()
18                .get()
19        }
20    }
21
22    /// Returns a mutable reference to the injected classes set.
23    ///
24    /// # Returns
25    ///
26    /// - `&'static mut HashSet<String>` - A mutable reference to the global set of injected class names.
27    pub(crate) fn get_mut_injected_classes() -> &'static mut HashSet<String> {
28        unsafe {
29            &mut *(*std::ptr::addr_of_mut!(INJECTED_CLASSES))
30                .deref()
31                .get_0()
32                .get()
33        }
34    }
35
36    /// Returns `true` if the given class name has already been injected into the DOM.
37    ///
38    /// Encapsulates `static mut` access so callers do not need `unsafe` blocks.
39    ///
40    /// # Arguments
41    ///
42    /// - `&str` - The CSS class name to check.
43    ///
44    /// # Returns
45    ///
46    /// - `bool` - Whether the class name has been injected.
47    pub(crate) fn is_injected(class_name: &str) -> bool {
48        Self::get_injected_classes().contains(class_name)
49    }
50
51    /// Marks a class name as injected so future calls to `is_injected` return `true`.
52    ///
53    /// Encapsulates `static mut` access so callers do not need `unsafe` blocks.
54    ///
55    /// # Arguments
56    ///
57    /// - `&str` - The CSS class name to mark as injected.
58    pub(crate) fn mark_injected(class_name: &str) {
59        Self::get_mut_injected_classes().insert(class_name.to_string());
60    }
61}
62
63/// Implementation of attribute value factory methods for reactive and merged values.
64impl AttributeValue {
65    /// Creates a reactive attribute `Self` for conditional attribute values.
66    ///
67    /// This function replaces the inline `Signal::create(...)` boilerplate
68    /// that was previously generated by the `html!` macro for every
69    /// attribute value containing an `if` condition.
70    ///
71    /// The returned `Self::Signal` is a value carrier created fresh on each
72    /// render of the enclosing dynamic node: the signals read inside
73    /// `compute` during that render register the dynamic node as their
74    /// dependent, so any source change re-renders the node and rebuilds
75    /// this attribute with the new value. No registry listener is kept —
76    /// the former `subscribe_attr` registration was unreachable dead weight
77    /// (its key could never be marked dirty) and aliased the dynamic-node
78    /// key space of the update registry.
79    ///
80    /// # Arguments
81    ///
82    /// - `F: Fn() -> String + 'static` - A closure that computes the current attribute value.
83    ///   Called once per render of the enclosing dynamic node.
84    ///
85    /// # Returns
86    ///
87    /// - `Self` - A `Self::Signal` backed by a `Signal<String>`
88    ///   carrying the freshly computed attribute value.
89    pub fn reactive<F>(compute: F) -> Self
90    where
91        F: Fn() -> String + 'static,
92    {
93        let attr_signal: Signal<String> = Signal::create(compute());
94        Self::Signal(attr_signal)
95    }
96
97    /// Merges multiple class attribute values into a single `Self`.
98    ///
99    /// Each input value is adapted into a `Self` via `IntoReactiveValue`.
100    /// `Css` values are injected into the DOM and their names are collected.
101    /// All non-empty class names are joined with spaces into a final `Text` attribute.
102    /// If any value is signal-backed, the result becomes a `Signal` attribute
103    /// carrying the freshly merged value for this render; the enclosing
104    /// dynamic node re-renders on any constituent signal change (see
105    /// [`AttributeValue::reactive`]).
106    ///
107    /// # Arguments
108    ///
109    /// - `&[Self]` - The class attribute values to merge.
110    ///
111    /// # Returns
112    ///
113    /// - `Self` - A merged attribute value containing space-separated class names.
114    pub fn merge_class(values: &[Self]) -> Self {
115        let has_signal: bool = values
116            .iter()
117            .any(|value: &Self| matches!(value, Self::Signal(_)));
118        if has_signal {
119            let owned_values: Vec<Self> = values.to_vec();
120            let compute: Box<dyn Fn() -> String> =
121                Box::new(move || Self::join_class_segments(&owned_values));
122            let attr_signal: Signal<String> = Signal::create(compute());
123            return Self::Signal(attr_signal);
124        }
125        Self::Text(Self::join_class_segments(values))
126    }
127
128    /// Joins class attribute values into a single space-separated string.
129    ///
130    /// OPT-20: builds the result in a single `String` (amortized growth,
131    /// no intermediate `Vec<String>` + `join(" ")` round-trip), iterating
132    /// the input values by reference and cloning only the segments that
133    /// survive the filter (skipping `_ => None` arms and empty `Text`
134    /// strings), so the per-class render allocation drops from `N + 2`
135    /// to `1`.
136    ///
137    /// OPT-11: both `Css` and `CssRef` arms inject the style on first
138    /// reference. Without the `CssRef` arm the ref would fall through
139    /// to `_ => None` and silently drop out of the merged class list —
140    /// the regression that wiped every multi-class CssRef entry (e.g.
141    /// the `c_binding_slider` class next to a parameterized
142    /// `c_slider_value("30%")` on the same `<input>`).
143    fn join_class_segments(values: &[Self]) -> String {
144        let mut joined: String = String::new();
145        for value in values.iter() {
146            let segment: std::borrow::Cow<'_, str> = match value {
147                Self::Css(css) => {
148                    css.inject_style();
149                    let name: &str = css.get_name();
150                    if name.is_empty() {
151                        continue;
152                    }
153                    std::borrow::Cow::Borrowed(name)
154                }
155                Self::CssRef(css) => {
156                    css.inject_style();
157                    let name: &str = css.get_name();
158                    if name.is_empty() {
159                        continue;
160                    }
161                    std::borrow::Cow::Borrowed(name)
162                }
163                Self::Text(text_value) => {
164                    if text_value.is_empty() {
165                        continue;
166                    }
167                    std::borrow::Cow::Borrowed(text_value.as_str())
168                }
169                Self::Signal(signal) => {
170                    let current: String = signal.get();
171                    if current.is_empty() {
172                        continue;
173                    }
174                    std::borrow::Cow::Owned(current)
175                }
176                _ => continue,
177            };
178            if !joined.is_empty() {
179                joined.push(' ');
180            }
181            joined.push_str(segment.as_ref());
182        }
183        joined
184    }
185
186    /// Merges multiple style attribute values into a single `Self`.
187    ///
188    /// Each input value is expected to be a style string (`Text`) or a reactive
189    /// `Signal<String>` producing a style string. All non-empty style strings are
190    /// joined with spaces into a final combined style attribute.
191    /// If any value is signal-backed, the result becomes a `Signal` attribute
192    /// carrying the freshly merged value for this render; the enclosing
193    /// dynamic node re-renders on any constituent signal change (see
194    /// [`AttributeValue::reactive`]).
195    ///
196    /// # Arguments
197    ///
198    /// - `&[Self]` - The style attribute values to merge.
199    ///
200    /// # Returns
201    ///
202    /// - `Self` - A merged attribute value containing the combined CSS style string.
203    pub fn merge_style(values: &[Self]) -> Self {
204        let has_signal: bool = values
205            .iter()
206            .any(|value: &Self| matches!(value, Self::Signal(_)));
207        if has_signal {
208            let owned_values: Vec<Self> = values.to_vec();
209            let compute: Box<dyn Fn() -> String> =
210                Box::new(move || Self::join_style_segments(&owned_values));
211            let attr_signal: Signal<String> = Signal::create(compute());
212            return Self::Signal(attr_signal);
213        }
214        Self::Text(Self::join_style_segments(values))
215    }
216
217    /// Joins style attribute values into a single space-separated string.
218    ///
219    /// OPT-20: same single-`String` approach as `join_class_segments`
220    /// (amortized growth), specialised to the `Text` + `Signal` cases that
221    /// style merging uses. Avoids the intermediate `Vec<String>` plus
222    /// `join(" ")` round-trip of the previous implementation, dropping
223    /// per-render allocation count from `N + 2` to `1`.
224    fn join_style_segments(values: &[Self]) -> String {
225        let mut joined: String = String::new();
226        for value in values.iter() {
227            let segment: std::borrow::Cow<'_, str> = match value {
228                Self::Text(text_value) => {
229                    if text_value.is_empty() {
230                        continue;
231                    }
232                    std::borrow::Cow::Borrowed(text_value.as_str())
233                }
234                Self::Signal(signal) => {
235                    let current: String = signal.get();
236                    if current.is_empty() {
237                        continue;
238                    }
239                    std::borrow::Cow::Owned(current)
240                }
241                _ => continue,
242            };
243            if !joined.is_empty() {
244                joined.push(' ');
245            }
246            joined.push_str(segment.as_ref());
247        }
248        joined
249    }
250
251    /// Converts a bool signal into a reactive boolean attribute value.
252    ///
253    /// Produces `AttributeValue::BoolSignal` directly: the renderer writes
254    /// `"true"` / `"false"` and subscribes the source signal to the element
255    /// with no intermediate mapping signal (the previous `Signal<String>`
256    /// bridge) and no per-render subscription.
257    ///
258    /// # Arguments
259    ///
260    /// - `Signal<bool>` - The source boolean signal.
261    ///
262    /// # Returns
263    ///
264    /// - `AttributeValue` - A `BoolSignal` wrapping the source signal.
265    pub(crate) fn bool_to_attr(source: Signal<bool>) -> AttributeValue {
266        AttributeValue::BoolSignal(source)
267    }
268}
269
270/// Visual equality comparison for attribute values.
271///
272/// Compares values by their visual output rather than identity. `Signal`
273/// values are compared by their current resolved string (borrow-compared
274/// via `Signal::with`, no `String` clones); when both signals share the
275/// same slab slot, they are always considered **unequal** — the patch walk
276/// recognises that case separately and skips the DOM write because the
277/// mount-time signal binding already owns the attribute's DOM value (see
278/// `patch_attributes`), while the unequal verdict keeps `visual_eq` from
279/// masking a value change between VDOM snapshots.
280/// `Event` values are always considered equal (re-binding is handled by the
281/// handler registry), and `Css` values are compared by class name.
282impl PartialEq for AttributeValue {
283    /// Compares two attribute values for visual equality.
284    ///
285    /// # Arguments
286    ///
287    /// - `&Self` - The first attribute value.
288    /// - `&Self` - The second attribute value.
289    ///
290    /// # Returns
291    ///
292    /// - `bool` - `true` if the values are visually equal.
293    fn eq(&self, other: &Self) -> bool {
294        match (self, other) {
295            (Self::Text(old_value), Self::Text(new_value)) => old_value == new_value,
296            (Self::StaticText(old_value), Self::StaticText(new_value)) => old_value == new_value,
297            (Self::Text(old_value), Self::StaticText(new_value)) => old_value == new_value,
298            (Self::StaticText(old_value), Self::Text(new_value)) => old_value == new_value,
299            (Self::Signal(old_signal), Self::Signal(new_signal)) => {
300                if old_signal.get_inner() == new_signal.get_inner() {
301                    return false;
302                }
303                old_signal.with(|old_value: &String| {
304                    new_signal.with(|new_value: &String| old_value == new_value)
305                })
306            }
307            (Self::Signal(old_signal), Self::Text(new_value)) => {
308                old_signal.with(|old_value: &String| old_value == new_value)
309            }
310            (Self::Text(old_value), Self::Signal(new_signal)) => {
311                new_signal.with(|new_value: &String| old_value == new_value)
312            }
313            (Self::BoolSignal(old_signal), Self::BoolSignal(new_signal)) => {
314                if old_signal.get_inner() == new_signal.get_inner() {
315                    return false;
316                }
317                old_signal.get() == new_signal.get()
318            }
319            (Self::Event(_), Self::Event(_)) => true,
320            (Self::Css(old_class), Self::Css(new_class)) => {
321                old_class.get_name() == new_class.get_name()
322            }
323            (Self::CssRef(old_class), Self::CssRef(new_class)) => {
324                old_class.get_name() == new_class.get_name()
325            }
326            (Self::CssRef(old_class), Self::Css(new_class)) => {
327                old_class.get_name() == new_class.get_name()
328            }
329            (Self::Css(old_class), Self::CssRef(new_class)) => {
330                old_class.get_name() == new_class.get_name()
331            }
332            (Self::Dynamic(old_dynamic), Self::Dynamic(new_dynamic)) => old_dynamic == new_dynamic,
333            _ => false,
334        }
335    }
336}
337
338/// Visual equality comparison for attribute entries.
339///
340/// Two attribute entries are equal when their names match and their values
341/// are visually equal as defined by `AttributeValue::eq`.
342impl PartialEq for AttributeEntry {
343    /// Compares two attribute entries for visual equality.
344    ///
345    /// # Arguments
346    ///
347    /// - `&Self` - The first attribute entry.
348    /// - `&Self` - The second attribute entry.
349    ///
350    /// # Returns
351    ///
352    /// - `bool` - `true` if both names and values match.
353    fn eq(&self, other: &Self) -> bool {
354        self.get_name() == other.get_name() && self.get_value() == other.get_value()
355    }
356}
357
358/// Visual equality comparison for CSS classes.
359///
360/// Two CSS classes are considered equal when their class names match,
361/// since the name uniquely identifies the visual style rule.
362impl PartialEq for Css {
363    /// Compares two CSS classes by name.
364    ///
365    /// # Arguments
366    ///
367    /// - `&Self` - The first CSS class.
368    /// - `&Self` - The second CSS class.
369    ///
370    /// # Returns
371    ///
372    /// - `bool` - `true` if the class names match.
373    fn eq(&self, other: &Self) -> bool {
374        self.get_name() == other.get_name()
375    }
376}
377
378/// Implementation of Css construction and style injection.
379impl Css {
380    /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
381    ///
382    /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
383    /// This is used by the `class!` macro for fully static class definitions
384    /// where pseudo rules can be computed at compile time.
385    ///
386    /// # Arguments
387    ///
388    /// - `I: AsRef<str>` - The serialized pseudo rules string.
389    ///
390    /// # Returns
391    ///
392    /// - `Vec<PseudoRule>` - The parsed pseudo rules.
393    pub fn parse_pseudo_rules<I>(input: I) -> Vec<PseudoRule>
394    where
395        I: AsRef<str>,
396    {
397        let mut remaining: &str = input.as_ref();
398        let mut rules: Vec<PseudoRule> = Vec::new();
399        while !remaining.is_empty() {
400            let selector_end: Option<usize> = remaining.find(CSS_RULE_OPEN);
401            let Some(selector_end_index) = selector_end else {
402                break;
403            };
404            let selector: &str = &remaining[..selector_end_index];
405            let after_selector: &str = remaining[selector_end_index..]
406                .strip_prefix(CSS_RULE_OPEN)
407                .unwrap_or_default();
408            let style_end: Option<usize> = after_selector.find(CHAR_CSS_RULE_CLOSE);
409            let Some(style_end_index) = style_end else {
410                break;
411            };
412            let style: &str = &after_selector[..style_end_index];
413            if !selector.is_empty() && !style.is_empty() {
414                rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
415            }
416            remaining = after_selector[style_end_index..]
417                .strip_prefix(CHAR_CSS_RULE_CLOSE)
418                .unwrap_or_default();
419        }
420        rules
421    }
422
423    /// Parses media query rules from a compact serialization string.
424    ///
425    /// The serialization format is:
426    /// `@media query { key: value; ::selector { key: value; } }@media query2 { ... }`
427    /// This is used by the `class!` macro for fully static class definitions
428    /// where media rules can be computed at compile time.
429    /// Supports nested pseudo-element blocks inside media query blocks.
430    ///
431    /// # Arguments
432    ///
433    /// - `S: AsRef<str>` - The serialized media rules string.
434    ///
435    /// # Returns
436    ///
437    /// - `Vec<MediaRule>` - The parsed media rules.
438    pub fn parse_media_rules<S>(input: S) -> Vec<MediaRule>
439    where
440        S: AsRef<str>,
441    {
442        let input: &str = input.as_ref();
443        let mut rules: Vec<MediaRule> = Vec::new();
444        let mut remaining: &str = input;
445        while !remaining.is_empty() {
446            if !remaining.starts_with(CSS_MEDIA_PREFIX) {
447                break;
448            }
449            let after_prefix: &str = remaining.strip_prefix(CSS_MEDIA_PREFIX).unwrap_or_default();
450            let query_end: Option<usize> = after_prefix.find(CSS_RULE_OPEN);
451            let Some(query_end_index) = query_end else {
452                break;
453            };
454            let query: &str = &after_prefix[..query_end_index];
455            let after_query: &str = after_prefix[query_end_index..]
456                .strip_prefix(CSS_RULE_OPEN)
457                .unwrap_or_default();
458            let mut depth: usize = 1;
459            let mut close_pos: usize = 0;
460            for (index, char_value) in after_query.char_indices() {
461                if char_value == '{' {
462                    depth += 1;
463                } else if char_value == '}' {
464                    depth -= 1;
465                    if depth == 0 {
466                        close_pos = index;
467                        break;
468                    }
469                }
470            }
471            if close_pos == 0 {
472                break;
473            }
474            let body: &str = &after_query[..close_pos];
475            let (style, pseudo_rules): (String, Vec<PseudoRule>) = Self::parse_media_body(body);
476            if !query.is_empty() && (!style.is_empty() || !pseudo_rules.is_empty()) {
477                rules.push(MediaRule::new(query.to_string(), style, pseudo_rules));
478            }
479            remaining = after_query[close_pos..]
480                .strip_prefix(CHAR_CSS_RULE_CLOSE)
481                .unwrap_or_default();
482        }
483        rules
484    }
485
486    /// Parses the body of a media rule, separating top-level style declarations
487    /// from nested pseudo-element blocks.
488    ///
489    /// # Arguments
490    ///
491    /// - `&str` - The media rule body content (between the outer braces).
492    ///
493    /// # Returns
494    ///
495    /// - `(String, Vec<PseudoRule>)` - A tuple of the style string and pseudo rules.
496    fn parse_media_body(body: &str) -> (String, Vec<PseudoRule>) {
497        let mut style_parts: String = String::new();
498        let mut pseudo_rules: Vec<PseudoRule> = Vec::new();
499        let mut remaining: &str = body;
500        while !remaining.is_empty() {
501            let brace_pos: Option<usize> = remaining.find('{');
502            match brace_pos {
503                Some(pos) => {
504                    let before_brace: &str = remaining[..pos].trim();
505                    if before_brace.starts_with("::") || before_brace.starts_with(':') {
506                        let selector: &str = before_brace;
507                        let after_brace: &str = &remaining[pos + 1..];
508                        let mut depth: usize = 1;
509                        let mut close_pos: usize = 0;
510                        for (index, char_value) in after_brace.char_indices() {
511                            if char_value == '{' {
512                                depth += 1;
513                            } else if char_value == '}' {
514                                depth -= 1;
515                                if depth == 0 {
516                                    close_pos = index;
517                                    break;
518                                }
519                            }
520                        }
521                        if close_pos > 0 {
522                            let inner_style: &str = after_brace[..close_pos].trim();
523                            if !selector.is_empty() && !inner_style.is_empty() {
524                                pseudo_rules.push(PseudoRule::new(
525                                    selector.to_string(),
526                                    inner_style.to_string(),
527                                ));
528                            }
529                            remaining = after_brace[close_pos + 1..].trim_start();
530                            continue;
531                        }
532                        break;
533                    } else {
534                        style_parts.push_str(before_brace);
535                        style_parts.push(' ');
536                        let after_brace: &str = &remaining[pos + 1..];
537                        let mut depth: usize = 1;
538                        let mut close_pos: usize = 0;
539                        for (index, char_value) in after_brace.char_indices() {
540                            if char_value == '{' {
541                                depth += 1;
542                            } else if char_value == '}' {
543                                depth -= 1;
544                                if depth == 0 {
545                                    close_pos = index;
546                                    break;
547                                }
548                            }
549                        }
550                        if close_pos > 0 {
551                            style_parts.push_str(after_brace[..close_pos].trim());
552                            style_parts.push(' ');
553                            remaining = after_brace[close_pos + 1..].trim_start();
554                            continue;
555                        }
556                        break;
557                    }
558                }
559                None => {
560                    style_parts.push_str(remaining.trim());
561                    break;
562                }
563            }
564        }
565        (style_parts.trim().to_string(), pseudo_rules)
566    }
567
568    /// Injects this class's styles into the DOM if not already present.
569    ///
570    /// Uses a global `HashSet` to track injected class names, avoiding the
571    /// expensive `existing_css.contains(css)` full-text search on every call.
572    /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
573    /// then appends them directly to the `<style>` element via
574    /// `append_child` with a new text node — no read-modify-write of the
575    /// entire stylesheet content.
576    ///
577    /// # Panics
578    ///
579    /// Panics if `window()` or `document()` is unavailable on the current platform.
580    pub fn inject_style(&self) {
581        let class_name: &String = self.get_name();
582        if InjectedClassesCell::is_injected(class_name) {
583            return;
584        }
585        InjectedClassesCell::mark_injected(class_name);
586        let raw_name: String = self.get_name().clone();
587        let mut escaped_name: String = String::with_capacity(raw_name.len() * 2);
588        for ch in raw_name.chars() {
589            if ch.is_ascii_alphanumeric() || ch == CHAR_HYPHEN || ch == CHAR_UNDERSCORE {
590                escaped_name.push(ch);
591            } else {
592                escaped_name.push(CHAR_CSS_ESCAPE);
593                escaped_name.push(ch);
594            }
595        }
596        let mut css_text: String = format!(
597            "{CHAR_CSS_CLASS_PREFIX}{escaped_name}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
598            self.get_style()
599        );
600        for pseudo_rule in self.get_pseudo_rules() {
601            if !pseudo_rule.get_style().is_empty() {
602                css_text = format!(
603                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
604                    escaped_name,
605                    pseudo_rule.get_selector(),
606                    pseudo_rule.get_style()
607                );
608            }
609        }
610        for media_rule in self.get_media_rules() {
611            if !media_rule.get_query().is_empty() {
612                let mut media_body: String = format!(
613                    "{CHAR_CSS_CLASS_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
614                    escaped_name,
615                    media_rule.get_style()
616                );
617                for pseudo_rule in media_rule.get_pseudo_rules() {
618                    if !pseudo_rule.get_style().is_empty() {
619                        media_body = format!(
620                            "{media_body} {CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
621                            escaped_name,
622                            pseudo_rule.get_selector(),
623                            pseudo_rule.get_style()
624                        );
625                    }
626                }
627                css_text = format!(
628                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CSS_MEDIA_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
629                    media_rule.get_query(),
630                    media_body
631                );
632            }
633        }
634        Self::append_css(&css_text);
635    }
636
637    /// Appends CSS text directly to the shared `<style>` element.
638    ///
639    /// Creates a new text node and appends it as a child of the `<style>`
640    /// element, avoiding the read-modify-write pattern of reading the entire
641    /// `innerText`, concatenating, and setting it back.
642    ///
643    /// # Arguments
644    ///
645    /// - `&str` - The CSS text to append.
646    ///
647    fn append_css(css_text: &str) {
648        let style_id: &str = EUV_CSS_INJECTED_ID;
649        let window_value: Window = match window() {
650            Some(window_instance) => window_instance,
651            None => return,
652        };
653        let document: Document = match window_value.document() {
654            Some(document_instance) => document_instance,
655            None => return,
656        };
657        let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
658            Some(existing_element) => match existing_element.dyn_into::<HtmlStyleElement>() {
659                Ok(element) => element,
660                Err(_err) => return,
661            },
662            None => {
663                let created: Element = match document.create_element(STYLE_TAG) {
664                    Ok(element) => element,
665                    Err(_err) => return,
666                };
667                let style_element_from_id: HtmlStyleElement =
668                    match created.dyn_into::<HtmlStyleElement>() {
669                        Ok(element) => element,
670                        Err(_err) => return,
671                    };
672                style_element_from_id.set_id(style_id);
673                if let Some(head) = document.head() {
674                    let _: Result<Node, JsValue> = head.append_child(&style_element_from_id);
675                }
676                style_element_from_id
677            }
678        };
679        if !css_text.is_empty() {
680            let text_node: Text = document.create_text_node(css_text);
681            let _: Result<Node, JsValue> = style_element.append_child(&text_node);
682        }
683    }
684
685    /// Builds a CSS style string from an array of key-value pairs.
686    ///
687    /// This function is used by the `html!` macro to convert static `style:`
688    /// attributes into a CSS string without allocating intermediate objects.
689    ///
690    /// # Arguments
691    ///
692    /// - `S: AsRef<str>` - An array of CSS property name-value pairs.
693    ///
694    /// # Returns
695    ///
696    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
697    pub fn style_string<K, V>(props: &[(K, V)]) -> String
698    where
699        K: AsRef<str>,
700        V: AsRef<str>,
701    {
702        // Single pre-sized `String` instead of per-prop `format!` +
703        // intermediate `Vec<String>` + `join` (was N + 2 allocations).
704        let total_len: usize = props
705            .iter()
706            .map(|(key, value): &(K, V)| {
707                key.as_ref().len() + CSS_PROP_SEPARATOR.len() + value.as_ref().len() + 2
708            })
709            .sum();
710        let mut out: String = String::with_capacity(total_len);
711        for (index, (key, value)) in props.iter().enumerate() {
712            if index > 0 {
713                out.push_str(CHAR_SPACE);
714            }
715            out.push_str(key.as_ref());
716            out.push_str(CSS_PROP_SEPARATOR);
717            out.push_str(value.as_ref());
718            out.push(CHAR_CSS_DECL_TERMINATOR);
719        }
720        out
721    }
722
723    /// Builds a stable suffix for a class name from a dynamic parameter value.
724    ///
725    /// Used by the `class!` macro when a parameter is wrapped in `{}` in the
726    /// class body. Wrapping a parameter opts it into value-dependent class
727    /// names, so each distinct value can inject its own CSS rule.
728    ///
729    /// # Arguments
730    ///
731    /// - `&str` - The dynamic parameter value.
732    ///
733    /// # Returns
734    ///
735    /// - `String` - A stable hexadecimal suffix for the class name.
736    pub fn param_class_name(value: &str) -> String {
737        let mut hash: u64 = CLASS_PARAM_HASH_FNV_OFFSET;
738        for byte in value.as_bytes() {
739            hash ^= u64::from(*byte);
740            hash = hash.wrapping_mul(CLASS_PARAM_HASH_FNV_PRIME);
741        }
742        format!("{hash:x}")
743    }
744
745    /// Builds a CSS style string from owned key-value pairs.
746    ///
747    /// Used by the `html!` macro for reactive style attributes (with `if`
748    /// conditions) where values are computed at runtime.
749    ///
750    /// # Arguments
751    ///
752    /// - `&[(String, String)]` - An array of owned CSS property name-value pairs.
753    ///
754    /// # Returns
755    ///
756    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
757    pub fn style_string_owned(props: &[(String, String)]) -> String {
758        // Single pre-sized `String` (same shape as `style_string`).
759        let total_len: usize = props
760            .iter()
761            .map(|(key, value): &(String, String)| {
762                key.len() + CSS_PROP_SEPARATOR.len() + value.len() + 2
763            })
764            .sum();
765        let mut out: String = String::with_capacity(total_len);
766        for (index, (key, value)) in props.iter().enumerate() {
767            if index > 0 {
768                out.push_str(CHAR_SPACE);
769            }
770            out.push_str(key);
771            out.push_str(CSS_PROP_SEPARATOR);
772            out.push_str(value);
773            out.push(CHAR_CSS_DECL_TERMINATOR);
774        }
775        out
776    }
777
778    /// Injects CSS text into the shared `<style>` element in the DOM.
779    ///
780    /// Delegates to [`Css::append_css`] for the actual DOM append.
781    /// Unlike the previous implementation, this does not read the existing
782    /// stylesheet content or perform a full-text `contains` search.
783    ///
784    /// # Arguments
785    ///
786    /// - `S: AsRef<str>` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
787    ///
788    /// # Panics
789    ///
790    /// Panics if `window()` or `document()` is unavailable on the current platform.
791    pub fn inject_css<S>(css_text: S)
792    where
793        S: AsRef<str>,
794    {
795        let css_text: &str = css_text.as_ref();
796        Self::append_css(css_text);
797    }
798}
799
800/// Displays the CSS class name.
801///
802/// This enables `format!("{css}")` to produce the class name string,
803/// which is required for reactive `if` conditions in `class:` attributes.
804impl Display for Css {
805    /// Formats the CSS class as its name string.
806    ///
807    /// # Arguments
808    ///
809    /// - `&mut Formatter` - The formatter.
810    ///
811    /// # Returns
812    ///
813    /// - `fmt::Result` - The formatting result.
814    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
815        write!(formatter, "{}", self.get_name())
816    }
817}