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