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