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