Skip to main content

azul_core/
xml.rs

1//! XML and XHTML parsing for declarative UI definitions.
2//!
3//! This module provides comprehensive XML parsing and manipulation for Azul's XML-based
4//! UI format (`.azul` files). It supports:
5//!
6//! - **XHTML parsing**: Parse HTML-like syntax into DOM structures
7//! - **CSS extraction**: Extract `<style>` blocks and inline styles
8//! - **Component system**: Define reusable UI components with arguments
9//! - **Hot reload**: Track file changes and rebuild UI incrementally
10//! - **Error reporting**: Detailed syntax error messages with line/column info
11//!
12//! # Examples
13//!
14//! ```rust,no_run,ignore
15//! use azul_core::xml::{XmlNode, XmlParseOptions};
16//!
17//! let xml = "<div>Hello</div>";
18//! // let node = XmlNode::parse(xml)?;
19//! ```
20
21use alloc::{
22    boxed::Box,
23    collections::BTreeMap,
24    string::{String, ToString},
25    vec::Vec,
26};
27use core::{fmt, fmt::Write, hash::Hash};
28
29use azul_css::{
30    css::{
31        Css, CssDeclaration, CssPath, CssPathPseudoSelector, CssPathSelector, CssRuleBlock,
32        NodeTypeTag,
33    },
34    codegen::format::VecContents,
35    parser2::{CssParseErrorOwned, ErrorLocation},
36    props::{
37        basic::{ColorU, StyleFontFamilyVec},
38        property::CssProperty,
39        style::{
40            NormalizedLinearColorStopVec, NormalizedRadialColorStopVec, StyleBackgroundContentVec,
41            StyleBackgroundPositionVec, StyleBackgroundRepeatVec, StyleBackgroundSizeVec,
42            StyleTransformVec,
43        },
44    },
45    AzString, OptionString, StringVec, U8Vec,
46};
47
48use crate::{
49    dom::{Dom, NodeType, OptionNodeType},
50    styled_dom::StyledDom,
51    window::{AzStringPair, StringPairVec},
52};
53
54/// Error that can occur during XML parsing or hot-reload.
55///
56/// Stringified for error reporting; not part of the public API.
57pub type SyntaxError = String;
58
59/// Tag of an XML node, such as the "button" in `<button>Hello</button>`.
60#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C)]
62pub struct XmlTagName {
63    pub inner: AzString,
64}
65
66impl From<AzString> for XmlTagName {
67    fn from(s: AzString) -> Self {
68        Self { inner: s }
69    }
70}
71
72impl From<String> for XmlTagName {
73    fn from(s: String) -> Self {
74        Self { inner: s.into() }
75    }
76}
77
78impl From<&str> for XmlTagName {
79    fn from(s: &str) -> Self {
80        Self { inner: s.into() }
81    }
82}
83
84impl core::ops::Deref for XmlTagName {
85    type Target = AzString;
86    fn deref(&self) -> &Self::Target {
87        &self.inner
88    }
89}
90
91/// (Unparsed) text content of an XML node, such as the "Hello" in `<button>Hello</button>`.
92pub type XmlTextContent = OptionString;
93
94/// Attributes of an XML node, such as `["color" => "blue"]` in `<button color="blue" />`.
95#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
96#[repr(C)]
97pub struct XmlAttributeMap {
98    pub inner: StringPairVec,
99}
100
101impl From<StringPairVec> for XmlAttributeMap {
102    fn from(v: StringPairVec) -> Self {
103        Self { inner: v }
104    }
105}
106
107impl core::ops::Deref for XmlAttributeMap {
108    type Target = StringPairVec;
109    fn deref(&self) -> &Self::Target {
110        &self.inner
111    }
112}
113
114impl core::ops::DerefMut for XmlAttributeMap {
115    fn deref_mut(&mut self) -> &mut Self::Target {
116        &mut self.inner
117    }
118}
119
120/// Name of a component argument (e.g. `"text"`, `"href"`).
121type ComponentArgumentName = String;
122/// Type of a component argument as a string (e.g. `"String"`, `"bool"`).
123type ComponentArgumentType = String;
124/// Zero-based position of an argument in the component's argument list.
125type ComponentArgumentOrder = usize;
126
127/// FFI-safe replacement for `(ComponentArgumentName, ComponentArgumentType)` tuple.
128#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
129#[repr(C)]
130pub struct ComponentArgument {
131    pub name: AzString,
132    pub arg_type: AzString,
133}
134
135impl_vec!(
136    ComponentArgument,
137    ComponentArgumentVec,
138    ComponentArgumentVecDestructor,
139    ComponentArgumentVecDestructorType,
140    ComponentArgumentVecSlice,
141    OptionComponentArgument
142);
143impl_option!(
144    ComponentArgument,
145    OptionComponentArgument,
146    copy = false,
147    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
148);
149impl_vec_debug!(ComponentArgument, ComponentArgumentVec);
150impl_vec_partialeq!(ComponentArgument, ComponentArgumentVec);
151impl_vec_eq!(ComponentArgument, ComponentArgumentVec);
152impl_vec_partialord!(ComponentArgument, ComponentArgumentVec);
153impl_vec_ord!(ComponentArgument, ComponentArgumentVec);
154impl_vec_hash!(ComponentArgument, ComponentArgumentVec);
155impl_vec_clone!(
156    ComponentArgument,
157    ComponentArgumentVec,
158    ComponentArgumentVecDestructor
159);
160impl_vec_mut!(ComponentArgument, ComponentArgumentVec);
161
162/// Holds the list of arguments and whether the component accepts text content.
163/// Used by the compile pipeline to generate Rust function signatures.
164#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
165pub struct ComponentArguments {
166    pub args: ComponentArgumentVec,
167    pub accepts_text: bool,
168}
169
170/// Name of an XML/HTML component (e.g. `"button"`, `"my-widget"`).
171type ComponentName = String;
172/// Compiled source code string for a component.
173type CompiledComponent = String;
174
175/// Universal HTML attribute names that are handled by the framework
176/// and should not be passed through to component-specific argument lists.
177const DEFAULT_ARGS: [&str; 8] = [
178    "id",
179    "class",
180    "tabindex",
181    "focusable",
182    "accepts_text",
183    "name",
184    "style",
185    "args",
186];
187
188/// Opaque void type for FFI pointers. Uses a custom definition instead of
189/// `core::ffi::c_void` for `#[repr(C)]` compatibility in the generated API.
190#[allow(non_camel_case_types)]
191#[derive(Debug, Copy, Clone)]
192pub enum c_void {}
193
194/// Type of an XML node in the parsed tree.
195#[repr(C)]
196#[derive(Debug, Copy, Clone)]
197pub enum XmlNodeType {
198    Root,
199    Element,
200    PI,
201    Comment,
202    Text,
203}
204
205/// A namespace-qualified XML name (e.g. `svg:rect` has namespace `"svg"` and local name `"rect"`).
206#[repr(C)]
207#[derive(Debug)]
208pub struct XmlQualifiedName {
209    pub local_name: AzString,
210    pub namespace: OptionString,
211}
212
213/// Classification of an external resource referenced in HTML/XML
214#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
215#[repr(C)]
216pub enum ExternalResourceKind {
217    /// Image resource (img src, background-image, etc.)
218    Image,
219    /// Font resource (@font-face src, link rel="preload" as="font")
220    Font,
221    /// Stylesheet (link rel="stylesheet", @import)
222    Stylesheet,
223    /// Script (script src)
224    Script,
225    /// Favicon or icon
226    Icon,
227    /// Video source
228    Video,
229    /// Audio source
230    Audio,
231    /// Generic link or unknown resource type
232    Unknown,
233}
234
235/// MIME type hint for an external resource
236#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
237#[repr(C)]
238pub struct MimeTypeHint {
239    pub inner: AzString,
240}
241
242impl MimeTypeHint {
243    #[must_use] pub fn new(s: &str) -> Self {
244        Self {
245            inner: AzString::from(s),
246        }
247    }
248
249    #[must_use] pub fn from_extension(ext: &str) -> Self {
250        let mime = match ext.to_lowercase().as_str() {
251            // Images
252            "png" => "image/png",
253            "jpg" | "jpeg" => "image/jpeg",
254            "gif" => "image/gif",
255            "webp" => "image/webp",
256            "svg" => "image/svg+xml",
257            "ico" => "image/x-icon",
258            "bmp" => "image/bmp",
259            "avif" => "image/avif",
260            // Fonts
261            "ttf" => "font/ttf",
262            "otf" => "font/otf",
263            "woff" => "font/woff",
264            "woff2" => "font/woff2",
265            "eot" => "application/vnd.ms-fontobject",
266            // Stylesheets
267            "css" => "text/css",
268            // Scripts
269            "js" | "mjs" => "application/javascript",
270            // Video
271            "mp4" => "video/mp4",
272            "webm" => "video/webm",
273            "ogg" => "video/ogg",
274            // Audio
275            "mp3" => "audio/mpeg",
276            "wav" => "audio/wav",
277            "flac" => "audio/flac",
278            // Default
279            _ => "application/octet-stream",
280        };
281        Self {
282            inner: AzString::from(mime),
283        }
284    }
285}
286
287impl_option!(
288    MimeTypeHint,
289    OptionMimeTypeHint,
290    copy = false,
291    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
292);
293
294/// An external resource URL found in an XML/HTML document
295#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
296#[repr(C)]
297pub struct ExternalResource {
298    /// The URL as found in the document (may be relative or absolute)
299    pub url: AzString,
300    /// Classification of the resource type
301    pub kind: ExternalResourceKind,
302    /// MIME type hint (from type attribute, file extension, or heuristics)
303    pub mime_type: OptionMimeTypeHint,
304    /// The HTML element that referenced this resource (e.g., "img", "link", "script")
305    pub source_element: AzString,
306    /// The attribute that contained the URL (e.g., "src", "href")
307    pub source_attribute: AzString,
308}
309
310impl_option!(
311    ExternalResource,
312    OptionExternalResource,
313    copy = false,
314    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
315);
316
317impl_vec!(
318    ExternalResource,
319    ExternalResourceVec,
320    ExternalResourceVecDestructor,
321    ExternalResourceVecDestructorType,
322    ExternalResourceVecSlice,
323    OptionExternalResource
324);
325impl_vec_mut!(ExternalResource, ExternalResourceVec);
326impl_vec_debug!(ExternalResource, ExternalResourceVec);
327impl_vec_partialeq!(ExternalResource, ExternalResourceVec);
328impl_vec_eq!(ExternalResource, ExternalResourceVec);
329impl_vec_partialord!(ExternalResource, ExternalResourceVec);
330impl_vec_ord!(ExternalResource, ExternalResourceVec);
331impl_vec_hash!(ExternalResource, ExternalResourceVec);
332impl_vec_clone!(
333    ExternalResource,
334    ExternalResourceVec,
335    ExternalResourceVecDestructor
336);
337
338/// AUDIT 2026-07-08: maximum XML/HTML nesting depth handled by the recursive
339/// DOM-build (`xml_node_to_dom_fast`, `xml_node_to_fast_dom`), resource-scan
340/// (iterative worklist in `scan_external_resources`) and `<body>`-lookup
341/// (`find_body_recursive`) passes. These bound descent per nesting level, so a pathologically deep
342/// document (e.g. tens of thousands of nested `<div>`s) would overflow the native
343/// stack. Beyond this depth, deeper children are ignored rather than crashing.
344/// 512 is far past any realistic hand-authored markup while staying comfortably
345/// inside the default thread stack.
346const MAX_XML_NESTING_DEPTH: usize = 512;
347
348/// AUDIT 2026-07-08: maximum recursion depth for [`ComponentFieldType::parse`],
349/// which recurses through `Option<..>` / `Vec<..>` wrappers. Caps attacker
350/// strings such as `"Option<".repeat(100_000)` that would otherwise overflow the
351/// stack. 64 nested type wrappers is far beyond any real field type.
352const MAX_TYPE_PARSE_DEPTH: usize = 64;
353
354#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
355#[repr(C)]
356pub struct Xml {
357    pub root: XmlNodeChildVec,
358}
359
360impl Xml {
361    /// Scan the XML/HTML document for external resource URLs.
362    ///
363    /// This function traverses the entire document tree and extracts URLs from:
364    /// - `<img src="...">` - Images
365    /// - `<link href="...">` - Stylesheets, icons, fonts
366    /// - `<script src="...">` - Scripts
367    /// - `<video src="...">`, `<source src="...">` - Video
368    /// - `<audio src="...">` - Audio
369    /// - `<a href="...">` - Links (classified as Unknown)
370    /// - CSS `url()` in style attributes
371    /// - `<style>` blocks with @import or `url()`
372    #[must_use] pub fn scan_external_resources(&self) -> ExternalResourceVec {
373        let mut resources = Vec::new();
374
375        // AUDIT 2026-07-08: iterative DFS with an explicit worklist. The old
376        // per-node recursion overflowed the stack on pathologically deep markup
377        // (a single-purpose scan frame is large: string lowercasing + closure +
378        // wide match). An explicit stack keeps memory on the heap; `depth` still
379        // bounds how deep we descend so unbounded input can't grow the worklist
380        // without limit.
381        let mut stack: Vec<(&XmlNodeChild, usize)> = Vec::new();
382        for child in self.root.as_ref() {
383            stack.push((child, 0));
384        }
385        while let Some((child, depth)) = stack.pop() {
386            match child {
387                XmlNodeChild::Text(text) => {
388                    // CSS @import / url() in text content (inside <style> tags).
389                    Self::extract_css_urls(text.as_str(), &mut resources);
390                }
391                XmlNodeChild::Element(node) => {
392                    if depth > MAX_XML_NESTING_DEPTH {
393                        // Deeper subtrees are simply not scanned.
394                        continue;
395                    }
396                    Self::scan_node(node, &mut resources);
397                    for c in node.children.as_ref() {
398                        stack.push((c, depth + 1));
399                    }
400                }
401            }
402        }
403
404        resources.into()
405    }
406
407    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
408    fn scan_node(node: &XmlNode, resources: &mut Vec<ExternalResource>) {
409        let tag_name = node.node_type.inner.as_str().to_lowercase();
410
411        // Get attribute lookup helper
412        let get_attr = |name: &str| -> Option<String> {
413            node.attributes
414                .inner
415                .as_ref()
416                .iter()
417                .find(|pair| pair.key.as_str().eq_ignore_ascii_case(name))
418                .map(|pair| pair.value.as_str().to_string())
419        };
420
421        match tag_name.as_str() {
422            "img" => {
423                if let Some(src) = get_attr("src") {
424                    let mime = Self::guess_mime_from_url(&src, "image");
425                    resources.push(ExternalResource {
426                        url: AzString::from(src),
427                        kind: ExternalResourceKind::Image,
428                        mime_type: mime.into(),
429                        source_element: AzString::from("img"),
430                        source_attribute: AzString::from("src"),
431                    });
432                }
433                // Also check srcset
434                if let Some(srcset) = get_attr("srcset") {
435                    for src in Self::parse_srcset(&srcset) {
436                        let mime = Self::guess_mime_from_url(&src, "image");
437                        resources.push(ExternalResource {
438                            url: AzString::from(src),
439                            kind: ExternalResourceKind::Image,
440                            mime_type: mime.into(),
441                            source_element: AzString::from("img"),
442                            source_attribute: AzString::from("srcset"),
443                        });
444                    }
445                }
446            }
447            "link" => {
448                if let Some(href) = get_attr("href") {
449                    let rel = get_attr("rel").unwrap_or_default().to_lowercase();
450                    let type_attr = get_attr("type");
451                    let as_attr = get_attr("as").unwrap_or_default().to_lowercase();
452
453                    let (kind, category) = if rel.contains("stylesheet") {
454                        (ExternalResourceKind::Stylesheet, "stylesheet")
455                    } else if rel.contains("icon") || rel.contains("apple-touch-icon") {
456                        (ExternalResourceKind::Icon, "image")
457                    } else if as_attr == "font" {
458                        (ExternalResourceKind::Font, "font")
459                    } else if as_attr == "script" {
460                        (ExternalResourceKind::Script, "script")
461                    } else if as_attr == "image" {
462                        (ExternalResourceKind::Image, "image")
463                    } else {
464                        (ExternalResourceKind::Unknown, "")
465                    };
466
467                    let mime = type_attr
468                        .map(|t| MimeTypeHint::new(&t))
469                        .or_else(|| Self::guess_mime_from_url(&href, category));
470
471                    resources.push(ExternalResource {
472                        url: AzString::from(href),
473                        kind,
474                        mime_type: mime.into(),
475                        source_element: AzString::from("link"),
476                        source_attribute: AzString::from("href"),
477                    });
478                }
479            }
480            "script" => {
481                if let Some(src) = get_attr("src") {
482                    let type_attr = get_attr("type");
483                    let mime = type_attr
484                        .map(|t| MimeTypeHint::new(&t))
485                        .or_else(|| Some(MimeTypeHint::new("application/javascript")));
486
487                    resources.push(ExternalResource {
488                        url: AzString::from(src),
489                        kind: ExternalResourceKind::Script,
490                        mime_type: mime.into(),
491                        source_element: AzString::from("script"),
492                        source_attribute: AzString::from("src"),
493                    });
494                }
495            }
496            "video" => {
497                if let Some(src) = get_attr("src") {
498                    let mime = Self::guess_mime_from_url(&src, "video");
499                    resources.push(ExternalResource {
500                        url: AzString::from(src),
501                        kind: ExternalResourceKind::Video,
502                        mime_type: mime.into(),
503                        source_element: AzString::from("video"),
504                        source_attribute: AzString::from("src"),
505                    });
506                }
507                if let Some(poster) = get_attr("poster") {
508                    let mime = Self::guess_mime_from_url(&poster, "image");
509                    resources.push(ExternalResource {
510                        url: AzString::from(poster),
511                        kind: ExternalResourceKind::Image,
512                        mime_type: mime.into(),
513                        source_element: AzString::from("video"),
514                        source_attribute: AzString::from("poster"),
515                    });
516                }
517            }
518            "audio" => {
519                if let Some(src) = get_attr("src") {
520                    let mime = Self::guess_mime_from_url(&src, "audio");
521                    resources.push(ExternalResource {
522                        url: AzString::from(src),
523                        kind: ExternalResourceKind::Audio,
524                        mime_type: mime.into(),
525                        source_element: AzString::from("audio"),
526                        source_attribute: AzString::from("src"),
527                    });
528                }
529            }
530            "source" => {
531                if let Some(src) = get_attr("src") {
532                    let type_attr = get_attr("type");
533                    // Determine kind based on type or parent (heuristic: assume video)
534                    let kind = if type_attr
535                        .as_ref()
536                        .is_some_and(|t| t.starts_with("audio"))
537                    {
538                        ExternalResourceKind::Audio
539                    } else {
540                        ExternalResourceKind::Video
541                    };
542                    let mime = type_attr.map(|t| MimeTypeHint::new(&t)).or_else(|| {
543                        Self::guess_mime_from_url(
544                            &src,
545                            if kind == ExternalResourceKind::Audio {
546                                "audio"
547                            } else {
548                                "video"
549                            },
550                        )
551                    });
552
553                    resources.push(ExternalResource {
554                        url: AzString::from(src),
555                        kind,
556                        mime_type: mime.into(),
557                        source_element: AzString::from("source"),
558                        source_attribute: AzString::from("src"),
559                    });
560                }
561                // Also handle srcset for picture elements
562                if let Some(srcset) = get_attr("srcset") {
563                    for src in Self::parse_srcset(&srcset) {
564                        let mime = Self::guess_mime_from_url(&src, "image");
565                        resources.push(ExternalResource {
566                            url: AzString::from(src),
567                            kind: ExternalResourceKind::Image,
568                            mime_type: mime.into(),
569                            source_element: AzString::from("source"),
570                            source_attribute: AzString::from("srcset"),
571                        });
572                    }
573                }
574            }
575            "a" => {
576                if let Some(href) = get_attr("href") {
577                    // Only include if it looks like a resource, not a page link
578                    if Self::looks_like_resource(&href) {
579                        let mime = Self::guess_mime_from_url(&href, "");
580                        resources.push(ExternalResource {
581                            url: AzString::from(href),
582                            kind: ExternalResourceKind::Unknown,
583                            mime_type: mime.into(),
584                            source_element: AzString::from("a"),
585                            source_attribute: AzString::from("href"),
586                        });
587                    }
588                }
589            }
590            "virtualized-view" | "embed" | "object" => {
591                let src_attr = if tag_name == "object" { "data" } else { "src" };
592                if let Some(src) = get_attr(src_attr) {
593                    resources.push(ExternalResource {
594                        url: AzString::from(src),
595                        kind: ExternalResourceKind::Unknown,
596                        mime_type: OptionMimeTypeHint::None,
597                        source_element: AzString::from(tag_name.clone()),
598                        source_attribute: AzString::from(src_attr),
599                    });
600                }
601            }
602            "style" => {
603                // Scan text content for CSS URLs
604                for child in node.children.as_ref() {
605                    if let XmlNodeChild::Text(text) = child {
606                        Self::extract_css_urls(text.as_str(), resources);
607                    }
608                }
609            }
610            _ => {}
611        }
612
613        // Check inline style attribute for url()
614        if let Some(style) = get_attr("style") {
615            Self::extract_css_urls(&style, resources);
616        }
617
618        // Check for background attribute (deprecated but still used)
619        if let Some(bg) = get_attr("background") {
620            let mime = Self::guess_mime_from_url(&bg, "image");
621            resources.push(ExternalResource {
622                url: AzString::from(bg),
623                kind: ExternalResourceKind::Image,
624                mime_type: mime.into(),
625                source_element: AzString::from(tag_name),
626                source_attribute: AzString::from("background"),
627            });
628        }
629
630        // Children are walked by the iterative driver in `scan_external_resources`.
631    }
632
633    /// Extract URLs from CSS content (handles `url()` and @import)
634    fn extract_css_urls(css: &str, resources: &mut Vec<ExternalResource>) {
635        // AUDIT 2026-07-08: fold to lowercase ONCE using ASCII-only folding.
636        // `to_ascii_lowercase` never changes a string's byte length (only A-Z are
637        // touched, multi-byte code points are left verbatim), so every byte offset
638        // into `lower` maps 1:1 onto `css`. The old code called `to_lowercase()`
639        // every iteration (O(n^2)) and then sliced the ORIGINAL `css` with an
640        // offset computed in the lowercased temporary -- for characters whose
641        // lowercase changes byte length (e.g. 'İ' U+0130, 2 bytes -> 3 bytes) that
642        // offset landed off a char boundary and panicked. Searching in `lower` and
643        // slicing `css` at the same offset also makes the `url(` / `@import` scans
644        // case-insensitive for free.
645        let lower = css.to_ascii_lowercase();
646
647        // url(...) scan (case-insensitive)
648        let mut search_from = 0;
649        while let Some(rel) = lower[search_from..].find("url(") {
650            let url_start = search_from + rel;
651            let after = url_start + 4;
652            // Skip a `url(` that is the argument of an `@import` — the @import scan
653            // below emits it, correctly tagged as a Stylesheet. Without this guard the
654            // same URL is pushed twice (once here, mistagged "url()").
655            if lower[..url_start].trim_end().ends_with("@import") {
656                search_from = after;
657                continue;
658            }
659            let after_url = &css[after..];
660            if let Some(url) = Self::extract_url_value(after_url) {
661                let mime = Self::guess_mime_from_url(&url, "");
662                let kind = Self::guess_kind_from_url(&url);
663                resources.push(ExternalResource {
664                    url: AzString::from(url),
665                    kind,
666                    mime_type: mime.into(),
667                    source_element: AzString::from("style"),
668                    source_attribute: AzString::from("url()"),
669                });
670            }
671            search_from = after;
672        }
673
674        // Handle @import "url" or @import url(...) (case-insensitive)
675        let mut search_from = 0;
676        while let Some(rel) = lower[search_from..].find("@import") {
677            let after = search_from + rel + 7;
678            let after_import = &css[after..];
679            let trimmed = after_import.trim_start();
680
681            // Match `url(` case-insensitively without allocating. `get(..4)`
682            // returns `None` if byte 4 is not a char boundary, so the slice below
683            // can never panic on multi-byte input.
684            let import_url = if trimmed.get(..4).is_some_and(|p| p.eq_ignore_ascii_case("url(")) {
685                Self::extract_url_value(&trimmed[4..])
686            } else {
687                Self::extract_quoted_string(trimmed)
688            };
689
690            if let Some(url) = import_url {
691                resources.push(ExternalResource {
692                    url: AzString::from(url),
693                    kind: ExternalResourceKind::Stylesheet,
694                    mime_type: Some(MimeTypeHint::new("text/css")).into(),
695                    source_element: AzString::from("style"),
696                    source_attribute: AzString::from("@import"),
697                });
698            }
699
700            search_from = after;
701        }
702    }
703
704    /// Extract value from url(...) - handles quoted and unquoted URLs
705    fn extract_url_value(s: &str) -> Option<String> {
706        let trimmed = s.trim_start();
707        if trimmed.starts_with('"') {
708            Self::extract_quoted_string(trimmed)
709        } else if let Some(rest) = trimmed.strip_prefix('\'') {
710            let end = rest.find('\'')?;
711            Some(rest[..end].to_string())
712        } else {
713            let end = trimmed.find(')')?;
714            Some(trimmed[..end].trim().to_string())
715        }
716    }
717
718    /// Extract a quoted string value
719    fn extract_quoted_string(s: &str) -> Option<String> {
720        if let Some(rest) = s.strip_prefix('"') {
721            let end = rest.find('"')?;
722            Some(rest[..end].to_string())
723        } else if let Some(rest) = s.strip_prefix('\'') {
724            let end = rest.find('\'')?;
725            Some(rest[..end].to_string())
726        } else {
727            None
728        }
729    }
730
731    /// Parse srcset attribute into individual URLs
732    fn parse_srcset(srcset: &str) -> Vec<String> {
733        srcset
734            .split(',')
735            .filter_map(|entry| {
736                let trimmed = entry.trim();
737                // srcset format: "url 1x" or "url 100w"
738                trimmed.split_whitespace().next().map(alloc::string::ToString::to_string)
739            })
740            .filter(|url| !url.is_empty())
741            .collect()
742    }
743
744    /// Check if a URL looks like a downloadable resource (not a page)
745    fn looks_like_resource(url: &str) -> bool {
746        let lower = url.to_lowercase();
747        // Check for common resource extensions
748        let resource_exts = [
749            ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp", ".ttf", ".otf",
750            ".woff", ".woff2", ".eot", ".css", ".js", ".mp4", ".webm", ".ogg", ".mp3", ".wav",
751            ".pdf", ".zip", ".tar", ".gz",
752        ];
753        resource_exts.iter().any(|ext| lower.ends_with(ext))
754    }
755
756    /// Guess the resource kind from URL based on file extension.
757    // `url` is lowercased into `path` below, so these literal `.ext` checks are
758    // already case-insensitive — the lint can't see the runtime lowercasing.
759    #[allow(clippy::case_sensitive_file_extension_comparisons)]
760    fn guess_kind_from_url(url: &str) -> ExternalResourceKind {
761        let lower = url.to_lowercase();
762        // Strip query string before checking extension
763        let path = lower.split('?').next().unwrap_or(&lower);
764        if path.ends_with(".png")
765            || path.ends_with(".jpg")
766            || path.ends_with(".jpeg")
767            || path.ends_with(".gif")
768            || path.ends_with(".webp")
769            || path.ends_with(".svg")
770            || path.ends_with(".bmp")
771            || path.ends_with(".avif")
772        {
773            ExternalResourceKind::Image
774        } else if path.ends_with(".ttf")
775            || path.ends_with(".otf")
776            || path.ends_with(".woff")
777            || path.ends_with(".woff2")
778            || path.ends_with(".eot")
779        {
780            ExternalResourceKind::Font
781        } else if path.ends_with(".css") {
782            ExternalResourceKind::Stylesheet
783        } else if path.ends_with(".js") || path.ends_with(".mjs") {
784            ExternalResourceKind::Script
785        } else if path.ends_with(".mp4") || path.ends_with(".webm") || path.ends_with(".ogg") {
786            ExternalResourceKind::Video
787        } else if path.ends_with(".mp3") || path.ends_with(".wav") || path.ends_with(".flac") {
788            ExternalResourceKind::Audio
789        } else if path.ends_with(".ico") {
790            ExternalResourceKind::Icon
791        } else {
792            ExternalResourceKind::Unknown
793        }
794    }
795
796    /// Guess MIME type from URL based on extension
797    fn guess_mime_from_url(url: &str, category: &str) -> Option<MimeTypeHint> {
798        let lower = url.to_lowercase();
799        // Find extension
800        let ext = lower.rsplit('.').next()?;
801        // Remove query string if present
802        let ext = ext.split('?').next()?;
803
804        // Check if it's a valid extension
805        let valid_exts = [
806            "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "avif", "ttf", "otf", "woff",
807            "woff2", "eot", "css", "js", "mjs", "mp4", "webm", "ogg", "mp3", "wav", "flac",
808        ];
809
810        if valid_exts.contains(&ext) {
811            Some(MimeTypeHint::from_extension(ext))
812        } else if !category.is_empty() {
813            // Use category hint for default
814            match category {
815                "image" => Some(MimeTypeHint::new("image/*")),
816                "font" => Some(MimeTypeHint::new("font/*")),
817                "stylesheet" => Some(MimeTypeHint::new("text/css")),
818                "script" => Some(MimeTypeHint::new("application/javascript")),
819                "video" => Some(MimeTypeHint::new("video/*")),
820                "audio" => Some(MimeTypeHint::new("audio/*")),
821                _ => None,
822            }
823        } else {
824            None
825        }
826    }
827}
828
829#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
830#[repr(C)]
831pub struct NonXmlCharError {
832    pub ch: u32, /* u32 = char, but ABI stable */
833    pub pos: XmlTextPos,
834}
835
836#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
837#[repr(C)]
838pub struct InvalidCharError {
839    pub expected: u8,
840    pub got: u8,
841    pub pos: XmlTextPos,
842}
843
844#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
845#[repr(C)]
846pub struct InvalidCharMultipleError {
847    pub expected: u8,
848    pub got: U8Vec,
849    pub pos: XmlTextPos,
850}
851
852#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
853#[repr(C)]
854pub struct InvalidQuoteError {
855    pub got: u8,
856    pub pos: XmlTextPos,
857}
858
859#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
860#[repr(C)]
861pub struct InvalidSpaceError {
862    pub got: u8,
863    pub pos: XmlTextPos,
864}
865
866#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
867#[repr(C)]
868pub struct InvalidStringError {
869    pub got: AzString,
870    pub pos: XmlTextPos,
871}
872
873#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
874#[repr(C, u8)]
875pub enum XmlStreamError {
876    UnexpectedEndOfStream,
877    InvalidName,
878    NonXmlChar(NonXmlCharError),
879    InvalidChar(InvalidCharError),
880    InvalidCharMultiple(InvalidCharMultipleError),
881    InvalidQuote(InvalidQuoteError),
882    InvalidSpace(InvalidSpaceError),
883    InvalidString(InvalidStringError),
884    InvalidReference,
885    InvalidExternalID,
886    InvalidCommentData,
887    InvalidCommentEnd,
888    InvalidCharacterData,
889}
890
891impl fmt::Display for XmlStreamError {
892    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
893        use self::XmlStreamError::{UnexpectedEndOfStream, InvalidName, NonXmlChar, InvalidChar, InvalidCharMultiple, InvalidQuote, InvalidSpace, InvalidString, InvalidReference, InvalidExternalID, InvalidCommentData, InvalidCommentEnd, InvalidCharacterData};
894        match self {
895            UnexpectedEndOfStream => write!(f, "Unexpected end of stream"),
896            InvalidName => write!(f, "Invalid name"),
897            NonXmlChar(nx) => write!(
898                f,
899                "Non-XML character: {:?} at {}",
900                core::char::from_u32(nx.ch),
901                nx.pos
902            ),
903            InvalidChar(ic) => write!(
904                f,
905                "Invalid character: expected: {}, got: {} at {}",
906                ic.expected as char, ic.got as char, ic.pos
907            ),
908            InvalidCharMultiple(imc) => write!(
909                f,
910                "Multiple invalid characters: expected: {}, got: {:?} at {}",
911                imc.expected,
912                imc.got.as_ref(),
913                imc.pos
914            ),
915            InvalidQuote(iq) => write!(f, "Invalid quote: got {} at {}", iq.got as char, iq.pos),
916            InvalidSpace(is) => write!(f, "Invalid space: got {} at {}", is.got as char, is.pos),
917            InvalidString(ise) => write!(
918                f,
919                "Invalid string: got \"{}\" at {}",
920                ise.got.as_str(),
921                ise.pos
922            ),
923            InvalidReference => write!(f, "Invalid reference"),
924            InvalidExternalID => write!(f, "Invalid external ID"),
925            InvalidCommentData => write!(f, "Invalid comment data"),
926            InvalidCommentEnd => write!(f, "Invalid comment end"),
927            InvalidCharacterData => write!(f, "Invalid character data"),
928        }
929    }
930}
931
932#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Ord, Hash, Eq)]
933#[repr(C)]
934pub struct XmlTextPos {
935    pub row: u32,
936    pub col: u32,
937}
938
939impl fmt::Display for XmlTextPos {
940    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
941        write!(f, "line {}:{}", self.row, self.col)
942    }
943}
944
945#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
946#[repr(C)]
947pub struct XmlTextError {
948    pub stream_error: XmlStreamError,
949    pub pos: XmlTextPos,
950}
951
952#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
953#[repr(C, u8)]
954pub enum XmlParseError {
955    InvalidDeclaration(XmlTextError),
956    InvalidComment(XmlTextError),
957    InvalidPI(XmlTextError),
958    InvalidDoctype(XmlTextError),
959    InvalidEntity(XmlTextError),
960    InvalidElement(XmlTextError),
961    InvalidAttribute(XmlTextError),
962    InvalidCdata(XmlTextError),
963    InvalidCharData(XmlTextError),
964    UnknownToken(XmlTextPos),
965}
966
967impl fmt::Display for XmlParseError {
968    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
969        use self::XmlParseError::{InvalidDeclaration, InvalidComment, InvalidPI, InvalidDoctype, InvalidEntity, InvalidElement, InvalidAttribute, InvalidCdata, InvalidCharData, UnknownToken};
970        match self {
971            InvalidDeclaration(e) => {
972                write!(f, "Invalid declaration: {} at {}", e.stream_error, e.pos)
973            }
974            InvalidComment(e) => write!(f, "Invalid comment: {} at {}", e.stream_error, e.pos),
975            InvalidPI(e) => write!(
976                f,
977                "Invalid processing instruction: {} at {}",
978                e.stream_error, e.pos
979            ),
980            InvalidDoctype(e) => write!(f, "Invalid doctype: {} at {}", e.stream_error, e.pos),
981            InvalidEntity(e) => write!(f, "Invalid entity: {} at {}", e.stream_error, e.pos),
982            InvalidElement(e) => write!(f, "Invalid element: {} at {}", e.stream_error, e.pos),
983            InvalidAttribute(e) => write!(f, "Invalid attribute: {} at {}", e.stream_error, e.pos),
984            InvalidCdata(e) => write!(f, "Invalid CDATA: {} at {}", e.stream_error, e.pos),
985            InvalidCharData(e) => write!(f, "Invalid char data: {} at {}", e.stream_error, e.pos),
986            UnknownToken(e) => write!(f, "Unknown token at {e}"),
987        }
988    }
989}
990
991impl_result!(
992    Xml,
993    XmlError,
994    ResultXmlXmlError,
995    copy = false,
996    [Debug, PartialEq, Eq, PartialOrd, Clone]
997);
998
999#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1000#[repr(C)]
1001pub struct DuplicatedNamespaceError {
1002    pub ns: AzString,
1003    pub pos: XmlTextPos,
1004}
1005
1006#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1007#[repr(C)]
1008pub struct UnknownNamespaceError {
1009    pub ns: AzString,
1010    pub pos: XmlTextPos,
1011}
1012
1013#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1014#[repr(C)]
1015pub struct UnexpectedCloseTagError {
1016    pub expected: AzString,
1017    pub actual: AzString,
1018    pub pos: XmlTextPos,
1019}
1020
1021#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1022#[repr(C)]
1023pub struct UnknownEntityReferenceError {
1024    pub entity: AzString,
1025    pub pos: XmlTextPos,
1026}
1027
1028#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1029#[repr(C)]
1030pub struct DuplicatedAttributeError {
1031    pub attribute: AzString,
1032    pub pos: XmlTextPos,
1033}
1034
1035/// Error for mismatched open/close tags in XML hierarchy
1036#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1037#[repr(C)]
1038pub struct MalformedHierarchyError {
1039    /// The tag that was expected (from the opening tag)
1040    pub expected: AzString,
1041    /// The tag that was actually found (the closing tag)
1042    pub got: AzString,
1043}
1044
1045#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1046#[repr(C, u8)]
1047pub enum XmlError {
1048    NoParserAvailable,
1049    InvalidXmlPrefixUri(XmlTextPos),
1050    UnexpectedXmlUri(XmlTextPos),
1051    UnexpectedXmlnsUri(XmlTextPos),
1052    InvalidElementNamePrefix(XmlTextPos),
1053    DuplicatedNamespace(DuplicatedNamespaceError),
1054    UnknownNamespace(UnknownNamespaceError),
1055    UnexpectedCloseTag(UnexpectedCloseTagError),
1056    UnexpectedEntityCloseTag(XmlTextPos),
1057    UnknownEntityReference(UnknownEntityReferenceError),
1058    MalformedEntityReference(XmlTextPos),
1059    EntityReferenceLoop(XmlTextPos),
1060    InvalidAttributeValue(XmlTextPos),
1061    DuplicatedAttribute(DuplicatedAttributeError),
1062    NoRootNode,
1063    SizeLimit,
1064    DtdDetected,
1065    /// Invalid hierarchy close tags, i.e `<app></p></app>`
1066    MalformedHierarchy(MalformedHierarchyError),
1067    ParserError(XmlParseError),
1068    UnclosedRootNode,
1069    UnexpectedDeclaration(XmlTextPos),
1070    NodesLimitReached,
1071    AttributesLimitReached,
1072    NamespacesLimitReached,
1073    InvalidName(XmlTextPos),
1074    NonXmlChar(XmlTextPos),
1075    InvalidChar(XmlTextPos),
1076    InvalidChar2(XmlTextPos),
1077    InvalidString(XmlTextPos),
1078    InvalidExternalID(XmlTextPos),
1079    InvalidComment(XmlTextPos),
1080    InvalidCharacterData(XmlTextPos),
1081    UnknownToken(XmlTextPos),
1082    UnexpectedEndOfStream,
1083}
1084
1085impl fmt::Display for XmlError {
1086    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1087        use self::XmlError::{NoParserAvailable, InvalidXmlPrefixUri, UnexpectedXmlUri, UnexpectedXmlnsUri, InvalidElementNamePrefix, DuplicatedNamespace, UnknownNamespace, UnexpectedCloseTag, UnexpectedEntityCloseTag, UnknownEntityReference, MalformedEntityReference, EntityReferenceLoop, InvalidAttributeValue, DuplicatedAttribute, NoRootNode, SizeLimit, DtdDetected, MalformedHierarchy, ParserError, UnclosedRootNode, UnexpectedDeclaration, NodesLimitReached, AttributesLimitReached, NamespacesLimitReached, InvalidName, NonXmlChar, InvalidChar, InvalidChar2, InvalidString, InvalidExternalID, InvalidComment, InvalidCharacterData, UnknownToken, UnexpectedEndOfStream};
1088        match self {
1089            NoParserAvailable => write!(
1090                f,
1091                "Library was compiled without XML parser (XML parser not available)"
1092            ),
1093            InvalidXmlPrefixUri(pos) => {
1094                write!(f, "Invalid XML Prefix URI at line {}:{}", pos.row, pos.col)
1095            }
1096            UnexpectedXmlUri(pos) => {
1097                write!(f, "Unexpected XML URI at line {}:{}", pos.row, pos.col)
1098            }
1099            UnexpectedXmlnsUri(pos) => write!(
1100                f,
1101                "Unexpected XML namespace URI at line {}:{}",
1102                pos.row, pos.col
1103            ),
1104            InvalidElementNamePrefix(pos) => write!(
1105                f,
1106                "Invalid element name prefix at line {}:{}",
1107                pos.row, pos.col
1108            ),
1109            DuplicatedNamespace(ns) => write!(
1110                f,
1111                "Duplicated namespace: \"{}\" at {}",
1112                ns.ns.as_str(),
1113                ns.pos
1114            ),
1115            UnknownNamespace(uns) => write!(
1116                f,
1117                "Unknown namespace: \"{}\" at {}",
1118                uns.ns.as_str(),
1119                uns.pos
1120            ),
1121            UnexpectedCloseTag(ct) => write!(
1122                f,
1123                "Unexpected close tag: expected \"{}\", got \"{}\" at {}",
1124                ct.expected.as_str(),
1125                ct.actual.as_str(),
1126                ct.pos
1127            ),
1128            UnexpectedEntityCloseTag(pos) => write!(
1129                f,
1130                "Unexpected entity close tag at line {}:{}",
1131                pos.row, pos.col
1132            ),
1133            UnknownEntityReference(uer) => write!(
1134                f,
1135                "Unexpected entity reference: \"{}\" at {}",
1136                uer.entity, uer.pos
1137            ),
1138            MalformedEntityReference(pos) => write!(
1139                f,
1140                "Malformed entity reference at line {}:{}",
1141                pos.row, pos.col
1142            ),
1143            EntityReferenceLoop(pos) => write!(
1144                f,
1145                "Entity reference loop (recursive entity reference) at line {}:{}",
1146                pos.row, pos.col
1147            ),
1148            InvalidAttributeValue(pos) => {
1149                write!(f, "Invalid attribute value at line {}:{}", pos.row, pos.col)
1150            }
1151            DuplicatedAttribute(ae) => write!(
1152                f,
1153                "Duplicated attribute \"{}\" at line {}:{}",
1154                ae.attribute.as_str(),
1155                ae.pos.row,
1156                ae.pos.col
1157            ),
1158            NoRootNode => write!(f, "No root node found"),
1159            SizeLimit => write!(f, "XML file too large (size limit reached)"),
1160            DtdDetected => write!(f, "Document type descriptor detected"),
1161            MalformedHierarchy(e) => write!(
1162                f,
1163                "Malformed hierarchy: expected <{}/> closing tag, got <{}/>",
1164                e.expected.as_str(),
1165                e.got.as_str()
1166            ),
1167            ParserError(p) => write!(f, "{p}"),
1168            UnclosedRootNode => write!(f, "unclosed root node"),
1169            UnexpectedDeclaration(tp) => write!(f, "unexpected declaration at {tp}"),
1170            NodesLimitReached => write!(f, "nodes limit reached"),
1171            AttributesLimitReached => write!(f, "attributes limit reached"),
1172            NamespacesLimitReached => write!(f, "namespaces limit reached"),
1173            InvalidName(tp) => write!(f, "invalid name at {tp}"),
1174            NonXmlChar(tp) => write!(f, "non xml char at {tp}"),
1175            InvalidChar(tp) => write!(f, "invalid char at {tp}"),
1176            InvalidChar2(tp) => write!(f, "invalid char2 at {tp}"),
1177            InvalidString(tp) => write!(f, "invalid string at {tp}"),
1178            InvalidExternalID(tp) => write!(f, "invalid externalid at {tp}"),
1179            InvalidComment(tp) => write!(f, "invalid comment at {tp}"),
1180            InvalidCharacterData(tp) => write!(f, "invalid character data at {tp}"),
1181            UnknownToken(tp) => write!(f, "unknown token at {tp}"),
1182            UnexpectedEndOfStream => write!(f, "unexpected end of stream"),
1183        }
1184    }
1185}
1186
1187// ============================================================================
1188// New repr(C) component system
1189// ============================================================================
1190
1191/// Identifies a component within a library collection.
1192/// e.g. collection="builtin", name="div" for the `<div>` element,
1193/// or collection="shadcn", name="avatar" for a custom component.
1194#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1195#[repr(C)]
1196pub struct ComponentId {
1197    /// Library / collection name: "builtin", "shadcn", "myproject"
1198    pub collection: AzString,
1199    /// Component name within the collection: "div", "avatar", "card"
1200    pub name: AzString,
1201}
1202
1203impl ComponentId {
1204    #[must_use] pub fn builtin(name: &str) -> Self {
1205        Self {
1206            collection: AzString::from_const_str("builtin"),
1207            name: AzString::from(name),
1208        }
1209    }
1210
1211    #[must_use] pub fn new(collection: &str, name: &str) -> Self {
1212        Self {
1213            collection: AzString::from(collection),
1214            name: AzString::from(name),
1215        }
1216    }
1217
1218    /// Returns "collection:name" format string
1219    #[must_use] pub fn qualified_name(&self) -> String {
1220        format!("{}:{}", self.collection.as_str(), self.name.as_str())
1221    }
1222}
1223
1224// ============================================================================
1225// Component type system — rich type descriptors for component fields
1226// ============================================================================
1227
1228/// A single argument in a callback signature.
1229#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1230#[repr(C)]
1231pub struct ComponentCallbackArg {
1232    /// Argument name, e.g. "`button_id`"
1233    pub name: AzString,
1234    /// Argument type
1235    pub arg_type: ComponentFieldType,
1236}
1237
1238impl_vec!(
1239    ComponentCallbackArg,
1240    ComponentCallbackArgVec,
1241    ComponentCallbackArgVecDestructor,
1242    ComponentCallbackArgVecDestructorType,
1243    ComponentCallbackArgVecSlice,
1244    OptionComponentCallbackArg
1245);
1246impl_option!(
1247    ComponentCallbackArg,
1248    OptionComponentCallbackArg,
1249    copy = false,
1250    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1251);
1252impl_vec_debug!(ComponentCallbackArg, ComponentCallbackArgVec);
1253impl_vec_partialeq!(ComponentCallbackArg, ComponentCallbackArgVec);
1254impl_vec_eq!(ComponentCallbackArg, ComponentCallbackArgVec);
1255impl_vec_partialord!(ComponentCallbackArg, ComponentCallbackArgVec);
1256impl_vec_ord!(ComponentCallbackArg, ComponentCallbackArgVec);
1257impl_vec_hash!(ComponentCallbackArg, ComponentCallbackArgVec);
1258impl_vec_clone!(
1259    ComponentCallbackArg,
1260    ComponentCallbackArgVec,
1261    ComponentCallbackArgVecDestructor
1262);
1263
1264/// Callback signature: return type + argument list.
1265#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1266#[repr(C)]
1267pub struct ComponentCallbackSignature {
1268    /// Return type name, e.g. "Update"
1269    pub return_type: AzString,
1270    /// Callback arguments (excluding the implicit `&mut RefAny` and `&mut CallbackInfo`)
1271    pub args: ComponentCallbackArgVec,
1272}
1273
1274/// Heap-allocated box for recursive `ComponentFieldType` (e.g. `Option<String>`).
1275/// Uses raw pointer indirection to break the infinite size.
1276#[repr(C)]
1277pub struct ComponentFieldTypeBox {
1278    pub ptr: *mut ComponentFieldType,
1279}
1280
1281impl ComponentFieldTypeBox {
1282    #[must_use] pub fn new(t: ComponentFieldType) -> Self {
1283        Self {
1284            ptr: Box::into_raw(Box::new(t)),
1285        }
1286    }
1287
1288    #[must_use] pub fn as_ref(&self) -> &ComponentFieldType {
1289        unsafe { &*self.ptr }
1290    }
1291}
1292
1293impl Clone for ComponentFieldTypeBox {
1294    fn clone(&self) -> Self {
1295        Self::new(unsafe { (*self.ptr).clone() })
1296    }
1297}
1298
1299impl Drop for ComponentFieldTypeBox {
1300    fn drop(&mut self) {
1301        // Null the pointer as we free it, so a *second* drop is a no-op instead
1302        // of a double free. This type is a by-value payload of the
1303        // `ComponentFieldType` enum, whose codegen FFI mirror gets
1304        // `impl Drop { _delete }` (= drop_in_place of the real type) AND Rust
1305        // field drop-glue — dropping each by-value field twice. Without this
1306        // take-and-null the second drop would `Box::from_raw` a dangling pointer.
1307        let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
1308        if !ptr.is_null() {
1309            unsafe {
1310                drop(Box::from_raw(ptr));
1311            }
1312        }
1313    }
1314}
1315
1316impl fmt::Debug for ComponentFieldTypeBox {
1317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1318        if self.ptr.is_null() {
1319            write!(f, "ComponentFieldTypeBox(null)")
1320        } else {
1321            write!(f, "ComponentFieldTypeBox({:?})", unsafe { &*self.ptr })
1322        }
1323    }
1324}
1325
1326impl PartialEq for ComponentFieldTypeBox {
1327    fn eq(&self, other: &Self) -> bool {
1328        if self.ptr.is_null() && other.ptr.is_null() {
1329            return true;
1330        }
1331        if self.ptr.is_null() || other.ptr.is_null() {
1332            return false;
1333        }
1334        unsafe { *self.ptr == *other.ptr }
1335    }
1336}
1337
1338impl Eq for ComponentFieldTypeBox {}
1339
1340impl PartialOrd for ComponentFieldTypeBox {
1341    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1342        Some(self.cmp(other))
1343    }
1344}
1345
1346impl Ord for ComponentFieldTypeBox {
1347    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1348        match (self.ptr.is_null(), other.ptr.is_null()) {
1349            (true, true) => core::cmp::Ordering::Equal,
1350            (true, false) => core::cmp::Ordering::Less,
1351            (false, true) => core::cmp::Ordering::Greater,
1352            (false, false) => unsafe { (*self.ptr).cmp(&*other.ptr) },
1353        }
1354    }
1355}
1356
1357impl Hash for ComponentFieldTypeBox {
1358    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1359        if !self.ptr.is_null() {
1360            unsafe {
1361                (*self.ptr).hash(state);
1362            }
1363        }
1364    }
1365}
1366
1367/// Heap-allocated box for recursive `ComponentFieldValue` (e.g. `Some(value)`).
1368/// Uses raw pointer indirection to break the infinite size.
1369#[repr(C)]
1370pub struct ComponentFieldValueBox {
1371    pub ptr: *mut ComponentFieldValue,
1372}
1373
1374impl ComponentFieldValueBox {
1375    #[must_use] pub fn new(v: ComponentFieldValue) -> Self {
1376        Self {
1377            ptr: Box::into_raw(Box::new(v)),
1378        }
1379    }
1380
1381    #[must_use] pub fn as_ref(&self) -> &ComponentFieldValue {
1382        unsafe { &*self.ptr }
1383    }
1384}
1385
1386impl Clone for ComponentFieldValueBox {
1387    fn clone(&self) -> Self {
1388        Self::new(unsafe { (*self.ptr).clone() })
1389    }
1390}
1391
1392impl Drop for ComponentFieldValueBox {
1393    fn drop(&mut self) {
1394        // Take-and-null so a second drop (codegen FFI double-drop of a by-value
1395        // field, see `ComponentFieldTypeBox`) is a no-op, not a double free.
1396        let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
1397        if !ptr.is_null() {
1398            unsafe {
1399                drop(Box::from_raw(ptr));
1400            }
1401        }
1402    }
1403}
1404
1405impl fmt::Debug for ComponentFieldValueBox {
1406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1407        if self.ptr.is_null() {
1408            write!(f, "ComponentFieldValueBox(null)")
1409        } else {
1410            write!(f, "ComponentFieldValueBox({:?})", unsafe { &*self.ptr })
1411        }
1412    }
1413}
1414
1415impl PartialEq for ComponentFieldValueBox {
1416    fn eq(&self, other: &Self) -> bool {
1417        if self.ptr.is_null() && other.ptr.is_null() {
1418            return true;
1419        }
1420        if self.ptr.is_null() || other.ptr.is_null() {
1421            return false;
1422        }
1423        unsafe { *self.ptr == *other.ptr }
1424    }
1425}
1426
1427/// Rich type descriptor for a component field.
1428/// Replaces the old `AzString` type names ("String", "bool", etc.) with
1429/// a structured enum that the debugger can use for type-aware editing.
1430#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1431#[repr(C, u8)]
1432pub enum ComponentFieldType {
1433    String,
1434    Bool,
1435    I32,
1436    I64,
1437    U32,
1438    U64,
1439    Usize,
1440    F32,
1441    F64,
1442    ColorU,
1443    CssProperty,
1444    ImageRef,
1445    FontRef,
1446    /// `StyledDom` slot — field name = slot name
1447    StyledDom,
1448    /// Callback with typed signature
1449    Callback(ComponentCallbackSignature),
1450    /// `RefAny` data binding with type hint
1451    RefAny(AzString),
1452    /// Optional value (recursive via Box)
1453    OptionType(ComponentFieldTypeBox),
1454    /// Vec of values (recursive via Box)
1455    VecType(ComponentFieldTypeBox),
1456    /// Reference to a struct defined in the same library
1457    StructRef(AzString),
1458    /// Reference to an enum defined in the same library
1459    EnumRef(AzString),
1460}
1461
1462impl ComponentFieldType {
1463    /// Parse a field type string like "String", "Option<Bool>", "Vec<I32>",
1464    /// "Callback(fn(LayoutCallbackInfo) -> Dom)", "StructRef(MyStruct)" etc.
1465    /// Returns `None` if the string cannot be parsed.
1466    #[must_use] pub fn parse(s: &str) -> Option<Self> {
1467        Self::parse_depth(s, 0)
1468    }
1469
1470    /// Depth-bounded implementation of [`parse`](Self::parse).
1471    ///
1472    /// AUDIT 2026-07-08: `Option<..>` / `Vec<..>` wrappers recurse once per level,
1473    /// so an attacker string like `"Option<".repeat(100_000)` (with matching `>`)
1474    /// overflowed the stack. Recursion is capped at [`MAX_TYPE_PARSE_DEPTH`];
1475    /// beyond it, parsing fails (`None`) instead of crashing.
1476    fn parse_depth(s: &str, depth: usize) -> Option<Self> {
1477        if depth > MAX_TYPE_PARSE_DEPTH {
1478            return None;
1479        }
1480        let s = s.trim();
1481        match s {
1482            "String" | "string" => return Some(Self::String),
1483            "Bool" | "bool" => return Some(Self::Bool),
1484            "I32" | "i32" => return Some(Self::I32),
1485            "I64" | "i64" => return Some(Self::I64),
1486            "U32" | "u32" => return Some(Self::U32),
1487            "U64" | "u64" => return Some(Self::U64),
1488            "Usize" | "usize" => return Some(Self::Usize),
1489            "F32" | "f32" => return Some(Self::F32),
1490            "F64" | "f64" => return Some(Self::F64),
1491            "ColorU" => return Some(Self::ColorU),
1492            "CssProperty" => return Some(Self::CssProperty),
1493            "ImageRef" => return Some(Self::ImageRef),
1494            "FontRef" => return Some(Self::FontRef),
1495            "StyledDom" => return Some(Self::StyledDom),
1496            "RefAny" => return Some(Self::RefAny(AzString::from(""))),
1497            _ => {}
1498        }
1499
1500        // Option<T>
1501        if let Some(inner) = s.strip_prefix("Option<").and_then(|r| r.strip_suffix('>')) {
1502            let inner_type = Self::parse_depth(inner, depth + 1)?;
1503            return Some(Self::OptionType(ComponentFieldTypeBox::new(
1504                inner_type,
1505            )));
1506        }
1507
1508        // Vec<T>
1509        if let Some(inner) = s.strip_prefix("Vec<").and_then(|r| r.strip_suffix('>')) {
1510            let inner_type = Self::parse_depth(inner, depth + 1)?;
1511            return Some(Self::VecType(ComponentFieldTypeBox::new(
1512                inner_type,
1513            )));
1514        }
1515
1516        // Callback(signature)
1517        if let Some(sig) = s
1518            .strip_prefix("Callback(")
1519            .and_then(|r| r.strip_suffix(')'))
1520        {
1521            return Some(Self::Callback(ComponentCallbackSignature {
1522                return_type: AzString::from(sig),
1523                args: Vec::new().into(),
1524            }));
1525        }
1526
1527        // RefAny(TypeHint)
1528        if let Some(hint) = s.strip_prefix("RefAny(").and_then(|r| r.strip_suffix(')')) {
1529            return Some(Self::RefAny(AzString::from(hint)));
1530        }
1531
1532        // EnumRef(Name) — explicit
1533        if let Some(name) = s.strip_prefix("EnumRef(").and_then(|r| r.strip_suffix(')')) {
1534            return Some(Self::EnumRef(AzString::from(name)));
1535        }
1536
1537        // StructRef(Name) — explicit
1538        if let Some(name) = s
1539            .strip_prefix("StructRef(")
1540            .and_then(|r| r.strip_suffix(')'))
1541        {
1542            return Some(Self::StructRef(AzString::from(name)));
1543        }
1544
1545        // If starts with uppercase, treat as StructRef
1546        if s.chars().next().is_some_and(char::is_uppercase) {
1547            return Some(Self::StructRef(AzString::from(s)));
1548        }
1549
1550        None
1551    }
1552
1553    /// Format this field type to its canonical string representation.
1554    /// This is the inverse of `parse`.
1555    #[must_use] pub fn format(&self) -> String {
1556        match self {
1557            Self::String => "String".to_string(),
1558            Self::Bool => "Bool".to_string(),
1559            Self::I32 => "I32".to_string(),
1560            Self::I64 => "I64".to_string(),
1561            Self::U32 => "U32".to_string(),
1562            Self::U64 => "U64".to_string(),
1563            Self::Usize => "Usize".to_string(),
1564            Self::F32 => "F32".to_string(),
1565            Self::F64 => "F64".to_string(),
1566            Self::ColorU => "ColorU".to_string(),
1567            Self::CssProperty => "CssProperty".to_string(),
1568            Self::ImageRef => "ImageRef".to_string(),
1569            Self::FontRef => "FontRef".to_string(),
1570            Self::StyledDom => "StyledDom".to_string(),
1571            Self::Callback(sig) => format!("Callback({})", sig.return_type.as_str()),
1572            Self::RefAny(hint) => {
1573                if hint.as_str().is_empty() {
1574                    "RefAny".to_string()
1575                } else {
1576                    format!("RefAny({})", hint.as_str())
1577                }
1578            }
1579            Self::OptionType(inner) => format!("Option<{}>", inner.as_ref().format()),
1580            Self::VecType(inner) => format!("Vec<{}>", inner.as_ref().format()),
1581            Self::StructRef(name) | Self::EnumRef(name) => name.as_str().to_string(),
1582        }
1583    }
1584}
1585
1586impl fmt::Display for ComponentFieldType {
1587    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1588        f.write_str(&self.format())
1589    }
1590}
1591
1592/// A single variant in a component enum model.
1593#[derive(Debug, Clone, PartialEq)]
1594#[repr(C)]
1595pub struct ComponentEnumVariant {
1596    /// Variant name, e.g. "Admin", "Editor", "Viewer"
1597    pub name: AzString,
1598    /// Human-readable description for this variant
1599    pub description: AzString,
1600    /// Optional associated fields for this variant
1601    pub fields: ComponentDataFieldVec,
1602}
1603
1604impl_vec!(
1605    ComponentEnumVariant,
1606    ComponentEnumVariantVec,
1607    ComponentEnumVariantVecDestructor,
1608    ComponentEnumVariantVecDestructorType,
1609    ComponentEnumVariantVecSlice,
1610    OptionComponentEnumVariant
1611);
1612impl_option!(
1613    ComponentEnumVariant,
1614    OptionComponentEnumVariant,
1615    copy = false,
1616    [Debug, Clone, PartialEq]
1617);
1618impl_vec_debug!(ComponentEnumVariant, ComponentEnumVariantVec);
1619impl_vec_partialeq!(ComponentEnumVariant, ComponentEnumVariantVec);
1620impl_vec_clone!(
1621    ComponentEnumVariant,
1622    ComponentEnumVariantVec,
1623    ComponentEnumVariantVecDestructor
1624);
1625
1626/// A named enum model for code generation.
1627/// Stored in `ComponentLibrary::enum_models`.
1628#[derive(Debug, Clone, PartialEq)]
1629#[repr(C)]
1630pub struct ComponentEnumModel {
1631    /// Enum name, e.g. "`UserRole`"
1632    pub name: AzString,
1633    /// Human-readable description
1634    pub description: AzString,
1635    /// Variants
1636    pub variants: ComponentEnumVariantVec,
1637}
1638
1639impl_vec!(
1640    ComponentEnumModel,
1641    ComponentEnumModelVec,
1642    ComponentEnumModelVecDestructor,
1643    ComponentEnumModelVecDestructorType,
1644    ComponentEnumModelVecSlice,
1645    OptionComponentEnumModel
1646);
1647impl_option!(
1648    ComponentEnumModel,
1649    OptionComponentEnumModel,
1650    copy = false,
1651    [Debug, Clone, PartialEq]
1652);
1653impl_vec_debug!(ComponentEnumModel, ComponentEnumModelVec);
1654impl_vec_partialeq!(ComponentEnumModel, ComponentEnumModelVec);
1655impl_vec_clone!(
1656    ComponentEnumModel,
1657    ComponentEnumModelVec,
1658    ComponentEnumModelVecDestructor
1659);
1660
1661/// Default value for a component field.
1662#[derive(Debug, Clone, PartialEq)]
1663#[repr(C, u8)]
1664pub enum ComponentDefaultValue {
1665    /// No default value (field is required)
1666    None,
1667    /// String literal default
1668    String(AzString),
1669    /// Boolean default
1670    Bool(bool),
1671    /// i32 default
1672    I32(i32),
1673    /// i64 default
1674    I64(i64),
1675    /// u32 default
1676    U32(u32),
1677    /// u64 default
1678    U64(u64),
1679    /// usize default
1680    Usize(usize),
1681    /// f32 default
1682    F32(f32),
1683    /// f64 default
1684    F64(f64),
1685    /// `ColorU` default
1686    ColorU(ColorU),
1687    /// Default is an instance of another component
1688    ComponentInstance(ComponentInstanceDefault),
1689    /// Default callback function pointer name
1690    CallbackFnPointer(AzString),
1691    /// JSON string representing a complex default value
1692    Json(AzString),
1693}
1694
1695impl_option!(
1696    ComponentDefaultValue,
1697    OptionComponentDefaultValue,
1698    copy = false,
1699    [Debug, Clone, PartialEq]
1700);
1701
1702/// Default component instance for a `StyledDom` slot.
1703#[derive(Debug, Clone, PartialEq)]
1704#[repr(C)]
1705pub struct ComponentInstanceDefault {
1706    /// Library name, e.g. "builtin"
1707    pub library: AzString,
1708    /// Component tag, e.g. "a"
1709    pub component: AzString,
1710    /// Field overrides for this instance
1711    pub field_overrides: ComponentFieldOverrideVec,
1712}
1713
1714/// An override for a single field in a component instance.
1715#[derive(Debug, Clone, PartialEq, Eq)]
1716#[repr(C)]
1717pub struct ComponentFieldOverride {
1718    /// Field name to override
1719    pub field_name: AzString,
1720    /// Value source for this override
1721    pub source: ComponentFieldValueSource,
1722}
1723
1724impl_vec!(
1725    ComponentFieldOverride,
1726    ComponentFieldOverrideVec,
1727    ComponentFieldOverrideVecDestructor,
1728    ComponentFieldOverrideVecDestructorType,
1729    ComponentFieldOverrideVecSlice,
1730    OptionComponentFieldOverride
1731);
1732impl_option!(
1733    ComponentFieldOverride,
1734    OptionComponentFieldOverride,
1735    copy = false,
1736    [Debug, Clone, PartialEq, Eq]
1737);
1738impl_vec_debug!(ComponentFieldOverride, ComponentFieldOverrideVec);
1739impl_vec_partialeq!(ComponentFieldOverride, ComponentFieldOverrideVec);
1740impl_vec_clone!(
1741    ComponentFieldOverride,
1742    ComponentFieldOverrideVec,
1743    ComponentFieldOverrideVecDestructor
1744);
1745
1746/// How a field value is sourced at the instance level.
1747#[derive(Debug, Clone, PartialEq, Eq)]
1748#[repr(C, u8)]
1749pub enum ComponentFieldValueSource {
1750    /// Use the component's default value
1751    Default,
1752    /// Hardcoded literal value (as string, parsed at runtime)
1753    Literal(AzString),
1754    /// Bound to an app state path (e.g. "`app_state.user.name`")
1755    Binding(AzString),
1756}
1757#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
1758/// Runtime value for a component field — the "instance" counterpart
1759/// to `ComponentFieldType` (which is the "class" / type descriptor).
1760#[derive(Debug, Clone, PartialEq)]
1761#[repr(C, u8)]
1762#[allow(clippy::large_enum_variant)] // #[repr(C,u8)] FFI enum: boxing a variant changes the C ABI/api.json
1763pub enum ComponentFieldValue {
1764    String(AzString),
1765    Bool(bool),
1766    I32(i32),
1767    I64(i64),
1768    U32(u32),
1769    U64(u64),
1770    Usize(usize),
1771    F32(f32),
1772    F64(f64),
1773    ColorU(ColorU),
1774    /// Option<T> with no value
1775    None,
1776    /// Option<T> with a value
1777    Some(ComponentFieldValueBox),
1778    /// Vec of values
1779    Vec(ComponentFieldValueVec),
1780    /// `StyledDom` slot content
1781    StyledDom(StyledDom),
1782    /// Struct fields, in order
1783    Struct(ComponentFieldNamedValueVec),
1784    /// Enum variant
1785    Enum {
1786        variant: AzString,
1787        fields: ComponentFieldNamedValueVec,
1788    },
1789    /// Callback function reference (function name as string)
1790    Callback(AzString),
1791    /// Opaque reference-counted data
1792    RefAny(crate::refany::RefAny),
1793}
1794
1795/// Named field value: (`field_name`, value) pair.
1796#[derive(Debug, Clone, PartialEq)]
1797#[repr(C)]
1798pub struct ComponentFieldNamedValue {
1799    pub name: AzString,
1800    pub value: ComponentFieldValue,
1801}
1802
1803impl_vec!(
1804    ComponentFieldNamedValue,
1805    ComponentFieldNamedValueVec,
1806    ComponentFieldNamedValueVecDestructor,
1807    ComponentFieldNamedValueVecDestructorType,
1808    ComponentFieldNamedValueVecSlice,
1809    OptionComponentFieldNamedValue
1810);
1811impl_option!(
1812    ComponentFieldNamedValue,
1813    OptionComponentFieldNamedValue,
1814    copy = false,
1815    [Debug, Clone, PartialEq]
1816);
1817impl_vec_debug!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1818impl_vec_partialeq!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1819impl_vec_clone!(
1820    ComponentFieldNamedValue,
1821    ComponentFieldNamedValueVec,
1822    ComponentFieldNamedValueVecDestructor
1823);
1824
1825impl ComponentFieldNamedValueVec {
1826    /// Look up a field by name, return a reference to its value.
1827    #[must_use] pub fn get_field(&self, name: &str) -> Option<&ComponentFieldValue> {
1828        self.as_ref().iter().find_map(|v| {
1829            if v.name.as_str() == name {
1830                Some(&v.value)
1831            } else {
1832                None
1833            }
1834        })
1835    }
1836
1837    /// Convenience: get a field as `&str` if it is `ComponentFieldValue::String`.
1838    #[must_use] pub fn get_string(&self, name: &str) -> Option<&AzString> {
1839        match self.get_field(name) {
1840            Some(ComponentFieldValue::String(s)) => Some(s),
1841            _ => None,
1842        }
1843    }
1844}
1845
1846impl_vec!(
1847    ComponentFieldValue,
1848    ComponentFieldValueVec,
1849    ComponentFieldValueVecDestructor,
1850    ComponentFieldValueVecDestructorType,
1851    ComponentFieldValueVecSlice,
1852    OptionComponentFieldValue
1853);
1854impl_option!(
1855    ComponentFieldValue,
1856    OptionComponentFieldValue,
1857    copy = false,
1858    [Debug, Clone, PartialEq]
1859);
1860impl_vec_debug!(ComponentFieldValue, ComponentFieldValueVec);
1861impl_vec_partialeq!(ComponentFieldValue, ComponentFieldValueVec);
1862impl_vec_clone!(
1863    ComponentFieldValue,
1864    ComponentFieldValueVec,
1865    ComponentFieldValueVecDestructor
1866);
1867
1868/// A field in the component's internal data model.
1869#[derive(Debug, Clone, PartialEq)]
1870#[repr(C)]
1871pub struct ComponentDataField {
1872    /// Field name, e.g. "counter", "text", "number"
1873    pub name: AzString,
1874    /// Rich type descriptor for this field
1875    pub field_type: ComponentFieldType,
1876    /// Typed default value, or None if the field is required
1877    pub default_value: OptionComponentDefaultValue,
1878    /// Whether this field is required (must be provided by the parent)
1879    pub required: bool,
1880    /// Human-readable description
1881    pub description: AzString,
1882}
1883
1884impl_vec!(
1885    ComponentDataField,
1886    ComponentDataFieldVec,
1887    ComponentDataFieldVecDestructor,
1888    ComponentDataFieldVecDestructorType,
1889    ComponentDataFieldVecSlice,
1890    OptionComponentDataField
1891);
1892impl_option!(
1893    ComponentDataField,
1894    OptionComponentDataField,
1895    copy = false,
1896    [Debug, Clone, PartialEq]
1897);
1898impl_vec_debug!(ComponentDataField, ComponentDataFieldVec);
1899impl_vec_partialeq!(ComponentDataField, ComponentDataFieldVec);
1900impl_vec_clone!(
1901    ComponentDataField,
1902    ComponentDataFieldVec,
1903    ComponentDataFieldVecDestructor
1904);
1905
1906/// A named data model (struct definition) for code generation.
1907///
1908/// Stored in `ComponentLibrary::data_models`. Components reference these
1909/// by name in `ComponentDataField::field_type`, enabling nested/structured
1910/// data models. For example, a `UserCard` component might have a field
1911/// `user: UserProfile` where `UserProfile` is a `ComponentDataModel`.
1912#[derive(Debug, Clone)]
1913#[repr(C)]
1914pub struct ComponentDataModel {
1915    /// Type name, e.g. "`UserProfile`", "`TodoItem`"
1916    pub name: AzString,
1917    /// Human-readable description
1918    pub description: AzString,
1919    /// Fields in this struct
1920    pub fields: ComponentDataFieldVec,
1921}
1922
1923impl ComponentDataModel {
1924    /// Look up a field by name.
1925    #[must_use] pub fn get_field(&self, name: &str) -> Option<&ComponentDataField> {
1926        self.fields
1927            .as_ref()
1928            .iter()
1929            .find(|f| f.name.as_str() == name)
1930    }
1931
1932    /// Look up a field's default value as a string, if it exists and is a String variant.
1933    #[must_use] pub fn get_default_string(&self, name: &str) -> Option<&AzString> {
1934        self.get_field(name).and_then(|f| match &f.default_value {
1935            OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s),
1936            _ => None,
1937        })
1938    }
1939
1940    /// Clone this data model, overriding the default value for a field by name.
1941    /// If the field is not found, the data model is returned unchanged.
1942    #[must_use] pub fn with_default(mut self, name: &str, value: ComponentDefaultValue) -> Self {
1943        let mut fields_vec = core::mem::replace(
1944            &mut self.fields,
1945            ComponentDataFieldVec::from_const_slice(&[]),
1946        )
1947        .into_library_owned_vec();
1948        for f in &mut fields_vec {
1949            if f.name.as_str() == name {
1950                f.default_value = OptionComponentDefaultValue::Some(value);
1951                break;
1952            }
1953        }
1954        self.fields = ComponentDataFieldVec::from_vec(fields_vec);
1955        self
1956    }
1957}
1958
1959impl_vec!(
1960    ComponentDataModel,
1961    ComponentDataModelVec,
1962    ComponentDataModelVecDestructor,
1963    ComponentDataModelVecDestructorType,
1964    ComponentDataModelVecSlice,
1965    OptionComponentDataModel
1966);
1967impl_option!(
1968    ComponentDataModel,
1969    OptionComponentDataModel,
1970    copy = false,
1971    [Debug, Clone]
1972);
1973impl_vec_debug!(ComponentDataModel, ComponentDataModelVec);
1974impl_vec_clone!(
1975    ComponentDataModel,
1976    ComponentDataModelVec,
1977    ComponentDataModelVecDestructor
1978);
1979impl_vec_mut!(ComponentDataModel, ComponentDataModelVec);
1980
1981// ============================================================================
1982// Serde support for ComponentDataModel (feature-gated)
1983// ============================================================================
1984
1985#[cfg(feature = "serde-json")]
1986mod serde_impl {
1987    use super::*;
1988    use serde::ser::SerializeStruct;
1989    use serde::{Deserialize, Deserializer, Serialize, Serializer};
1990
1991    // --- AzString helpers ---
1992
1993    fn ser_azstring<S: Serializer>(s: &AzString, serializer: S) -> Result<S::Ok, S::Error> {
1994        serializer.serialize_str(s.as_str())
1995    }
1996
1997    fn de_azstring<'de, D: Deserializer<'de>>(deserializer: D) -> Result<AzString, D::Error> {
1998        let s = alloc::string::String::deserialize(deserializer)?;
1999        Ok(AzString::from(s.as_str()))
2000    }
2001
2002    // --- ComponentFieldType ---
2003
2004    impl Serialize for ComponentFieldType {
2005        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2006            serializer.serialize_str(&field_type_to_string(self))
2007        }
2008    }
2009
2010    impl<'de> Deserialize<'de> for ComponentFieldType {
2011        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2012            let s = alloc::string::String::deserialize(deserializer)?;
2013            Ok(string_to_field_type(&s))
2014        }
2015    }
2016
2017    fn field_type_to_string(ft: &ComponentFieldType) -> alloc::string::String {
2018        match ft {
2019            ComponentFieldType::String => "String".into(),
2020            ComponentFieldType::Bool => "bool".into(),
2021            ComponentFieldType::I32 => "i32".into(),
2022            ComponentFieldType::I64 => "i64".into(),
2023            ComponentFieldType::U32 => "u32".into(),
2024            ComponentFieldType::U64 => "u64".into(),
2025            ComponentFieldType::Usize => "usize".into(),
2026            ComponentFieldType::F32 => "f32".into(),
2027            ComponentFieldType::F64 => "f64".into(),
2028            ComponentFieldType::ColorU => "ColorU".into(),
2029            ComponentFieldType::CssProperty => "CssProperty".into(),
2030            ComponentFieldType::ImageRef => "ImageRef".into(),
2031            ComponentFieldType::FontRef => "FontRef".into(),
2032            ComponentFieldType::StyledDom => "Dom".into(),
2033            ComponentFieldType::Callback(sig) => {
2034                alloc::format!("Callback({})", sig.return_type.as_str())
2035            }
2036            ComponentFieldType::RefAny(hint) => alloc::format!("RefAny({})", hint.as_str()),
2037            ComponentFieldType::OptionType(inner) => {
2038                alloc::format!("Option<{}>", field_type_to_string(inner.as_ref()))
2039            }
2040            ComponentFieldType::VecType(inner) => {
2041                alloc::format!("Vec<{}>", field_type_to_string(inner.as_ref()))
2042            }
2043            ComponentFieldType::StructRef(name) => alloc::format!("struct:{}", name.as_str()),
2044            ComponentFieldType::EnumRef(name) => alloc::format!("enum:{}", name.as_str()),
2045        }
2046    }
2047
2048    fn string_to_field_type(s: &str) -> ComponentFieldType {
2049        match s {
2050            "String" | "string" => ComponentFieldType::String,
2051            "bool" | "Bool" => ComponentFieldType::Bool,
2052            "i32" | "I32" => ComponentFieldType::I32,
2053            "i64" | "I64" => ComponentFieldType::I64,
2054            "u32" | "U32" => ComponentFieldType::U32,
2055            "u64" | "U64" => ComponentFieldType::U64,
2056            "usize" | "Usize" => ComponentFieldType::Usize,
2057            "f32" | "F32" => ComponentFieldType::F32,
2058            "f64" | "F64" => ComponentFieldType::F64,
2059            "ColorU" | "Color" | "color" => ComponentFieldType::ColorU,
2060            "CssProperty" => ComponentFieldType::CssProperty,
2061            "ImageRef" | "Image" => ComponentFieldType::ImageRef,
2062            "FontRef" | "Font" => ComponentFieldType::FontRef,
2063            "Dom" | "StyledDom" | "Children" => ComponentFieldType::StyledDom,
2064            other => {
2065                if let Some(inner) = other
2066                    .strip_prefix("Option<")
2067                    .and_then(|s| s.strip_suffix('>'))
2068                {
2069                    ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
2070                        string_to_field_type(inner),
2071                    ))
2072                } else if let Some(inner) =
2073                    other.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>'))
2074                {
2075                    ComponentFieldType::VecType(ComponentFieldTypeBox::new(string_to_field_type(
2076                        inner,
2077                    )))
2078                } else if let Some(name) = other.strip_prefix("struct:") {
2079                    ComponentFieldType::StructRef(AzString::from(name))
2080                } else if let Some(name) = other.strip_prefix("enum:") {
2081                    ComponentFieldType::EnumRef(AzString::from(name))
2082                } else if other.starts_with("Callback") {
2083                    let ret = other
2084                        .strip_prefix("Callback(")
2085                        .and_then(|s| s.strip_suffix(')'))
2086                        .unwrap_or("()");
2087                    ComponentFieldType::Callback(ComponentCallbackSignature {
2088                        return_type: AzString::from(ret),
2089                        args: ComponentCallbackArgVec::from_const_slice(&[]),
2090                    })
2091                } else if other.starts_with("RefAny") {
2092                    let hint = other
2093                        .strip_prefix("RefAny(")
2094                        .and_then(|s| s.strip_suffix(')'))
2095                        .unwrap_or("");
2096                    ComponentFieldType::RefAny(AzString::from(hint))
2097                } else {
2098                    ComponentFieldType::String // fallback
2099                }
2100            }
2101        }
2102    }
2103
2104    // --- ComponentDefaultValue ---
2105
2106    impl Serialize for ComponentDefaultValue {
2107        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2108            use serde::ser::SerializeMap;
2109            match self {
2110                ComponentDefaultValue::None => serializer.serialize_none(),
2111                ComponentDefaultValue::String(s) => serializer.serialize_str(s.as_str()),
2112                ComponentDefaultValue::Bool(b) => serializer.serialize_bool(*b),
2113                ComponentDefaultValue::I32(v) => serializer.serialize_i32(*v),
2114                ComponentDefaultValue::I64(v) => serializer.serialize_i64(*v),
2115                ComponentDefaultValue::U32(v) => serializer.serialize_u32(*v),
2116                ComponentDefaultValue::U64(v) => serializer.serialize_u64(*v),
2117                ComponentDefaultValue::Usize(v) => serializer.serialize_u64(*v as u64),
2118                ComponentDefaultValue::F32(v) => serializer.serialize_f32(*v),
2119                ComponentDefaultValue::F64(v) => serializer.serialize_f64(*v),
2120                ComponentDefaultValue::ColorU(c) => serializer.serialize_str(&alloc::format!(
2121                    "#{:02x}{:02x}{:02x}{:02x}",
2122                    c.r,
2123                    c.g,
2124                    c.b,
2125                    c.a
2126                )),
2127                ComponentDefaultValue::ComponentInstance(ci) => {
2128                    let mut map = serializer.serialize_map(Some(2))?;
2129                    map.serialize_entry("library", ci.library.as_str())?;
2130                    map.serialize_entry("component", ci.component.as_str())?;
2131                    map.end()
2132                }
2133                ComponentDefaultValue::CallbackFnPointer(name) => {
2134                    serializer.serialize_str(name.as_str())
2135                }
2136                ComponentDefaultValue::Json(json_str) => {
2137                    // Serialize raw JSON string as-is by parsing and re-emitting
2138                    match serde_json::from_str::<serde_json::Value>(json_str.as_str()) {
2139                        Ok(v) => v.serialize(serializer),
2140                        Err(_) => serializer.serialize_str(json_str.as_str()),
2141                    }
2142                }
2143            }
2144        }
2145    }
2146
2147    impl<'de> Deserialize<'de> for ComponentDefaultValue {
2148        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2149            let val = serde_json::Value::deserialize(deserializer)?;
2150            Ok(match val {
2151                serde_json::Value::Null => ComponentDefaultValue::None,
2152                serde_json::Value::Bool(b) => ComponentDefaultValue::Bool(b),
2153                serde_json::Value::Number(n) => {
2154                    if let Some(i) = n.as_i64() {
2155                        if let Ok(v) = i32::try_from(i) {
2156                            ComponentDefaultValue::I32(v)
2157                        } else {
2158                            ComponentDefaultValue::I64(i)
2159                        }
2160                    } else if let Some(f) = n.as_f64() {
2161                        ComponentDefaultValue::F64(f)
2162                    } else {
2163                        ComponentDefaultValue::None
2164                    }
2165                }
2166                serde_json::Value::String(s) => {
2167                    ComponentDefaultValue::String(AzString::from(s.as_str()))
2168                }
2169                _ => ComponentDefaultValue::None,
2170            })
2171        }
2172    }
2173
2174    // --- OptionComponentDefaultValue ---
2175
2176    impl Serialize for OptionComponentDefaultValue {
2177        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2178            match self {
2179                OptionComponentDefaultValue::Some(v) => v.serialize(serializer),
2180                OptionComponentDefaultValue::None => serializer.serialize_none(),
2181            }
2182        }
2183    }
2184
2185    impl<'de> Deserialize<'de> for OptionComponentDefaultValue {
2186        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2187            let val = Option::<ComponentDefaultValue>::deserialize(deserializer)?;
2188            Ok(match val {
2189                Some(v) => OptionComponentDefaultValue::Some(v),
2190                None => OptionComponentDefaultValue::None,
2191            })
2192        }
2193    }
2194
2195    // --- ComponentDataField ---
2196
2197    impl Serialize for ComponentDataField {
2198        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2199            let mut s = serializer.serialize_struct("ComponentDataField", 5)?;
2200            s.serialize_field("name", self.name.as_str())?;
2201            s.serialize_field("type", &self.field_type)?;
2202            s.serialize_field("default", &self.default_value)?;
2203            s.serialize_field("required", &self.required)?;
2204            s.serialize_field("description", self.description.as_str())?;
2205            s.end()
2206        }
2207    }
2208
2209    impl<'de> Deserialize<'de> for ComponentDataField {
2210        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2211            #[derive(Deserialize)]
2212            struct Helper {
2213                name: alloc::string::String,
2214                #[serde(rename = "type", default = "default_type")]
2215                field_type: ComponentFieldType,
2216                #[serde(default)]
2217                default: OptionComponentDefaultValue,
2218                #[serde(default)]
2219                required: bool,
2220                #[serde(default)]
2221                description: alloc::string::String,
2222            }
2223            fn default_type() -> ComponentFieldType {
2224                ComponentFieldType::String
2225            }
2226
2227            let h = Helper::deserialize(deserializer)?;
2228            Ok(ComponentDataField {
2229                name: AzString::from(h.name.as_str()),
2230                field_type: h.field_type,
2231                default_value: h.default,
2232                required: h.required,
2233                description: AzString::from(h.description.as_str()),
2234            })
2235        }
2236    }
2237
2238    // --- ComponentDataModel ---
2239
2240    impl Serialize for ComponentDataModel {
2241        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2242            let mut s = serializer.serialize_struct("ComponentDataModel", 3)?;
2243            s.serialize_field("name", self.name.as_str())?;
2244            s.serialize_field("description", self.description.as_str())?;
2245            let fields: alloc::vec::Vec<&ComponentDataField> =
2246                self.fields.as_ref().iter().collect();
2247            s.serialize_field("fields", &fields)?;
2248            s.end()
2249        }
2250    }
2251
2252    impl<'de> Deserialize<'de> for ComponentDataModel {
2253        /// A data model is a JSON **object**. This deliberately drives the
2254        /// deserializer with `deserialize_map` instead of `deserialize_struct`:
2255        /// the struct hint makes serde accept a *sequence* as well (the
2256        /// positional encoding used by compact formats), so `from_json("[]")`
2257        /// used to succeed and hand back a nameless, field-less model instead of
2258        /// reporting that the input is not a data model at all. Every key stays
2259        /// optional, so `{}` still deserializes to the empty model.
2260        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2261            use serde::de::{IgnoredAny, MapAccess, Visitor};
2262
2263            struct ModelVisitor;
2264
2265            impl<'de> Visitor<'de> for ModelVisitor {
2266                type Value = ComponentDataModel;
2267
2268                fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
2269                    f.write_str("a data model object with `name`, `description` and `fields`")
2270                }
2271
2272                fn visit_map<A: MapAccess<'de>>(
2273                    self,
2274                    mut map: A,
2275                ) -> Result<Self::Value, A::Error> {
2276                    let mut name: Option<alloc::string::String> = None;
2277                    let mut description: Option<alloc::string::String> = None;
2278                    let mut fields: Option<alloc::vec::Vec<ComponentDataField>> = None;
2279
2280                    while let Some(key) = map.next_key::<alloc::string::String>()? {
2281                        match key.as_str() {
2282                            "name" => name = Some(map.next_value()?),
2283                            "description" => description = Some(map.next_value()?),
2284                            "fields" => fields = Some(map.next_value()?),
2285                            // Unknown keys are ignored (forward compatibility),
2286                            // but their values must still be consumed.
2287                            _ => {
2288                                map.next_value::<IgnoredAny>()?;
2289                            }
2290                        }
2291                    }
2292
2293                    Ok(ComponentDataModel {
2294                        name: AzString::from(name.unwrap_or_default().as_str()),
2295                        description: AzString::from(description.unwrap_or_default().as_str()),
2296                        fields: ComponentDataFieldVec::from_vec(fields.unwrap_or_default()),
2297                    })
2298                }
2299            }
2300
2301            deserializer.deserialize_map(ModelVisitor)
2302        }
2303    }
2304}
2305
2306// Re-export serde impls so they're visible when the feature is enabled
2307#[cfg(feature = "serde-json")]
2308pub use serde_impl::*;
2309
2310#[cfg(feature = "serde-json")]
2311impl ComponentDataModel {
2312    /// Serialize this data model to a JSON string.
2313    pub fn to_json(&self) -> Result<alloc::string::String, alloc::string::String> {
2314        serde_json::to_string_pretty(self).map_err(|e| alloc::format!("{}", e))
2315    }
2316
2317    /// Deserialize a data model from a JSON string.
2318    pub fn from_json(json: &str) -> Result<Self, alloc::string::String> {
2319        serde_json::from_str(json).map_err(|e| alloc::format!("{}", e))
2320    }
2321}
2322
2323/// Source of a component definition — determines whether it can be exported
2324#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2325#[repr(C)]
2326#[derive(Default)]
2327pub enum ComponentSource {
2328    /// Built into the DLL (HTML elements). Never exported.
2329    Builtin,
2330    /// Compiled Rust widget (Button, `TextInput`, etc.). Never exported.
2331    Compiled,
2332    /// Defined via JSON/XML at runtime. Can be exported.
2333    #[default]
2334    UserDefined,
2335}
2336
2337
2338impl ComponentSource {
2339    #[must_use] pub fn create() -> Self {
2340        Self::default()
2341    }
2342}
2343
2344/// The target language for code compilation
2345// Threaded by reference through the codegen call graph; kept non-Copy so
2346// deriving Copy doesn't force trivially_copy_pass_by_ref churn across the many
2347// &CompileTarget codegen callers for a perf-neutral change.
2348#[allow(missing_copy_implementations)]
2349#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2350#[repr(C)]
2351pub enum CompileTarget {
2352    Rust,
2353    C,
2354    Cpp,
2355    Python,
2356}
2357
2358impl_result!(
2359    StyledDom,
2360    RenderDomError,
2361    ResultStyledDomRenderDomError,
2362    copy = false,
2363    [Debug, Clone, PartialEq]
2364);
2365
2366impl_result!(
2367    AzString,
2368    CompileError,
2369    ResultStringCompileError,
2370    copy = false,
2371    [Debug, Clone, PartialEq]
2372);
2373
2374/// Render function type: takes component definition + data model (with current values
2375/// in `default_value` fields) + component map for recursive sub-component instantiation,
2376/// returns `StyledDom`.
2377///
2378/// The `data` parameter is typically `def.data_model` cloned and with caller-provided
2379/// values substituted into the `default_value` fields.
2380pub type ComponentRenderFn =
2381    fn(&ComponentDef, &ComponentDataModel, &ComponentMap) -> ResultStyledDomRenderDomError;
2382
2383/// Compile function type: takes component definition + target language + data model, returns source code.
2384pub type ComponentCompileFn = fn(
2385    &ComponentDef,
2386    &CompileTarget,
2387    &ComponentDataModel,
2388    indent: usize,
2389) -> ResultStringCompileError;
2390
2391/// Raw function pointer type that returns a single `ComponentDef` when called.
2392/// Used as the `cb` field in `RegisterComponentFn`.
2393pub type RegisterComponentFnType = extern "C" fn() -> ComponentDef;
2394
2395/// Callback struct for registering individual components at startup.
2396///
2397/// In C: pass a bare `extern "C" fn() -> ComponentDef` function pointer —
2398/// it converts automatically via `From<RegisterComponentFnType>`.
2399///
2400/// In Python: construct this struct with `cb` set to a trampoline and
2401/// `ctx` set to `Some(RefAny(...))` wrapping the Python callable.
2402#[repr(C)]
2403pub struct RegisterComponentFn {
2404    pub cb: RegisterComponentFnType,
2405    /// For FFI: stores the foreign callable (e.g., `PyFunction`).
2406    /// Native Rust/C code sets this to None.
2407    pub ctx: crate::refany::OptionRefAny,
2408}
2409
2410impl_callback!(RegisterComponentFn, RegisterComponentFnType);
2411
2412/// Raw function pointer type that returns a complete `ComponentLibrary` when called.
2413/// Used as the `cb` field in `RegisterComponentLibraryFn`.
2414pub type RegisterComponentLibraryFnType = extern "C" fn() -> ComponentLibrary;
2415
2416/// Callback struct for registering entire component libraries at startup.
2417///
2418/// In C: pass a bare `extern "C" fn() -> ComponentLibrary` function pointer —
2419/// it converts automatically via `From<RegisterComponentLibraryFnType>`.
2420///
2421/// In Python: construct this struct with `cb` set to a trampoline and
2422/// `ctx` set to `Some(RefAny(...))` wrapping the Python callable.
2423#[repr(C)]
2424pub struct RegisterComponentLibraryFn {
2425    pub cb: RegisterComponentLibraryFnType,
2426    /// For FFI: stores the foreign callable (e.g., `PyFunction`).
2427    /// Native Rust/C code sets this to None.
2428    pub ctx: crate::refany::OptionRefAny,
2429}
2430
2431impl_callback!(RegisterComponentLibraryFn, RegisterComponentLibraryFnType);
2432
2433/// A component definition — the "class" / "template" of a component.
2434/// Can come from Rust builtins, compiled widgets, JSON, or user creation in debugger.
2435///
2436#[derive(Clone)]
2437#[repr(C)]
2438pub struct ComponentDef {
2439    /// Collection + name, e.g. builtin:div, shadcn:avatar
2440    pub id: ComponentId,
2441    /// Human-readable display name, e.g. "Link" for builtin:a, "Avatar" for shadcn:avatar
2442    pub display_name: AzString,
2443    /// Markdown documentation for the component
2444    pub description: AzString,
2445    /// The component's CSS
2446    pub css: AzString,
2447    /// Where this component was defined (determines exportability)
2448    pub source: ComponentSource,
2449    /// Unified data model: all value fields, callback slots, and child slots
2450    /// in a single named struct. Code gen uses `data_model.name` as the
2451    /// input struct type name (e.g. "`ButtonData`").
2452    /// The `default_value` on each field doubles as the "current value" for
2453    /// preview rendering — callers override defaults before calling `render_fn`.
2454    pub data_model: ComponentDataModel,
2455    /// Render to live DOM
2456    pub render_fn: ComponentRenderFn,
2457    /// Compile to source code in target language
2458    pub compile_fn: ComponentCompileFn,
2459    /// Source code for `render_fn` (user-defined components only)
2460    pub render_fn_source: OptionString,
2461    /// Source code for `compile_fn` (user-defined components only)
2462    pub compile_fn_source: OptionString,
2463}
2464
2465impl fmt::Debug for ComponentDef {
2466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2467        f.debug_struct("ComponentDef")
2468            .field("id", &self.id)
2469            .field("display_name", &self.display_name)
2470            .field("source", &self.source)
2471            .field("data_model", &self.data_model.name)
2472            .finish_non_exhaustive()
2473    }
2474}
2475
2476impl_vec!(
2477    ComponentDef,
2478    ComponentDefVec,
2479    ComponentDefVecDestructor,
2480    ComponentDefVecDestructorType,
2481    ComponentDefVecSlice,
2482    OptionComponentDef
2483);
2484impl_option!(ComponentDef, OptionComponentDef, copy = false, [Clone]);
2485impl_vec_debug!(ComponentDef, ComponentDefVec);
2486impl_vec_clone!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor);
2487impl_vec_mut!(ComponentDef, ComponentDefVec);
2488
2489/// A named collection of component definitions
2490#[derive(Debug, Clone)]
2491#[repr(C)]
2492pub struct ComponentLibrary {
2493    /// Library identifier, e.g. "builtin", "shadcn", "myproject"
2494    pub name: AzString,
2495    /// Version string
2496    pub version: AzString,
2497    /// Human-readable description
2498    pub description: AzString,
2499    /// The components in this library
2500    pub components: ComponentDefVec,
2501    /// Whether this library can be exported (false for builtin/compiled)
2502    pub exportable: bool,
2503    /// Whether this library can be modified by the user (add/remove/edit components).
2504    /// False for builtin and compiled libraries. True for user-created libraries.
2505    pub modifiable: bool,
2506    /// Named data model types defined by this library.
2507    /// Components reference these by name in their `field_type`.
2508    pub data_models: ComponentDataModelVec,
2509    /// Named enum types defined by this library.
2510    /// Components reference these via `ComponentFieldType::EnumRef(name)`.
2511    pub enum_models: ComponentEnumModelVec,
2512}
2513
2514impl_vec!(
2515    ComponentLibrary,
2516    ComponentLibraryVec,
2517    ComponentLibraryVecDestructor,
2518    ComponentLibraryVecDestructorType,
2519    ComponentLibraryVecSlice,
2520    OptionComponentLibrary
2521);
2522impl_option!(
2523    ComponentLibrary,
2524    OptionComponentLibrary,
2525    copy = false,
2526    [Debug, Clone]
2527);
2528impl_vec_debug!(ComponentLibrary, ComponentLibraryVec);
2529impl_vec_clone!(
2530    ComponentLibrary,
2531    ComponentLibraryVec,
2532    ComponentLibraryVecDestructor
2533);
2534impl_vec_mut!(ComponentLibrary, ComponentLibraryVec);
2535
2536/// The component map — holds libraries with namespaced components.
2537#[derive(Debug, Clone)]
2538#[repr(C)]
2539pub struct ComponentMap {
2540    /// Libraries indexed by name. "builtin" is always present.
2541    pub libraries: ComponentLibraryVec,
2542}
2543
2544impl ComponentMap {
2545    /// Qualified lookup: "shadcn:avatar" -> finds library "shadcn", component "avatar"
2546    #[must_use] pub fn get(&self, collection: &str, name: &str) -> Option<&ComponentDef> {
2547        self.libraries
2548            .iter()
2549            .find(|lib| lib.name.as_str() == collection)
2550            .and_then(|lib| lib.components.iter().find(|c| c.id.name.as_str() == name))
2551    }
2552
2553    /// Unqualified lookup: "div" -> searches ONLY the "builtin" library.
2554    #[must_use] pub fn get_unqualified(&self, name: &str) -> Option<&ComponentDef> {
2555        self.get("builtin", name)
2556    }
2557
2558    /// Parse a "collection:name" string into a lookup
2559    #[must_use] pub fn get_by_qualified_name(&self, qualified: &str) -> Option<&ComponentDef> {
2560        if let Some((collection, name)) = qualified.split_once(':') {
2561            self.get(collection, name)
2562        } else {
2563            self.get_unqualified(qualified)
2564        }
2565    }
2566
2567    /// Get all libraries that can be exported (user-defined only)
2568    #[must_use] pub fn get_exportable_libraries(&self) -> Vec<&ComponentLibrary> {
2569        self.libraries.iter().filter(|lib| lib.exportable).collect()
2570    }
2571
2572    /// Get all component definitions across all libraries
2573    #[must_use] pub fn all_components(&self) -> Vec<&ComponentDef> {
2574        self.libraries
2575            .iter()
2576            .flat_map(|lib| lib.components.iter())
2577            .collect()
2578    }
2579}
2580
2581// ============================================================================
2582// Builtin component bridge — wraps existing render/compile into ComponentDef
2583// ============================================================================
2584
2585/// Single source of truth mapping HTML/SVG tag names to node variants.
2586///
2587/// Each `"tag" => Variant` entry expands to **both** a `NodeType::Variant` arm in
2588/// [`tag_to_node_type`] and a `NodeTypeTag::Variant` arm in [`tag_to_node_type_tag`],
2589/// so the two lookups can never drift apart. Tags whose two enums diverge —
2590/// `img`, `image`, `icon` — are handled as explicit special cases inside each
2591/// generated function and are intentionally absent from this table.
2592macro_rules! html_tag_node_types {
2593    ($($tag:literal => $variant:ident),* $(,)?) => {
2594        /// Map a builtin tag name to its corresponding `NodeType`.
2595        /// Falls back to `NodeType::Div` for unknown tags.
2596        #[must_use] pub fn tag_to_node_type(tag: &str) -> NodeType {
2597            match tag {
2598                // `<img>` becomes a replaced `NodeType::Image`. The `src` attribute is not
2599                // available here, so a placeholder `NullImage` (0x0, empty tag) is created;
2600                // `xml_node_to_dom_fast` overrides it with a `NullImage` whose `tag` carries
2601                // the `src` bytes so a renderer (e.g. printpdf) can resolve the actual image.
2602                "img" => NodeType::Image(azul_css::css::BoxOrStatic::heap(
2603                    crate::resources::ImageRef::null_image(
2604                        0,
2605                        0,
2606                        crate::resources::RawImageFormat::RGBA8,
2607                        alloc::vec::Vec::new(),
2608                    ),
2609                )),
2610                $($tag => NodeType::$variant,)*
2611                _ => NodeType::Div,
2612            }
2613        }
2614
2615        /// Map a tag name to its CSS `NodeTypeTag` for CSS matching in the compile pipeline.
2616        /// Falls back to `NodeTypeTag::Div` for unknown tags.
2617        fn tag_to_node_type_tag(tag: &str) -> NodeTypeTag {
2618            match tag {
2619                // `img`/`image`/`icon` have no 1:1 `NodeType` equivalent (see
2620                // `tag_to_node_type`), so they map to dedicated `NodeTypeTag` variants.
2621                "img" | "image" => NodeTypeTag::Img,
2622                "icon" => NodeTypeTag::Icon,
2623                $($tag => NodeTypeTag::$variant,)*
2624                _ => NodeTypeTag::Div,
2625            }
2626        }
2627    };
2628}
2629
2630html_tag_node_types! {
2631    // Document structure
2632    "html" => Html,
2633    "head" => Head,
2634    "title" => Title,
2635    "body" => Body,
2636    // Block-level
2637    "div" => Div,
2638    "header" => Header,
2639    "footer" => Footer,
2640    "section" => Section,
2641    "article" => Article,
2642    "aside" => Aside,
2643    "nav" => Nav,
2644    "main" => Main,
2645    "figure" => Figure,
2646    "figcaption" => FigCaption,
2647    "address" => Address,
2648    "details" => Details,
2649    "summary" => Summary,
2650    "dialog" => Dialog,
2651    // Headings
2652    "h1" => H1,
2653    "h2" => H2,
2654    "h3" => H3,
2655    "h4" => H4,
2656    "h5" => H5,
2657    "h6" => H6,
2658    // Text content
2659    "p" => P,
2660    "span" => Span,
2661    "pre" => Pre,
2662    "code" => Code,
2663    "blockquote" => BlockQuote,
2664    "br" => Br,
2665    "hr" => Hr,
2666    // Lists
2667    "ul" => Ul,
2668    "ol" => Ol,
2669    "li" => Li,
2670    "dl" => Dl,
2671    "dt" => Dt,
2672    "dd" => Dd,
2673    "menu" => Menu,
2674    "menuitem" => MenuItem,
2675    "dir" => Dir,
2676    // Tables
2677    "table" => Table,
2678    "caption" => Caption,
2679    "thead" => THead,
2680    "tbody" => TBody,
2681    "tfoot" => TFoot,
2682    "tr" => Tr,
2683    "th" => Th,
2684    "td" => Td,
2685    "colgroup" => ColGroup,
2686    "col" => Col,
2687    // Forms
2688    "form" => Form,
2689    "fieldset" => FieldSet,
2690    "legend" => Legend,
2691    "label" => Label,
2692    "input" => Input,
2693    "button" => Button,
2694    "select" => Select,
2695    "optgroup" => OptGroup,
2696    "option" => SelectOption,
2697    "textarea" => TextArea,
2698    "output" => Output,
2699    "progress" => Progress,
2700    "meter" => Meter,
2701    "datalist" => DataList,
2702    // Inline
2703    "a" => A,
2704    "strong" => Strong,
2705    "em" => Em,
2706    "b" => B,
2707    "i" => I,
2708    "u" => U,
2709    "s" => S,
2710    "small" => Small,
2711    "mark" => Mark,
2712    "del" => Del,
2713    "ins" => Ins,
2714    "samp" => Samp,
2715    "kbd" => Kbd,
2716    "var" => Var,
2717    "cite" => Cite,
2718    "dfn" => Dfn,
2719    "abbr" => Abbr,
2720    "acronym" => Acronym,
2721    "q" => Q,
2722    "time" => Time,
2723    "sub" => Sub,
2724    "sup" => Sup,
2725    "big" => Big,
2726    "bdo" => Bdo,
2727    "bdi" => Bdi,
2728    "wbr" => Wbr,
2729    "ruby" => Ruby,
2730    "rt" => Rt,
2731    "rtc" => Rtc,
2732    "rp" => Rp,
2733    "data" => Data,
2734    // Embedded content (`img` is a special case in the generated fns)
2735    "canvas" => Canvas,
2736    "object" => Object,
2737    "param" => Param,
2738    "embed" => Embed,
2739    "audio" => Audio,
2740    "video" => Video,
2741    "source" => Source,
2742    "track" => Track,
2743    "map" => Map,
2744    "area" => Area,
2745    // SVG elements
2746    "svg" => Svg,
2747    "g" => SvgG,
2748    "defs" => SvgDefs,
2749    "symbol" => SvgSymbol,
2750    "use" => SvgUse,
2751    "switch" => SvgSwitch,
2752    "path" => SvgPath,
2753    "circle" => SvgCircle,
2754    "rect" => SvgRect,
2755    "ellipse" => SvgEllipse,
2756    "line" => SvgLine,
2757    "polygon" => SvgPolygon,
2758    "polyline" => SvgPolyline,
2759    "tspan" => SvgTspan,
2760    "textpath" => SvgTextPath,
2761    "lineargradient" => SvgLinearGradient,
2762    "radialgradient" => SvgRadialGradient,
2763    "stop" => SvgStop,
2764    "pattern" => SvgPattern,
2765    "clippath" => SvgClipPathElement,
2766    "mask" => SvgMask,
2767    "filter" => SvgFilter,
2768    "feblend" => SvgFeBlend,
2769    "fecolormatrix" => SvgFeColorMatrix,
2770    "fecomponenttransfer" => SvgFeComponentTransfer,
2771    "fecomposite" => SvgFeComposite,
2772    "feconvolvematrix" => SvgFeConvolveMatrix,
2773    "fediffuselighting" => SvgFeDiffuseLighting,
2774    "fedisplacementmap" => SvgFeDisplacementMap,
2775    "fedistantlight" => SvgFeDistantLight,
2776    "fedropshadow" => SvgFeDropShadow,
2777    "feflood" => SvgFeFlood,
2778    "fefuncr" => SvgFeFuncR,
2779    "fefuncg" => SvgFeFuncG,
2780    "fefuncb" => SvgFeFuncB,
2781    "fefunca" => SvgFeFuncA,
2782    "fegaussianblur" => SvgFeGaussianBlur,
2783    "feimage" => SvgFeImage,
2784    "femerge" => SvgFeMerge,
2785    "femergenode" => SvgFeMergeNode,
2786    "femorphology" => SvgFeMorphology,
2787    "feoffset" => SvgFeOffset,
2788    "fepointlight" => SvgFePointLight,
2789    "fespecularlighting" => SvgFeSpecularLighting,
2790    "fespotlight" => SvgFeSpotLight,
2791    "fetile" => SvgFeTile,
2792    "feturbulence" => SvgFeTurbulence,
2793    "foreignobject" => SvgForeignObject,
2794    "desc" => SvgDesc,
2795    "view" => SvgView,
2796    "animate" => SvgAnimate,
2797    "animatemotion" => SvgAnimateMotion,
2798    "animatetransform" => SvgAnimateTransform,
2799    "set" => SvgSet,
2800    "mpath" => SvgMpath,
2801    // Metadata
2802    "meta" => Meta,
2803    "link" => Link,
2804    "script" => Script,
2805    "style" => Style,
2806    "base" => Base,
2807}
2808
2809/// Default render function for builtin HTML elements.
2810/// Delegates to creating a DOM node of the appropriate `NodeType`.
2811fn builtin_render_fn(
2812    def: &ComponentDef,
2813    data: &ComponentDataModel,
2814    _component_map: &ComponentMap,
2815) -> ResultStyledDomRenderDomError {
2816    let node_type = tag_to_node_type(def.id.name.as_str());
2817    let mut dom = Dom::create_node(node_type);
2818    if let Some(text_str) = data.get_default_string("text") {
2819        let prepared = prepare_string(text_str);
2820        if !prepared.is_empty() {
2821            dom = dom.with_children(alloc::vec![Dom::create_text(prepared)].into());
2822        }
2823    }
2824    let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut dom, Css::empty()));
2825    r.into()
2826}
2827
2828/// Default compile function for builtin HTML elements.
2829/// Generates `Dom::create_node(NodeType::Div)` style code for the target language.
2830fn builtin_compile_fn(
2831    def: &ComponentDef,
2832    target: &CompileTarget,
2833    data: &ComponentDataModel,
2834    indent: usize,
2835) -> ResultStringCompileError {
2836    let node_type = tag_to_node_type(def.id.name.as_str());
2837    let type_name = format!("{node_type:?}"); // "Div", "Body", "P", etc.
2838    let text = data.get_default_string("text");
2839
2840    let r: Result<AzString, CompileError> = match target {
2841        CompileTarget::Rust => {
2842            text.map_or_else(|| Ok(format!("Dom::create_node(NodeType::{type_name})").into()), |text_str| Ok(format!(
2843                    "Dom::create_node(NodeType::{}).with_children(vec![Dom::create_text(\"{}\")])",
2844                    type_name,
2845                    text_str.as_str().replace('\\', "\\\\").replace('"', "\\\"")
2846                ).into()))
2847        }
2848        CompileTarget::C => {
2849            text.map_or_else(|| Ok(format!("AzDom_create{type_name}()").into()), |text_str| Ok(format!(
2850                    "AzDom_createText(AZ_STR(\"{}\"))",
2851                    text_str
2852                        .as_str()
2853                        .replace('\\', "\\\\")
2854                        .replace('"', "\\\"")
2855                )
2856                .into()))
2857        }
2858        CompileTarget::Cpp => Ok(format!("Dom::create_{}()", type_name.to_lowercase()).into()),
2859        CompileTarget::Python => Ok(format!("Dom.create_{}()", type_name.to_lowercase()).into()),
2860    };
2861    r.into()
2862}
2863
2864/// Pushes a `<div>` containing `"field_name: value"` text into the children list.
2865fn push_scalar_field(children: &mut Vec<Dom>, field_name: &str, value: &dyn fmt::Display) {
2866    use crate::dom::{Dom, NodeType};
2867    let text = alloc::format!("{field_name}: {value}");
2868    children.push(
2869        Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(text)].into()),
2870    );
2871}
2872
2873/// Default render function for user-defined (JSON-imported) components.
2874///
2875/// Interprets the `ComponentDef` structure generically:
2876/// 1. Creates a wrapper `<div>` with the component's CSS class
2877/// 2. For each data field, renders content based on type:
2878///    - String fields → text node with current value
2879///    - Bool fields → conditional display
2880///    - `StyledDom` fields → embeds the child DOM subtree
2881///    - StructRef/EnumRef → recursively renders sub-components if found in `ComponentMap`
2882///    - Other scalar fields → text display of the value
2883/// 3. Applies the component's scoped CSS
2884#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
2885#[must_use] pub fn user_defined_render_fn(
2886    def: &ComponentDef,
2887    data: &ComponentDataModel,
2888    component_map: &ComponentMap,
2889) -> ResultStyledDomRenderDomError {
2890    use crate::dom::{Dom, NodeType};
2891    use azul_css::css::Css;
2892
2893    let mut children: Vec<Dom> = Vec::new();
2894
2895    for field in data.fields.as_ref() {
2896        let field_name = field.name.as_str();
2897
2898        // Get the current value from default_value
2899        match &field.default_value {
2900            OptionComponentDefaultValue::None => {
2901                // Required field with no value — skip in preview
2902            }
2903            OptionComponentDefaultValue::Some(default_val) => {
2904                match default_val {
2905                    ComponentDefaultValue::String(s) => {
2906                        let text = s.as_str().trim();
2907                        if !text.is_empty() {
2908                            let label_dom = Dom::create_node(NodeType::Div).with_children(
2909                                alloc::vec![Dom::create_text(text.to_string())].into(),
2910                            );
2911                            children.push(label_dom);
2912                        }
2913                    }
2914                    ComponentDefaultValue::Bool(v) => {
2915                        push_scalar_field(&mut children, field_name, v);
2916                    }
2917                    ComponentDefaultValue::I32(v) => {
2918                        push_scalar_field(&mut children, field_name, v);
2919                    }
2920                    ComponentDefaultValue::I64(v) => {
2921                        push_scalar_field(&mut children, field_name, v);
2922                    }
2923                    ComponentDefaultValue::U32(v) => {
2924                        push_scalar_field(&mut children, field_name, v);
2925                    }
2926                    ComponentDefaultValue::U64(v) => {
2927                        push_scalar_field(&mut children, field_name, v);
2928                    }
2929                    ComponentDefaultValue::Usize(v) => {
2930                        push_scalar_field(&mut children, field_name, v);
2931                    }
2932                    ComponentDefaultValue::F32(v) => {
2933                        push_scalar_field(&mut children, field_name, v);
2934                    }
2935                    ComponentDefaultValue::F64(v) => {
2936                        push_scalar_field(&mut children, field_name, v);
2937                    }
2938                    ComponentDefaultValue::ColorU(c) => {
2939                        let text = alloc::format!(
2940                            "{}: #{:02x}{:02x}{:02x}{:02x}",
2941                            field_name,
2942                            c.r,
2943                            c.g,
2944                            c.b,
2945                            c.a
2946                        );
2947                        children.push(
2948                            Dom::create_node(NodeType::Div)
2949                                .with_children(alloc::vec![Dom::create_text(text)].into()),
2950                        );
2951                    }
2952                    ComponentDefaultValue::ComponentInstance(ci) => {
2953                        // Recursively instantiate sub-component from ComponentMap
2954                        if let Some(sub_comp) =
2955                            component_map.get(ci.library.as_str(), ci.component.as_str())
2956                        {
2957                            let sub_data = sub_comp.data_model.clone();
2958                            match (sub_comp.render_fn)(sub_comp, &sub_data, component_map) {
2959                                ResultStyledDomRenderDomError::Ok(_styled_dom) => {
2960                                    // Sub-component rendered successfully — add a placeholder
2961                                    // (StyledDom cannot be directly converted back to Dom)
2962                                    let text = alloc::format!(
2963                                        "[{}:{}]",
2964                                        ci.library.as_str(),
2965                                        ci.component.as_str()
2966                                    );
2967                                    children.push(
2968                                        Dom::create_node(NodeType::Div).with_children(
2969                                            alloc::vec![Dom::create_text(text)].into(),
2970                                        ),
2971                                    );
2972                                }
2973                                ResultStyledDomRenderDomError::Err(_) => {
2974                                    // On error, show a placeholder
2975                                    let text = alloc::format!(
2976                                        "[Error rendering {}:{}]",
2977                                        ci.library.as_str(),
2978                                        ci.component.as_str()
2979                                    );
2980                                    children.push(
2981                                        Dom::create_node(NodeType::Div).with_children(
2982                                            alloc::vec![Dom::create_text(text)].into(),
2983                                        ),
2984                                    );
2985                                }
2986                            }
2987                        } else {
2988                            let text = alloc::format!(
2989                                "[Unknown component {}:{}]",
2990                                ci.library.as_str(),
2991                                ci.component.as_str()
2992                            );
2993                            children.push(
2994                                Dom::create_node(NodeType::Div)
2995                                    .with_children(alloc::vec![Dom::create_text(text)].into()),
2996                            );
2997                        }
2998                    }
2999                    ComponentDefaultValue::CallbackFnPointer(name) => {
3000                        // Callbacks are not rendered, just acknowledged
3001                        let text = alloc::format!("{}: fn({})", field_name, name.as_str());
3002                        children.push(
3003                            Dom::create_node(NodeType::Div)
3004                                .with_children(alloc::vec![Dom::create_text(text)].into()),
3005                        );
3006                    }
3007                    ComponentDefaultValue::Json(json_str) => {
3008                        let text = alloc::format!("{}: {}", field_name, json_str.as_str());
3009                        children.push(
3010                            Dom::create_node(NodeType::Div)
3011                                .with_children(alloc::vec![Dom::create_text(text)].into()),
3012                        );
3013                    }
3014                    ComponentDefaultValue::None => {
3015                        // No default, skip
3016                    }
3017                }
3018            }
3019        }
3020    }
3021
3022    let mut wrapper = Dom::create_node(NodeType::Div);
3023    if !children.is_empty() {
3024        wrapper = wrapper.with_children(children.into());
3025    }
3026
3027    // Apply component CSS
3028    let css = if def.css.as_str().is_empty() {
3029        Css::empty()
3030    } else {
3031        Css::from_string(def.css.clone())
3032    };
3033
3034    let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut wrapper, css));
3035    r.into()
3036}
3037
3038/// Default compile function for user-defined (JSON-imported) components.
3039///
3040/// Generates source code that creates the component's DOM structure for the
3041/// target language. For each data field, emits the appropriate code:
3042/// - String fields → text node creation
3043/// - Scalar fields → formatted display
3044/// - `ComponentInstance` → function call to sub-component's render function
3045/// - `StyledDom` slots → child parameter pass-through
3046#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3047#[must_use] pub fn user_defined_compile_fn(
3048    def: &ComponentDef,
3049    target: &CompileTarget,
3050    data: &ComponentDataModel,
3051    indent: usize,
3052) -> ResultStringCompileError {
3053    let tag = def.id.name.as_str();
3054    let indent_str = " ".repeat(indent * 4);
3055    let inner_indent = " ".repeat((indent + 1) * 4);
3056
3057    let r: Result<AzString, CompileError> = match target {
3058        CompileTarget::Rust => {
3059            let mut lines = Vec::new();
3060            lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3061            lines.push(alloc::format!(
3062                "{indent_str}let mut children: Vec<Dom> = Vec::new();"
3063            ));
3064
3065            for field in data.fields.as_ref() {
3066                let fname = field.name.as_str();
3067                match &field.default_value {
3068                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3069                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3070                        lines.push(alloc::format!(
3071                            "{inner_indent}children.push(Dom::create_text(\"{escaped}\"));"
3072                        ));
3073                    }
3074                    OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => {
3075                        lines.push(alloc::format!(
3076                            "{inner_indent}children.push(Dom::create_text(format!(\"{{}}: {{}}\", \"{fname}\", {b}).as_str()));"
3077                        ));
3078                    }
3079                    OptionComponentDefaultValue::Some(
3080                        ComponentDefaultValue::ComponentInstance(ci),
3081                    ) => {
3082                        let fn_name =
3083                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3084                        lines.push(alloc::format!(
3085                            "{}children.push({}()); // sub-component {}:{}",
3086                            inner_indent,
3087                            fn_name,
3088                            ci.library.as_str(),
3089                            ci.component.as_str()
3090                        ));
3091                    }
3092                    _ => {
3093                        // For other types, generate a placeholder comment
3094                        lines.push(alloc::format!(
3095                            "{}// field '{}': {:?}",
3096                            inner_indent,
3097                            fname,
3098                            field.field_type
3099                        ));
3100                    }
3101                }
3102            }
3103
3104            lines.push(alloc::format!(
3105                "{indent_str}Dom::create_node(NodeType::Div).with_children(children.into())"
3106            ));
3107            Ok(lines.join("\n").into())
3108        }
3109        CompileTarget::C => {
3110            let mut lines = Vec::new();
3111            lines.push(alloc::format!("{indent_str}/* Component: {tag} */"));
3112            lines.push(alloc::format!(
3113                "{indent_str}AzDom root = AzDom_createDiv();"
3114            ));
3115
3116            for field in data.fields.as_ref() {
3117                let fname = field.name.as_str();
3118                match &field.default_value {
3119                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3120                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3121                        lines.push(alloc::format!(
3122                            "{inner_indent}AzDom_addChild(&root, AzDom_createText(AZ_STR(\"{escaped}\")));"
3123                        ));
3124                    }
3125                    OptionComponentDefaultValue::Some(
3126                        ComponentDefaultValue::ComponentInstance(ci),
3127                    ) => {
3128                        let fn_name =
3129                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3130                        lines.push(alloc::format!(
3131                            "{inner_indent}AzDom_addChild(&root, {fn_name}());"
3132                        ));
3133                    }
3134                    _ => {
3135                        lines.push(alloc::format!("{inner_indent}/* field '{fname}' */"));
3136                    }
3137                }
3138            }
3139
3140            lines.push(alloc::format!("{indent_str}return root;"));
3141            Ok(lines.join("\n").into())
3142        }
3143        CompileTarget::Cpp => {
3144            let mut lines = Vec::new();
3145            lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3146            lines.push(alloc::format!(
3147                "{indent_str}auto root = Dom::create_div();"
3148            ));
3149
3150            for field in data.fields.as_ref() {
3151                let fname = field.name.as_str();
3152                match &field.default_value {
3153                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3154                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3155                        lines.push(alloc::format!(
3156                            "{inner_indent}root.add_child(Dom::create_text(String(\"{escaped}\")));"
3157                        ));
3158                    }
3159                    OptionComponentDefaultValue::Some(
3160                        ComponentDefaultValue::ComponentInstance(ci),
3161                    ) => {
3162                        let fn_name =
3163                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3164                        lines.push(alloc::format!(
3165                            "{inner_indent}root.add_child({fn_name}());"
3166                        ));
3167                    }
3168                    _ => {
3169                        lines.push(alloc::format!("{inner_indent}// field '{fname}'"));
3170                    }
3171                }
3172            }
3173
3174            lines.push(alloc::format!("{indent_str}return root;"));
3175            Ok(lines.join("\n").into())
3176        }
3177        CompileTarget::Python => {
3178            let mut lines = Vec::new();
3179            lines.push(alloc::format!("{indent_str}# Component: {tag}"));
3180            lines.push(alloc::format!("{indent_str}root = Dom.create_div()"));
3181
3182            for field in data.fields.as_ref() {
3183                let fname = field.name.as_str();
3184                match &field.default_value {
3185                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3186                        let escaped = s
3187                            .as_str()
3188                            .replace('\\', "\\\\")
3189                            .replace('"', "\\\"")
3190                            .replace('\'', "\\'");
3191                        lines.push(alloc::format!(
3192                            "{inner_indent}root = root.with_child(Dom.create_text(\"{escaped}\"))"
3193                        ));
3194                    }
3195                    OptionComponentDefaultValue::Some(
3196                        ComponentDefaultValue::ComponentInstance(ci),
3197                    ) => {
3198                        let fn_name =
3199                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3200                        lines.push(alloc::format!(
3201                            "{inner_indent}root = root.with_child({fn_name}())"
3202                        ));
3203                    }
3204                    _ => {
3205                        lines.push(alloc::format!("{inner_indent}# field '{fname}'"));
3206                    }
3207                }
3208            }
3209
3210            lines.push(alloc::format!("{indent_str}return root"));
3211            Ok(lines.join("\n").into())
3212        }
3213    };
3214    r.into()
3215}
3216
3217/// Create a `ComponentDef` for a builtin HTML element.
3218///
3219/// # Arguments
3220/// * `tag` - HTML tag name (e.g. "button", "div")
3221/// * `display_name` - Human-readable name (e.g. "Button", "Div")
3222/// * `default_text` - Default text content for the preview, or `None` if the element has no text.
3223///   Pass `Some("Button text")` for `<button>`, `Some("")` for text elements like `<span>` that
3224///   accept text but have no meaningful default.
3225/// * `css` - Component-level CSS string. For most builtin elements this is `""` because
3226///   styling comes from `ua_css.rs` and the `SystemStyle`. Components that need extra
3227///   styling (e.g. a future high-level button widget) can pass CSS here.
3228fn builtin_component_def(
3229    tag: &str,
3230    display_name: &str,
3231    default_text: Option<&str>,
3232    css: &str,
3233) -> ComponentDef {
3234    let mut fields = builtin_data_model(tag);
3235    // If a default_text is provided, this element accepts text content
3236    if let Some(text) = default_text {
3237        fields.push(data_field(
3238            "text",
3239            ComponentFieldType::String,
3240            Some(ComponentDefaultValue::String(AzString::from(text))),
3241            "Text content of the element",
3242        ));
3243    }
3244    let model_name = format!("{display_name}Data");
3245    ComponentDef {
3246        id: ComponentId::builtin(tag),
3247        display_name: AzString::from(display_name),
3248        description: AzString::from(format!("HTML <{tag}> element").as_str()),
3249        css: AzString::from(css),
3250        source: ComponentSource::Builtin,
3251        data_model: ComponentDataModel {
3252            name: AzString::from(model_name.as_str()),
3253            description: AzString::from(format!("Data model for <{tag}>").as_str()),
3254            fields: fields.into(),
3255        },
3256        render_fn: builtin_render_fn,
3257        compile_fn: builtin_compile_fn,
3258        render_fn_source: None.into(),
3259        compile_fn_source: None.into(),
3260    }
3261}
3262
3263/// Helper to create a `ComponentDataField` with a rich type
3264fn data_field(
3265    name: &str,
3266    ft: ComponentFieldType,
3267    default: Option<ComponentDefaultValue>,
3268    description: &str,
3269) -> ComponentDataField {
3270    let required = default.is_none();
3271    ComponentDataField {
3272        name: AzString::from(name),
3273        field_type: ft,
3274        default_value: default.map_or_else(|| OptionComponentDefaultValue::None, OptionComponentDefaultValue::Some),
3275        required,
3276        description: AzString::from(description),
3277    }
3278}
3279
3280/// Returns the tag-specific data model fields for builtin HTML elements.
3281/// These are the component's "main data model" — the attributes that define
3282/// what the component needs as configuration (e.g., `href` for `<a>`,
3283/// `src` for `<img>`). Universal HTML attributes (id, class, style, etc.)
3284/// are NOT included here — they are added separately by the debug server.
3285#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3286fn builtin_data_model(tag: &str) -> Vec<ComponentDataField> {
3287    use ComponentDefaultValue as D;
3288    use ComponentFieldType::{String, Bool, I32};
3289    match tag {
3290        "a" => alloc::vec![
3291            data_field(
3292                "href",
3293                String,
3294                Some(D::String(AzString::from_const_str(""))),
3295                "URL the link points to"
3296            ),
3297            data_field(
3298                "target",
3299                String,
3300                Some(D::String(AzString::from_const_str(""))),
3301                "Where to open the linked document (_blank, _self, _parent, _top)"
3302            ),
3303            data_field(
3304                "rel",
3305                String,
3306                Some(D::String(AzString::from_const_str(""))),
3307                "Relationship between current and linked document"
3308            ),
3309        ],
3310        "img" | "image" => alloc::vec![
3311            data_field("src", String, None, "URL of the image"),
3312            data_field(
3313                "alt",
3314                String,
3315                Some(D::String(AzString::from_const_str(""))),
3316                "Alternative text for the image"
3317            ),
3318            data_field(
3319                "width",
3320                String,
3321                Some(D::String(AzString::from_const_str(""))),
3322                "Width of the image"
3323            ),
3324            data_field(
3325                "height",
3326                String,
3327                Some(D::String(AzString::from_const_str(""))),
3328                "Height of the image"
3329            ),
3330        ],
3331        "form" => alloc::vec![
3332            data_field(
3333                "action",
3334                String,
3335                Some(D::String(AzString::from_const_str(""))),
3336                "URL where form data is submitted"
3337            ),
3338            data_field(
3339                "method",
3340                String,
3341                Some(D::String(AzString::from_const_str("GET"))),
3342                "HTTP method for form submission (GET or POST)"
3343            ),
3344        ],
3345        "label" => alloc::vec![data_field(
3346            "for",
3347            String,
3348            Some(D::String(AzString::from_const_str(""))),
3349            "ID of the form element this label is for"
3350        ),],
3351        "button" => alloc::vec![
3352            data_field(
3353                "type",
3354                String,
3355                Some(D::String(AzString::from_const_str("button"))),
3356                "Button type (button, submit, reset)"
3357            ),
3358            data_field(
3359                "disabled",
3360                Bool,
3361                Some(D::Bool(false)),
3362                "Whether the button is disabled"
3363            ),
3364        ],
3365        "td" | "th" => alloc::vec![
3366            data_field(
3367                "colspan",
3368                I32,
3369                Some(D::I32(1)),
3370                "Number of columns the cell spans"
3371            ),
3372            data_field(
3373                "rowspan",
3374                I32,
3375                Some(D::I32(1)),
3376                "Number of rows the cell spans"
3377            ),
3378        ],
3379        "icon" => alloc::vec![data_field(
3380            "name",
3381            String,
3382            Some(D::String(AzString::from_const_str(""))),
3383            "Icon name"
3384        ),],
3385        "ol" => alloc::vec![
3386            data_field(
3387                "start",
3388                I32,
3389                Some(D::I32(1)),
3390                "Start value for the ordered list"
3391            ),
3392            data_field(
3393                "type",
3394                String,
3395                Some(D::String(AzString::from_const_str("1"))),
3396                "Numbering type (1, A, a, I, i)"
3397            ),
3398        ],
3399        // Form controls
3400        "input" => alloc::vec![
3401            data_field(
3402                "type",
3403                String,
3404                Some(D::String(AzString::from_const_str("text"))),
3405                "Input type (text, password, email, number, checkbox, radio, etc.)"
3406            ),
3407            data_field(
3408                "name",
3409                String,
3410                Some(D::String(AzString::from_const_str(""))),
3411                "Name of the input for form submission"
3412            ),
3413            data_field(
3414                "value",
3415                String,
3416                Some(D::String(AzString::from_const_str(""))),
3417                "Current value of the input"
3418            ),
3419            data_field(
3420                "placeholder",
3421                String,
3422                Some(D::String(AzString::from_const_str(""))),
3423                "Placeholder text"
3424            ),
3425            data_field(
3426                "disabled",
3427                Bool,
3428                Some(D::Bool(false)),
3429                "Whether the input is disabled"
3430            ),
3431            data_field(
3432                "required",
3433                Bool,
3434                Some(D::Bool(false)),
3435                "Whether the input is required"
3436            ),
3437            data_field(
3438                "readonly",
3439                Bool,
3440                Some(D::Bool(false)),
3441                "Whether the input is read-only"
3442            ),
3443            data_field(
3444                "checked",
3445                Bool,
3446                Some(D::Bool(false)),
3447                "Whether the checkbox/radio is checked"
3448            ),
3449            data_field(
3450                "min",
3451                String,
3452                Some(D::String(AzString::from_const_str(""))),
3453                "Minimum value (for number, range, date)"
3454            ),
3455            data_field(
3456                "max",
3457                String,
3458                Some(D::String(AzString::from_const_str(""))),
3459                "Maximum value (for number, range, date)"
3460            ),
3461            data_field(
3462                "step",
3463                String,
3464                Some(D::String(AzString::from_const_str(""))),
3465                "Step increment (for number, range)"
3466            ),
3467            data_field(
3468                "pattern",
3469                String,
3470                Some(D::String(AzString::from_const_str(""))),
3471                "Regex pattern for validation"
3472            ),
3473            data_field(
3474                "maxlength",
3475                String,
3476                Some(D::String(AzString::from_const_str(""))),
3477                "Maximum number of characters"
3478            ),
3479        ],
3480        "select" => alloc::vec![
3481            data_field(
3482                "name",
3483                String,
3484                Some(D::String(AzString::from_const_str(""))),
3485                "Name for form submission"
3486            ),
3487            data_field(
3488                "multiple",
3489                Bool,
3490                Some(D::Bool(false)),
3491                "Whether multiple options can be selected"
3492            ),
3493            data_field(
3494                "disabled",
3495                Bool,
3496                Some(D::Bool(false)),
3497                "Whether the select is disabled"
3498            ),
3499            data_field(
3500                "required",
3501                Bool,
3502                Some(D::Bool(false)),
3503                "Whether selection is required"
3504            ),
3505            data_field(
3506                "size",
3507                String,
3508                Some(D::String(AzString::from_const_str(""))),
3509                "Number of visible options"
3510            ),
3511        ],
3512        "option" => alloc::vec![
3513            data_field(
3514                "value",
3515                String,
3516                Some(D::String(AzString::from_const_str(""))),
3517                "Value submitted with the form"
3518            ),
3519            data_field(
3520                "selected",
3521                Bool,
3522                Some(D::Bool(false)),
3523                "Whether this option is selected"
3524            ),
3525            data_field(
3526                "disabled",
3527                Bool,
3528                Some(D::Bool(false)),
3529                "Whether this option is disabled"
3530            ),
3531        ],
3532        "optgroup" => alloc::vec![
3533            data_field(
3534                "label",
3535                String,
3536                Some(D::String(AzString::from_const_str(""))),
3537                "Label for the option group"
3538            ),
3539            data_field(
3540                "disabled",
3541                Bool,
3542                Some(D::Bool(false)),
3543                "Whether the group is disabled"
3544            ),
3545        ],
3546        "textarea" => alloc::vec![
3547            data_field(
3548                "name",
3549                String,
3550                Some(D::String(AzString::from_const_str(""))),
3551                "Name for form submission"
3552            ),
3553            data_field(
3554                "placeholder",
3555                String,
3556                Some(D::String(AzString::from_const_str(""))),
3557                "Placeholder text"
3558            ),
3559            data_field("rows", I32, Some(D::I32(2)), "Number of visible text lines"),
3560            data_field(
3561                "cols",
3562                I32,
3563                Some(D::I32(20)),
3564                "Visible width in average character widths"
3565            ),
3566            data_field(
3567                "disabled",
3568                Bool,
3569                Some(D::Bool(false)),
3570                "Whether the textarea is disabled"
3571            ),
3572            data_field(
3573                "required",
3574                Bool,
3575                Some(D::Bool(false)),
3576                "Whether content is required"
3577            ),
3578            data_field(
3579                "readonly",
3580                Bool,
3581                Some(D::Bool(false)),
3582                "Whether the textarea is read-only"
3583            ),
3584            data_field(
3585                "maxlength",
3586                String,
3587                Some(D::String(AzString::from_const_str(""))),
3588                "Maximum number of characters"
3589            ),
3590        ],
3591        "fieldset" => alloc::vec![data_field(
3592            "disabled",
3593            Bool,
3594            Some(D::Bool(false)),
3595            "Whether all controls in the fieldset are disabled"
3596        ),],
3597        "output" => alloc::vec![
3598            data_field(
3599                "for",
3600                String,
3601                Some(D::String(AzString::from_const_str(""))),
3602                "IDs of elements that contributed to the output"
3603            ),
3604            data_field(
3605                "name",
3606                String,
3607                Some(D::String(AzString::from_const_str(""))),
3608                "Name for form submission"
3609            ),
3610        ],
3611        "progress" => alloc::vec![
3612            data_field(
3613                "value",
3614                String,
3615                Some(D::String(AzString::from_const_str(""))),
3616                "Current progress value"
3617            ),
3618            data_field(
3619                "max",
3620                String,
3621                Some(D::String(AzString::from_const_str("1"))),
3622                "Maximum value"
3623            ),
3624        ],
3625        "meter" => alloc::vec![
3626            data_field(
3627                "value",
3628                String,
3629                Some(D::String(AzString::from_const_str(""))),
3630                "Current value"
3631            ),
3632            data_field(
3633                "min",
3634                String,
3635                Some(D::String(AzString::from_const_str("0"))),
3636                "Minimum value"
3637            ),
3638            data_field(
3639                "max",
3640                String,
3641                Some(D::String(AzString::from_const_str("1"))),
3642                "Maximum value"
3643            ),
3644            data_field(
3645                "low",
3646                String,
3647                Some(D::String(AzString::from_const_str(""))),
3648                "Low threshold"
3649            ),
3650            data_field(
3651                "high",
3652                String,
3653                Some(D::String(AzString::from_const_str(""))),
3654                "High threshold"
3655            ),
3656            data_field(
3657                "optimum",
3658                String,
3659                Some(D::String(AzString::from_const_str(""))),
3660                "Optimum value"
3661            ),
3662        ],
3663        // Interactive
3664        "details" => alloc::vec![data_field(
3665            "open",
3666            Bool,
3667            Some(D::Bool(false)),
3668            "Whether the details are visible"
3669        ),],
3670        "dialog" => alloc::vec![data_field(
3671            "open",
3672            Bool,
3673            Some(D::Bool(false)),
3674            "Whether the dialog is active and can be interacted with"
3675        ),],
3676        // Embedded content
3677        "audio" | "video" => alloc::vec![
3678            data_field(
3679                "src",
3680                String,
3681                Some(D::String(AzString::from_const_str(""))),
3682                "URL of the media resource"
3683            ),
3684            data_field(
3685                "controls",
3686                Bool,
3687                Some(D::Bool(false)),
3688                "Whether to show playback controls"
3689            ),
3690            data_field(
3691                "autoplay",
3692                Bool,
3693                Some(D::Bool(false)),
3694                "Whether to start playing automatically"
3695            ),
3696            data_field(
3697                "loop",
3698                Bool,
3699                Some(D::Bool(false)),
3700                "Whether to loop playback"
3701            ),
3702            data_field(
3703                "muted",
3704                Bool,
3705                Some(D::Bool(false)),
3706                "Whether audio is muted"
3707            ),
3708            data_field(
3709                "preload",
3710                String,
3711                Some(D::String(AzString::from_const_str("auto"))),
3712                "Preload hint (none, metadata, auto)"
3713            ),
3714        ],
3715        "source" => alloc::vec![
3716            data_field("src", String, None, "URL of the media resource"),
3717            data_field(
3718                "type",
3719                String,
3720                Some(D::String(AzString::from_const_str(""))),
3721                "MIME type of the resource"
3722            ),
3723        ],
3724        "track" => alloc::vec![
3725            data_field("src", String, None, "URL of the track file"),
3726            data_field(
3727                "kind",
3728                String,
3729                Some(D::String(AzString::from_const_str("subtitles"))),
3730                "Kind of text track (subtitles, captions, descriptions, chapters, metadata)"
3731            ),
3732            data_field(
3733                "srclang",
3734                String,
3735                Some(D::String(AzString::from_const_str(""))),
3736                "Language of the track text"
3737            ),
3738            data_field(
3739                "label",
3740                String,
3741                Some(D::String(AzString::from_const_str(""))),
3742                "User-readable title for the track"
3743            ),
3744            data_field(
3745                "default",
3746                Bool,
3747                Some(D::Bool(false)),
3748                "Whether this is the default track"
3749            ),
3750        ],
3751        "canvas" => alloc::vec![
3752            data_field(
3753                "width",
3754                String,
3755                Some(D::String(AzString::from_const_str("300"))),
3756                "Width of the canvas in pixels"
3757            ),
3758            data_field(
3759                "height",
3760                String,
3761                Some(D::String(AzString::from_const_str("150"))),
3762                "Height of the canvas in pixels"
3763            ),
3764        ],
3765        "embed" => alloc::vec![
3766            data_field("src", String, None, "URL of the resource to embed"),
3767            data_field(
3768                "type",
3769                String,
3770                Some(D::String(AzString::from_const_str(""))),
3771                "MIME type of the embedded content"
3772            ),
3773            data_field(
3774                "width",
3775                String,
3776                Some(D::String(AzString::from_const_str(""))),
3777                "Width"
3778            ),
3779            data_field(
3780                "height",
3781                String,
3782                Some(D::String(AzString::from_const_str(""))),
3783                "Height"
3784            ),
3785        ],
3786        "object" => alloc::vec![
3787            data_field(
3788                "data",
3789                String,
3790                Some(D::String(AzString::from_const_str(""))),
3791                "URL of the resource"
3792            ),
3793            data_field(
3794                "type",
3795                String,
3796                Some(D::String(AzString::from_const_str(""))),
3797                "MIME type of the resource"
3798            ),
3799            data_field(
3800                "width",
3801                String,
3802                Some(D::String(AzString::from_const_str(""))),
3803                "Width"
3804            ),
3805            data_field(
3806                "height",
3807                String,
3808                Some(D::String(AzString::from_const_str(""))),
3809                "Height"
3810            ),
3811        ],
3812        "param" => alloc::vec![
3813            data_field("name", String, None, "Name of the parameter"),
3814            data_field(
3815                "value",
3816                String,
3817                Some(D::String(AzString::from_const_str(""))),
3818                "Value of the parameter"
3819            ),
3820        ],
3821        "area" => alloc::vec![
3822            data_field(
3823                "shape",
3824                String,
3825                Some(D::String(AzString::from_const_str("default"))),
3826                "Shape of the area (default, rect, circle, poly)"
3827            ),
3828            data_field(
3829                "coords",
3830                String,
3831                Some(D::String(AzString::from_const_str(""))),
3832                "Coordinates of the area"
3833            ),
3834            data_field(
3835                "href",
3836                String,
3837                Some(D::String(AzString::from_const_str(""))),
3838                "URL for the area link"
3839            ),
3840            data_field(
3841                "alt",
3842                String,
3843                Some(D::String(AzString::from_const_str(""))),
3844                "Alternative text"
3845            ),
3846            data_field(
3847                "target",
3848                String,
3849                Some(D::String(AzString::from_const_str(""))),
3850                "Where to open the linked document"
3851            ),
3852        ],
3853        "map" => alloc::vec![data_field(
3854            "name",
3855            String,
3856            None,
3857            "Name of the image map (referenced by usemap)"
3858        ),],
3859        // Inline semantics with special attributes
3860        "time" => alloc::vec![data_field(
3861            "datetime",
3862            String,
3863            Some(D::String(AzString::from_const_str(""))),
3864            "Machine-readable date/time value"
3865        ),],
3866        "data" => alloc::vec![data_field(
3867            "value",
3868            String,
3869            Some(D::String(AzString::from_const_str(""))),
3870            "Machine-readable value"
3871        ),],
3872        "abbr" | "acronym" | "dfn" => alloc::vec![data_field(
3873            "title",
3874            String,
3875            Some(D::String(AzString::from_const_str(""))),
3876            "Full expansion or definition"
3877        ),],
3878        "q" | "blockquote" => alloc::vec![data_field(
3879            "cite",
3880            String,
3881            Some(D::String(AzString::from_const_str(""))),
3882            "URL of the source of the quotation"
3883        ),],
3884        "del" | "ins" => alloc::vec![
3885            data_field(
3886                "cite",
3887                String,
3888                Some(D::String(AzString::from_const_str(""))),
3889                "URL explaining the change"
3890            ),
3891            data_field(
3892                "datetime",
3893                String,
3894                Some(D::String(AzString::from_const_str(""))),
3895                "Date/time of the change"
3896            ),
3897        ],
3898        "bdo" => alloc::vec![data_field(
3899            "dir",
3900            String,
3901            Some(D::String(AzString::from_const_str("ltr"))),
3902            "Text direction (ltr, rtl)"
3903        ),],
3904        "col" | "colgroup" => alloc::vec![data_field(
3905            "span",
3906            I32,
3907            Some(D::I32(1)),
3908            "Number of columns the element spans"
3909        ),],
3910        // Metadata
3911        "meta" => alloc::vec![
3912            data_field(
3913                "name",
3914                String,
3915                Some(D::String(AzString::from_const_str(""))),
3916                "Metadata name"
3917            ),
3918            data_field(
3919                "content",
3920                String,
3921                Some(D::String(AzString::from_const_str(""))),
3922                "Metadata value"
3923            ),
3924            data_field(
3925                "charset",
3926                String,
3927                Some(D::String(AzString::from_const_str(""))),
3928                "Character encoding"
3929            ),
3930            data_field(
3931                "http-equiv",
3932                String,
3933                Some(D::String(AzString::from_const_str(""))),
3934                "HTTP header equivalent"
3935            ),
3936        ],
3937        "link" => alloc::vec![
3938            data_field("rel", String, None, "Relationship type"),
3939            data_field(
3940                "href",
3941                String,
3942                Some(D::String(AzString::from_const_str(""))),
3943                "URL of the linked resource"
3944            ),
3945            data_field(
3946                "type",
3947                String,
3948                Some(D::String(AzString::from_const_str(""))),
3949                "MIME type of the linked resource"
3950            ),
3951        ],
3952        "script" => alloc::vec![
3953            data_field(
3954                "src",
3955                String,
3956                Some(D::String(AzString::from_const_str(""))),
3957                "URL of external script"
3958            ),
3959            data_field(
3960                "type",
3961                String,
3962                Some(D::String(AzString::from_const_str(""))),
3963                "MIME type or module"
3964            ),
3965            data_field(
3966                "async",
3967                Bool,
3968                Some(D::Bool(false)),
3969                "Execute asynchronously"
3970            ),
3971            data_field(
3972                "defer",
3973                Bool,
3974                Some(D::Bool(false)),
3975                "Defer execution until page load"
3976            ),
3977        ],
3978        "style" => alloc::vec![data_field(
3979            "type",
3980            String,
3981            Some(D::String(AzString::from_const_str("text/css"))),
3982            "MIME type of the style sheet"
3983        ),],
3984        "base" => alloc::vec![
3985            data_field(
3986                "href",
3987                String,
3988                Some(D::String(AzString::from_const_str(""))),
3989                "Base URL for relative URLs"
3990            ),
3991            data_field(
3992                "target",
3993                String,
3994                Some(D::String(AzString::from_const_str(""))),
3995                "Default target for hyperlinks"
3996            ),
3997        ],
3998        _ => alloc::vec![],
3999    }
4000}
4001
4002impl Default for ComponentMap {
4003    /// Returns an empty `ComponentMap` with no libraries.
4004    ///
4005    /// Use `AppConfig::create()` (which registers the 52 builtins via
4006    /// `register_builtin_components`) followed by `ComponentMap::from_libraries()`
4007    /// to get a fully-populated map.
4008    fn default() -> Self {
4009        Self {
4010            libraries: ComponentLibraryVec::from_const_slice(&[]),
4011        }
4012    }
4013}
4014
4015impl ComponentMap {
4016    #[must_use] pub fn create() -> Self {
4017        Self::default()
4018    }
4019
4020    /// Create a `ComponentMap` with the 52 built-in HTML element components pre-registered.
4021    #[must_use] pub fn with_builtin() -> Self {
4022        Self {
4023            libraries: alloc::vec![register_builtin_components()].into(),
4024        }
4025    }
4026
4027    /// Build a `ComponentMap` from the libraries stored in an `AppConfig`.
4028    ///
4029    /// The `component_libraries` field already contains builtins (registered in
4030    /// `AppConfig::create()`) plus any user-added libraries.  No merging needed —
4031    /// `add_component_library` / `add_component` handle insertion at registration time.
4032    #[must_use] pub fn from_libraries(libs: &ComponentLibraryVec) -> Self {
4033        Self {
4034            libraries: libs.clone(),
4035        }
4036    }
4037}
4038
4039/// Convert XML attributes to a `ComponentDataModel` by cloning the component's
4040/// base data model and overriding field defaults with values from the XML attributes.
4041///
4042/// This is the bridge between the XML parsing layer (key-value string pairs)
4043/// and the typed component data model. For each field in the base model,
4044/// if a matching XML attribute exists, its string value is set as the new default.
4045///
4046/// # Arguments
4047/// * `base_model` - The component's data model template (from `ComponentDef::data_model`)
4048/// * `xml_attributes` - The XML node's attribute map
4049/// * `text_content` - Optional text content from child text nodes
4050///
4051/// # Returns
4052/// A cloned `ComponentDataModel` with overridden defaults
4053fn xml_attrs_to_data_model(
4054    base_model: &ComponentDataModel,
4055    xml_attributes: &XmlAttributeMap,
4056    text_content: Option<&str>,
4057) -> ComponentDataModel {
4058    let mut model = base_model.clone();
4059
4060    // Override defaults from XML attributes
4061    let mut fields_vec = core::mem::replace(
4062        &mut model.fields,
4063        ComponentDataFieldVec::from_const_slice(&[]),
4064    )
4065    .into_library_owned_vec();
4066
4067    for field in &mut fields_vec {
4068        if let Some(attr_value) = xml_attributes.get_key(field.name.as_str()) {
4069            // Override the default_value with the XML attribute's string value
4070            field.default_value = OptionComponentDefaultValue::Some(ComponentDefaultValue::String(
4071                attr_value.clone(),
4072            ));
4073        }
4074    }
4075
4076    model.fields = ComponentDataFieldVec::from_vec(fields_vec);
4077
4078    // Handle text content — set the "text" field if present
4079    if let Some(text) = text_content {
4080        let prepared = prepare_string(text);
4081        if !prepared.is_empty() {
4082            model = model.with_default(
4083                "text",
4084                ComponentDefaultValue::String(AzString::from(prepared.as_str())),
4085            );
4086        }
4087    }
4088
4089    model
4090}
4091
4092// ============================================================================
4093// Structural builtin components: if, for, map
4094// ============================================================================
4095
4096/// `builtin:if` — conditional rendering.
4097/// Takes `condition: Bool`, `then: StyledDom`, and optionally `else: StyledDom`.
4098fn builtin_if_component() -> ComponentDef {
4099    ComponentDef {
4100        id: ComponentId::builtin("if"),
4101        display_name: AzString::from_const_str("If"),
4102        description: AzString::from_const_str("Conditional rendering: shows 'then' if condition is true, else shows 'else' (if provided)."),
4103        css: AzString::from_const_str(""),
4104        source: ComponentSource::Builtin,
4105        data_model: ComponentDataModel {
4106            name: AzString::from_const_str("IfData"),
4107            description: AzString::from_const_str("Data for conditional rendering"),
4108            fields: alloc::vec![
4109                data_field("condition", ComponentFieldType::Bool, Some(ComponentDefaultValue::Bool(false)), "The boolean condition to evaluate"),
4110            ].into(),
4111        },
4112        render_fn: builtin_if_render_fn,
4113        compile_fn: builtin_if_compile_fn,
4114        render_fn_source: None.into(),
4115        compile_fn_source: None.into(),
4116    }
4117}
4118
4119fn builtin_if_render_fn(
4120    _comp: &ComponentDef,
4121    data_model: &ComponentDataModel,
4122    _component_map: &ComponentMap,
4123) -> ResultStyledDomRenderDomError {
4124    // Evaluate the condition field
4125    let condition = data_model
4126        .fields
4127        .iter()
4128        .find(|f| f.name.as_str() == "condition")
4129        .and_then(|f| match &f.default_value {
4130            OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => Some(*b),
4131            _ => None,
4132        })
4133        .unwrap_or(false);
4134
4135    let label = if condition {
4136        "if: true (then branch)"
4137    } else {
4138        "if: false (else branch)"
4139    };
4140    let mut dom =
4141        Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(label)].into());
4142    let css = Css::empty();
4143    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4144}
4145
4146fn builtin_if_compile_fn(
4147    _comp: &ComponentDef,
4148    target: &CompileTarget,
4149    _data: &ComponentDataModel,
4150    _indent: usize,
4151) -> ResultStringCompileError {
4152    match target {
4153        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4154            "if data.condition {\n    // then branch\n    Dom::create_div()\n} else {\n    // else branch\n    Dom::create_div()\n}"
4155        )),
4156        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4157            "if (data.condition) {\n    // then branch\n    AzDom_createDiv();\n} else {\n    // else branch\n    AzDom_createDiv();\n}"
4158        )),
4159        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4160            "if (data.condition) {\n    // then branch\n    Dom::create_div();\n} else {\n    // else branch\n    Dom::create_div();\n}"
4161        )),
4162        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4163            "if data.condition:\n    # then branch\n    Dom.create_div()\nelse:\n    # else branch\n    Dom.create_div()"
4164        )),
4165    }
4166}
4167
4168/// `builtin:for` — iterative rendering.
4169/// Takes `count: U32` (number of iterations), renders children N times.
4170fn builtin_for_component() -> ComponentDef {
4171    ComponentDef {
4172        id: ComponentId::builtin("for"),
4173        display_name: AzString::from_const_str("For Loop"),
4174        description: AzString::from_const_str(
4175            "Iterative rendering: repeats children 'count' times.",
4176        ),
4177        css: AzString::from_const_str(""),
4178        source: ComponentSource::Builtin,
4179        data_model: ComponentDataModel {
4180            name: AzString::from_const_str("ForData"),
4181            description: AzString::from_const_str("Data for iterative rendering"),
4182            fields: alloc::vec![data_field(
4183                "count",
4184                ComponentFieldType::U32,
4185                Some(ComponentDefaultValue::U32(3)),
4186                "Number of iterations"
4187            ),]
4188            .into(),
4189        },
4190        render_fn: builtin_for_render_fn,
4191        compile_fn: builtin_for_compile_fn,
4192        render_fn_source: None.into(),
4193        compile_fn_source: None.into(),
4194    }
4195}
4196
4197fn builtin_for_render_fn(
4198    _comp: &ComponentDef,
4199    data_model: &ComponentDataModel,
4200    _component_map: &ComponentMap,
4201) -> ResultStyledDomRenderDomError {
4202    let count = data_model
4203        .fields
4204        .iter()
4205        .find(|f| f.name.as_str() == "count")
4206        .and_then(|f| match &f.default_value {
4207            OptionComponentDefaultValue::Some(ComponentDefaultValue::U32(n)) => Some(*n),
4208            _ => None,
4209        })
4210        .unwrap_or(3);
4211
4212    let mut items: Vec<Dom> = Vec::new();
4213    for i in 0..count {
4214        items.push(
4215            Dom::create_node(NodeType::Div)
4216                .with_children(alloc::vec![Dom::create_text(alloc::format!("Item {i}"))].into()),
4217        );
4218    }
4219    let mut dom = Dom::create_node(NodeType::Div).with_children(items.into());
4220    let css = Css::empty();
4221    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4222}
4223
4224fn builtin_for_compile_fn(
4225    _comp: &ComponentDef,
4226    target: &CompileTarget,
4227    _data: &ComponentDataModel,
4228    _indent: usize,
4229) -> ResultStringCompileError {
4230    match target {
4231        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4232            "let mut children = Vec::new();\nfor i in 0..data.count {\n    children.push(Dom::create_div());\n}\nDom::create_div().with_children(children)"
4233        )),
4234        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4235            "AzDom container = AzDom_createDiv();\nfor (uint32_t i = 0; i < data.count; i++) {\n    AzDom_addChild(&container, AzDom_createDiv());\n}"
4236        )),
4237        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4238            "auto container = Dom::create_div();\nfor (uint32_t i = 0; i < data.count; i++) {\n    container.add_child(Dom::create_div());\n}"
4239        )),
4240        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4241            "container = Dom.create_div()\nfor i in range(data.count):\n    container = container.with_child(Dom.create_div())"
4242        )),
4243    }
4244}
4245
4246/// `builtin:map` — map data to DOM.
4247/// Takes `data_json: String` (JSON array) + maps each element.
4248fn builtin_map_component() -> ComponentDef {
4249    ComponentDef {
4250        id: ComponentId::builtin("map"),
4251        display_name: AzString::from_const_str("Map"),
4252        description: AzString::from_const_str(
4253            "Map data to DOM: applies a template to each item in a collection.",
4254        ),
4255        css: AzString::from_const_str(""),
4256        source: ComponentSource::Builtin,
4257        data_model: ComponentDataModel {
4258            name: AzString::from_const_str("MapData"),
4259            description: AzString::from_const_str("Data for map rendering"),
4260            fields: alloc::vec![data_field(
4261                "data_json",
4262                ComponentFieldType::String,
4263                Some(ComponentDefaultValue::String(AzString::from_const_str(
4264                    "[]"
4265                ))),
4266                "JSON array of items to map over"
4267            ),]
4268            .into(),
4269        },
4270        render_fn: builtin_map_render_fn,
4271        compile_fn: builtin_map_compile_fn,
4272        render_fn_source: None.into(),
4273        compile_fn_source: None.into(),
4274    }
4275}
4276
4277fn builtin_map_render_fn(
4278    _comp: &ComponentDef,
4279    data_model: &ComponentDataModel,
4280    _component_map: &ComponentMap,
4281) -> ResultStyledDomRenderDomError {
4282    // For now, render a placeholder — actual mapping requires callback support
4283    let data_str = data_model
4284        .fields
4285        .iter()
4286        .find(|f| f.name.as_str() == "data_json")
4287        .and_then(|f| match &f.default_value {
4288            OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
4289                Some(s.as_str().to_string())
4290            }
4291            _ => None,
4292        })
4293        .unwrap_or_else(|| "[]".to_string());
4294
4295    let label = alloc::format!("map: data_json={data_str}");
4296    let mut dom =
4297        Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(label)].into());
4298    let css = Css::empty();
4299    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4300}
4301
4302fn builtin_map_compile_fn(
4303    _comp: &ComponentDef,
4304    target: &CompileTarget,
4305    _data: &ComponentDataModel,
4306    _indent: usize,
4307) -> ResultStringCompileError {
4308    match target {
4309        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4310            "let items: Vec<serde_json::Value> = serde_json::from_str(&data.data_json).unwrap_or_default();\nlet children: Vec<Dom> = items.iter().map(|item| {\n    Dom::create_div() // map template\n}).collect();\nDom::create_div().with_children(children)"
4311        )),
4312        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4313            "// Parse data.data_json and map each item\nAzDom container = AzDom_createDiv();\n// TODO: iterate parsed JSON array"
4314        )),
4315        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4316            "// Parse data.data_json and map each item\nauto container = Dom::create_div();\n// TODO: iterate parsed JSON array"
4317        )),
4318        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4319            "import json\nitems = json.loads(data.data_json)\ncontainer = Dom.create_div()\nfor item in items:\n    container = container.with_child(Dom.create_div())"
4320        )),
4321    }
4322}
4323
4324/// Register the 52 built-in HTML element components.
4325///
4326/// This is an `extern "C"` function pointer compatible with
4327/// `RegisterComponentLibraryFnType`, so it can be passed directly to
4328/// `AppConfig::add_component_library()`.
4329///
4330/// Called once during `AppConfig::create()` — the framework dogfoods
4331/// its own component registration system for builtins.
4332#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
4333#[must_use] pub extern "C" fn register_builtin_components() -> ComponentLibrary {
4334    ComponentLibrary {
4335        name: AzString::from_const_str("builtin"),
4336        version: AzString::from_const_str("1.0.0"),
4337        description: AzString::from_const_str("Built-in HTML elements"),
4338        exportable: false,
4339        modifiable: false,
4340        data_models: Vec::new().into(),
4341        enum_models: Vec::new().into(),
4342        components: alloc::vec![
4343            // Structural
4344            builtin_component_def("html", "HTML", None, ""),
4345            builtin_component_def("head", "Head", None, ""),
4346            builtin_component_def("title", "Title", Some(""), ""),
4347            builtin_component_def("body", "Body", None, ""),
4348            // Block-level
4349            builtin_component_def("div", "Div", None, ""),
4350            builtin_component_def("header", "Header", None, ""),
4351            builtin_component_def("footer", "Footer", None, ""),
4352            builtin_component_def("section", "Section", None, ""),
4353            builtin_component_def("article", "Article", None, ""),
4354            builtin_component_def("aside", "Aside", None, ""),
4355            builtin_component_def("nav", "Nav", None, ""),
4356            builtin_component_def("main", "Main", None, ""),
4357            builtin_component_def("figure", "Figure", None, ""),
4358            builtin_component_def("figcaption", "Figure Caption", Some(""), ""),
4359            builtin_component_def("address", "Address", Some(""), ""),
4360            builtin_component_def("details", "Details", None, ""),
4361            builtin_component_def("summary", "Summary", Some("Details"), ""),
4362            builtin_component_def("dialog", "Dialog", None, ""),
4363            // Headings — default text is the heading level name so preview is visible
4364            builtin_component_def("h1", "Heading 1", Some("Heading 1"), ""),
4365            builtin_component_def("h2", "Heading 2", Some("Heading 2"), ""),
4366            builtin_component_def("h3", "Heading 3", Some("Heading 3"), ""),
4367            builtin_component_def("h4", "Heading 4", Some("Heading 4"), ""),
4368            builtin_component_def("h5", "Heading 5", Some("Heading 5"), ""),
4369            builtin_component_def("h6", "Heading 6", Some("Heading 6"), ""),
4370            // Text content
4371            builtin_component_def("p", "Paragraph", Some("Paragraph text"), ""),
4372            builtin_component_def("span", "Span", Some(""), ""),
4373            builtin_component_def("pre", "Preformatted", Some(""), ""),
4374            builtin_component_def("code", "Code", Some(""), ""),
4375            builtin_component_def("blockquote", "Blockquote", Some(""), ""),
4376            builtin_component_def("br", "Line Break", None, ""),
4377            builtin_component_def("hr", "Horizontal Rule", None, ""),
4378            builtin_component_def("icon", "Icon", Some(""), ""),
4379            // Lists
4380            builtin_component_def("ul", "Unordered List", None, ""),
4381            builtin_component_def("ol", "Ordered List", None, ""),
4382            builtin_component_def("li", "List Item", Some("List item"), ""),
4383            builtin_component_def("dl", "Description List", None, ""),
4384            builtin_component_def("dt", "Description Term", Some(""), ""),
4385            builtin_component_def("dd", "Description Details", Some(""), ""),
4386            builtin_component_def("menu", "Menu", None, ""),
4387            builtin_component_def("menuitem", "Menu Item", Some(""), ""),
4388            builtin_component_def("dir", "Directory List", None, ""),
4389            // Tables
4390            builtin_component_def("table", "Table", None, ""),
4391            builtin_component_def("caption", "Table Caption", Some(""), ""),
4392            builtin_component_def("thead", "Table Head", None, ""),
4393            builtin_component_def("tbody", "Table Body", None, ""),
4394            builtin_component_def("tfoot", "Table Foot", None, ""),
4395            builtin_component_def("tr", "Table Row", None, ""),
4396            builtin_component_def("th", "Table Header Cell", Some("Header"), ""),
4397            builtin_component_def("td", "Table Data Cell", Some(""), ""),
4398            builtin_component_def("colgroup", "Column Group", None, ""),
4399            builtin_component_def("col", "Column", None, ""),
4400            // Inline
4401            builtin_component_def("a", "Link", Some("Link text"), ""),
4402            builtin_component_def("strong", "Strong", Some(""), ""),
4403            builtin_component_def("em", "Emphasis", Some(""), ""),
4404            builtin_component_def("b", "Bold", Some(""), ""),
4405            builtin_component_def("i", "Italic", Some(""), ""),
4406            builtin_component_def("u", "Underline", Some(""), ""),
4407            builtin_component_def("s", "Strikethrough", Some(""), ""),
4408            builtin_component_def("small", "Small", Some(""), ""),
4409            builtin_component_def("mark", "Mark", Some(""), ""),
4410            builtin_component_def("del", "Deleted Text", Some(""), ""),
4411            builtin_component_def("ins", "Inserted Text", Some(""), ""),
4412            builtin_component_def("sub", "Subscript", Some(""), ""),
4413            builtin_component_def("sup", "Superscript", Some(""), ""),
4414            builtin_component_def("samp", "Sample Output", Some(""), ""),
4415            builtin_component_def("kbd", "Keyboard Input", Some(""), ""),
4416            builtin_component_def("var", "Variable", Some(""), ""),
4417            builtin_component_def("cite", "Citation", Some(""), ""),
4418            builtin_component_def("dfn", "Definition", Some(""), ""),
4419            builtin_component_def("abbr", "Abbreviation", Some(""), ""),
4420            builtin_component_def("acronym", "Acronym", Some(""), ""),
4421            builtin_component_def("q", "Inline Quote", Some(""), ""),
4422            builtin_component_def("time", "Time", Some(""), ""),
4423            builtin_component_def("big", "Big", Some(""), ""),
4424            builtin_component_def("bdo", "BiDi Override", Some(""), ""),
4425            builtin_component_def("bdi", "BiDi Isolate", Some(""), ""),
4426            builtin_component_def("wbr", "Word Break Opportunity", None, ""),
4427            builtin_component_def("ruby", "Ruby Annotation", None, ""),
4428            builtin_component_def("rt", "Ruby Text", Some(""), ""),
4429            builtin_component_def("rtc", "Ruby Text Container", None, ""),
4430            builtin_component_def("rp", "Ruby Parenthesis", Some(""), ""),
4431            builtin_component_def("data", "Data", Some(""), ""),
4432            // Forms
4433            builtin_component_def("form", "Form", None, ""),
4434            builtin_component_def("fieldset", "Field Set", None, ""),
4435            builtin_component_def("legend", "Legend", Some("Legend"), ""),
4436            builtin_component_def("label", "Label", Some("Label"), ""),
4437            builtin_component_def("input", "Input", None, ""),
4438            builtin_component_def("button", "Button", Some("Button text"), ""),
4439            builtin_component_def("select", "Select", None, ""),
4440            builtin_component_def("optgroup", "Option Group", None, ""),
4441            builtin_component_def("option", "Option", Some(""), ""),
4442            builtin_component_def("textarea", "Text Area", Some(""), ""),
4443            builtin_component_def("output", "Output", Some(""), ""),
4444            builtin_component_def("progress", "Progress", None, ""),
4445            builtin_component_def("meter", "Meter", None, ""),
4446            builtin_component_def("datalist", "Data List", None, ""),
4447            // Embedded content
4448            builtin_component_def("canvas", "Canvas", None, ""),
4449            builtin_component_def("object", "Object", None, ""),
4450            builtin_component_def("param", "Parameter", None, ""),
4451            builtin_component_def("embed", "Embed", None, ""),
4452            builtin_component_def("audio", "Audio", None, ""),
4453            builtin_component_def("video", "Video", None, ""),
4454            builtin_component_def("source", "Source", None, ""),
4455            builtin_component_def("track", "Track", None, ""),
4456            builtin_component_def("map", "Image Map", None, ""),
4457            builtin_component_def("area", "Map Area", None, ""),
4458            builtin_component_def("svg", "SVG", None, ""),
4459            // Metadata
4460            builtin_component_def("meta", "Meta", None, ""),
4461            builtin_component_def("link", "Link (Resource)", None, ""),
4462            builtin_component_def("script", "Script", Some(""), ""),
4463            builtin_component_def("style", "Style", Some(""), ""),
4464            builtin_component_def("base", "Base URL", None, ""),
4465            // Structural control-flow builtins (F1-F3)
4466            builtin_if_component(),
4467            builtin_for_component(),
4468            builtin_map_component(),
4469        ]
4470        .into(),
4471    }
4472}
4473
4474// ============================================================================
4475// End new component system types
4476// ============================================================================
4477
4478/// Wrapper for the XML parser - necessary to easily create a Dom from
4479/// XML without putting an XML solver into `azul-core`.
4480#[derive(Debug, Default)]
4481pub struct DomXml {
4482    pub parsed_dom: StyledDom,
4483}
4484
4485impl DomXml {
4486    /// Convenience function, only available in tests, useful for quickly writing UI tests.
4487    /// Wraps the XML string in the required `<app></app>` braces, panics if the XML couldn't be
4488    /// parsed.
4489    ///
4490    /// ## Example
4491    ///
4492    /// ```rust,ignore
4493    /// # use azul::dom::Dom;
4494    /// # use azul::xml::DomXml;
4495    /// let dom = DomXml::mock("<div id='test' />");
4496    /// dom.assert_eq(Dom::create_div().with_id("test"));
4497    /// ```
4498    ///
4499    /// # Panics
4500    ///
4501    /// Panics if the rendered DOM does not equal `other` (this is a test-only
4502    /// assertion helper).
4503    #[cfg(test)]
4504    pub fn assert_eq(self, other: StyledDom) {
4505        let mut body = Dom::create_body();
4506        let mut fixed = StyledDom::create(&mut body, Css::empty());
4507        fixed.append_child(other);
4508        assert!(!(self.parsed_dom != fixed), 
4509                "\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: \
4510                 ----------\r\n{}\r\n",
4511                self.parsed_dom.get_html_string("", "", true),
4512                fixed.get_html_string("", "", true)
4513            );
4514    }
4515
4516    #[must_use] pub fn into_styled_dom(self) -> StyledDom {
4517        self.into()
4518    }
4519}
4520
4521impl From<DomXml> for StyledDom {
4522    fn from(val: DomXml) -> Self {
4523        val.parsed_dom
4524    }
4525}
4526
4527/// Represents a child of an XML node - either an element or text
4528#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4529#[repr(C, u8)]
4530pub enum XmlNodeChild {
4531    /// A text node
4532    Text(AzString),
4533    /// An element node
4534    Element(XmlNode),
4535}
4536
4537impl_option!(
4538    XmlNodeChild,
4539    OptionXmlNodeChild,
4540    copy = false,
4541    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4542);
4543
4544impl XmlNodeChild {
4545    /// Get the text content if this is a text node
4546    #[must_use] pub fn as_text(&self) -> Option<&str> {
4547        match self {
4548            Self::Text(s) => Some(s.as_str()),
4549            Self::Element(_) => None,
4550        }
4551    }
4552
4553    /// Get the element if this is an element node
4554    #[must_use] pub const fn as_element(&self) -> Option<&XmlNode> {
4555        match self {
4556            Self::Text(_) => None,
4557            Self::Element(node) => Some(node),
4558        }
4559    }
4560
4561    /// Get the element mutably if this is an element node
4562    pub const fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
4563        match self {
4564            Self::Text(_) => None,
4565            Self::Element(node) => Some(node),
4566        }
4567    }
4568}
4569
4570impl_vec!(
4571    XmlNodeChild,
4572    XmlNodeChildVec,
4573    XmlNodeChildVecDestructor,
4574    XmlNodeChildVecDestructorType,
4575    XmlNodeChildVecSlice,
4576    OptionXmlNodeChild
4577);
4578impl_vec_mut!(XmlNodeChild, XmlNodeChildVec);
4579impl_vec_debug!(XmlNodeChild, XmlNodeChildVec);
4580impl_vec_partialeq!(XmlNodeChild, XmlNodeChildVec);
4581impl_vec_eq!(XmlNodeChild, XmlNodeChildVec);
4582impl_vec_partialord!(XmlNodeChild, XmlNodeChildVec);
4583impl_vec_ord!(XmlNodeChild, XmlNodeChildVec);
4584impl_vec_hash!(XmlNodeChild, XmlNodeChildVec);
4585impl_vec_clone!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor);
4586
4587/// Represents one XML node tag
4588#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4589#[repr(C)]
4590pub struct XmlNode {
4591    /// Type of the node
4592    pub node_type: XmlTagName,
4593    /// Attributes of an XML node (note: not yet filtered and / or broken into function arguments!)
4594    pub attributes: XmlAttributeMap,
4595    /// Direct children of this node (can be text or element nodes)
4596    pub children: XmlNodeChildVec,
4597}
4598
4599impl_option!(
4600    XmlNode,
4601    OptionXmlNode,
4602    copy = false,
4603    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4604);
4605
4606impl XmlNode {
4607    pub fn create<I: Into<XmlTagName>>(node_type: I) -> Self {
4608        Self {
4609            node_type: node_type.into(),
4610            ..Default::default()
4611        }
4612    }
4613    #[must_use] pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
4614        Self {
4615            children: v.into(),
4616            ..self
4617        }
4618    }
4619
4620    /// Get all text content concatenated from direct children
4621    #[must_use] pub fn get_text_content(&self) -> String {
4622        self.children
4623            .as_ref()
4624            .iter()
4625            .filter_map(|child| child.as_text())
4626            .collect::<Vec<_>>()
4627            .join("")
4628    }
4629
4630    /// Check if this node has only text children (no element children)
4631    #[must_use] pub fn has_only_text_children(&self) -> bool {
4632        self.children
4633            .as_ref()
4634            .iter()
4635            .all(|child| matches!(child, XmlNodeChild::Text(_)))
4636    }
4637}
4638
4639impl_vec!(
4640    XmlNode,
4641    XmlNodeVec,
4642    XmlNodeVecDestructor,
4643    XmlNodeVecDestructorType,
4644    XmlNodeVecSlice,
4645    OptionXmlNode
4646);
4647impl_vec_mut!(XmlNode, XmlNodeVec);
4648impl_vec_debug!(XmlNode, XmlNodeVec);
4649impl_vec_partialeq!(XmlNode, XmlNodeVec);
4650impl_vec_eq!(XmlNode, XmlNodeVec);
4651impl_vec_partialord!(XmlNode, XmlNodeVec);
4652impl_vec_ord!(XmlNode, XmlNodeVec);
4653impl_vec_hash!(XmlNode, XmlNodeVec);
4654impl_vec_clone!(XmlNode, XmlNodeVec, XmlNodeVecDestructor);
4655
4656#[derive(Debug, Clone, PartialEq)]
4657#[repr(C, u8)]
4658pub enum DomXmlParseError {
4659    /// No `<html></html>` node component present
4660    NoHtmlNode,
4661    /// Multiple `<html>` nodes
4662    MultipleHtmlRootNodes,
4663    /// No ´<body></body>´ node in the root HTML
4664    NoBodyInHtml,
4665    /// The DOM can only have one <body> node, not multiple.
4666    MultipleBodyNodes,
4667    /// Note: Sadly, the error type can only be a string because xmlparser
4668    /// returns all errors as strings. There is an open PR to fix
4669    /// this deficiency, but since the XML parsing is only needed for
4670    /// hot-reloading and compiling, it doesn't matter that much.
4671    Xml(XmlError),
4672    /// Invalid hierarchy close tags, i.e `<app></p></app>`
4673    MalformedHierarchy(MalformedHierarchyError),
4674    /// A component raised an error while rendering the DOM - holds the component name + error
4675    /// string
4676    RenderDom(RenderDomError),
4677    /// Something went wrong while parsing an XML component
4678    Component(ComponentParseError),
4679    /// Error parsing global CSS in head node
4680    Css(CssParseErrorOwned),
4681}
4682
4683impl From<XmlError> for DomXmlParseError {
4684    fn from(e: XmlError) -> Self {
4685        Self::Xml(e)
4686    }
4687}
4688
4689impl From<ComponentParseError> for DomXmlParseError {
4690    fn from(e: ComponentParseError) -> Self {
4691        Self::Component(e)
4692    }
4693}
4694
4695impl From<RenderDomError> for DomXmlParseError {
4696    fn from(e: RenderDomError) -> Self {
4697        Self::RenderDom(e)
4698    }
4699}
4700
4701impl From<CssParseErrorOwned> for DomXmlParseError {
4702    fn from(e: CssParseErrorOwned) -> Self {
4703        Self::Css(e)
4704    }
4705}
4706
4707/// Error that can happen from the translation from XML code to Rust code -
4708/// stringified, since it is only used for printing and is not exposed in the public API
4709#[derive(Debug, Clone, PartialEq)]
4710#[repr(C, u8)]
4711pub enum CompileError {
4712    Dom(RenderDomError),
4713    Xml(DomXmlParseError),
4714    Css(CssParseErrorOwned),
4715}
4716
4717impl From<ComponentError> for CompileError {
4718    fn from(e: ComponentError) -> Self {
4719        Self::Dom(RenderDomError::Component(e))
4720    }
4721}
4722
4723impl From<CssParseErrorOwned> for CompileError {
4724    fn from(e: CssParseErrorOwned) -> Self {
4725        Self::Css(e)
4726    }
4727}
4728
4729impl fmt::Display for CompileError {
4730    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4731        use self::CompileError::{Dom, Xml, Css};
4732        match self {
4733            Dom(d) => write!(f, "{d}"),
4734            Xml(s) => write!(f, "{s}"),
4735            Css(s) => write!(f, "{}", s.to_shared()),
4736        }
4737    }
4738}
4739
4740impl From<RenderDomError> for CompileError {
4741    fn from(e: RenderDomError) -> Self {
4742        Self::Dom(e)
4743    }
4744}
4745
4746impl From<DomXmlParseError> for CompileError {
4747    fn from(e: DomXmlParseError) -> Self {
4748        Self::Xml(e)
4749    }
4750}
4751
4752/// Wrapper for `UselessFunctionArgument` error data.
4753#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4754#[repr(C)]
4755pub struct UselessFunctionArgumentError {
4756    pub component_name: AzString,
4757    pub argument_name: AzString,
4758    pub valid_args: StringVec,
4759}
4760
4761#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4762#[repr(C, u8)]
4763pub enum ComponentError {
4764    /// While instantiating a component, a function argument
4765    /// was encountered that the component won't use or react to.
4766    UselessFunctionArgument(UselessFunctionArgumentError),
4767    /// A certain node type can't be rendered, because the
4768    /// renderer for this node is not available isn't available
4769    ///
4770    /// `UnknownComponent(component_name)`
4771    UnknownComponent(AzString),
4772}
4773
4774#[derive(Debug, Clone, PartialEq)]
4775#[repr(C, u8)]
4776pub enum RenderDomError {
4777    Component(ComponentError),
4778    /// Error parsing the CSS on the component style
4779    CssError(CssParseErrorOwned),
4780}
4781
4782impl From<ComponentError> for RenderDomError {
4783    fn from(e: ComponentError) -> Self {
4784        Self::Component(e)
4785    }
4786}
4787
4788impl From<CssParseErrorOwned> for RenderDomError {
4789    fn from(e: CssParseErrorOwned) -> Self {
4790        Self::CssError(e)
4791    }
4792}
4793
4794/// Wrapper for `MissingType` error data.
4795#[derive(Debug, Clone, PartialEq, Eq)]
4796#[repr(C)]
4797pub struct MissingTypeError {
4798    pub arg_pos: usize,
4799    pub arg_name: AzString,
4800}
4801
4802/// Wrapper for `WhiteSpaceInComponentName` error data.
4803#[derive(Debug, Clone, PartialEq, Eq)]
4804#[repr(C)]
4805pub struct WhiteSpaceInComponentNameError {
4806    pub arg_pos: usize,
4807    pub arg_name: AzString,
4808}
4809
4810/// Wrapper for `WhiteSpaceInComponentType` error data.
4811#[derive(Debug, Clone, PartialEq, Eq)]
4812#[repr(C)]
4813pub struct WhiteSpaceInComponentTypeError {
4814    pub arg_pos: usize,
4815    pub arg_name: AzString,
4816    pub arg_type: AzString,
4817}
4818
4819#[derive(Debug, Clone, PartialEq)]
4820#[repr(C, u8)]
4821pub enum ComponentParseError {
4822    /// Given `XmlNode` is not a `<component />` node.
4823    NotAComponent,
4824    /// A `<component>` node does not have a `name` attribute.
4825    UnnamedComponent,
4826    /// Argument at position `usize` is either empty or has no name
4827    MissingName(usize),
4828    /// Argument at position `usize` with the name
4829    /// `String` doesn't have a `: type`
4830    MissingType(MissingTypeError),
4831    /// Component name may not contain a whitespace
4832    /// (probably missing a `:` between the name and the type)
4833    WhiteSpaceInComponentName(WhiteSpaceInComponentNameError),
4834    /// Component type may not contain a whitespace
4835    /// (probably missing a `,` between the type and the next name)
4836    WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError),
4837    /// Error parsing the <style> tag / CSS
4838    CssError(CssParseErrorOwned),
4839}
4840
4841impl fmt::Display for DomXmlParseError {
4842    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4843        use self::DomXmlParseError::{NoHtmlNode, MultipleHtmlRootNodes, NoBodyInHtml, MultipleBodyNodes, Xml, MalformedHierarchy, RenderDom, Component, Css};
4844        match self {
4845            NoHtmlNode => write!(
4846                f,
4847                "No <html> node found as the root of the file - empty file?"
4848            ),
4849            MultipleHtmlRootNodes => write!(
4850                f,
4851                "Multiple <html> nodes found as the root of the file - only one root node allowed"
4852            ),
4853            NoBodyInHtml => write!(
4854                f,
4855                "No <body> node found as a direct child of an <html> node - malformed DOM \
4856                 hierarchy?"
4857            ),
4858            MultipleBodyNodes => write!(
4859                f,
4860                "Multiple <body> nodes present, only one <body> node is allowed"
4861            ),
4862            Xml(e) => write!(f, "Error parsing XML: {e}"),
4863            MalformedHierarchy(e) => write!(
4864                f,
4865                "Invalid </{}> tag: expected </{}>",
4866                e.got.as_str(),
4867                e.expected.as_str()
4868            ),
4869            RenderDom(e) => write!(f, "Error rendering DOM: {e}"),
4870            Component(c) => write!(f, "Error parsing component in <head> node:\r\n{c}"),
4871            Css(c) => write!(f, "Error parsing CSS in <head> node:\r\n{}", c.to_shared()),
4872        }
4873    }
4874}
4875
4876impl fmt::Display for ComponentParseError {
4877    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4878        use self::ComponentParseError::{NotAComponent, UnnamedComponent, MissingName, MissingType, WhiteSpaceInComponentName, WhiteSpaceInComponentType, CssError};
4879        match self {
4880            NotAComponent => write!(f, "Expected <component/> node, found no such node"),
4881            UnnamedComponent => write!(
4882                f,
4883                "Found <component/> tag with out a \"name\" attribute, component must have a name"
4884            ),
4885            MissingName(arg_pos) => write!(
4886                f,
4887                "Argument at position {arg_pos} is either empty or has no name"
4888            ),
4889            MissingType(e) => write!(
4890                f,
4891                "Argument \"{}\" at position {} doesn't have a `: type`",
4892                e.arg_name, e.arg_pos
4893            ),
4894            WhiteSpaceInComponentName(e) => {
4895                write!(
4896                    f,
4897                    "Missing `:` between the name and the type in argument {} (around \"{}\")",
4898                    e.arg_pos, e.arg_name
4899                )
4900            }
4901            WhiteSpaceInComponentType(e) => {
4902                write!(
4903                    f,
4904                    "Missing `,` between two arguments (in argument {}, position {}, around \
4905                     \"{}\")",
4906                    e.arg_name, e.arg_pos, e.arg_type
4907                )
4908            }
4909            CssError(lsf) => write!(f, "Error parsing <style> tag: {}", lsf.to_shared()),
4910        }
4911    }
4912}
4913
4914impl fmt::Display for ComponentError {
4915    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4916        use self::ComponentError::{UselessFunctionArgument, UnknownComponent};
4917        match self {
4918            UselessFunctionArgument(e) => {
4919                write!(
4920                    f,
4921                    "Useless component argument \"{}\": \"{}\" - available args are: {:#?}",
4922                    e.component_name, e.argument_name, e.valid_args
4923                )
4924            }
4925            UnknownComponent(name) => write!(f, "Unknown component: \"{name}\""),
4926        }
4927    }
4928}
4929
4930impl fmt::Display for RenderDomError {
4931    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4932        use self::RenderDomError::{Component, CssError};
4933        match self {
4934            Component(c) => write!(f, "{c}"),
4935            CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
4936        }
4937    }
4938}
4939
4940/// Find the one and only `<body>` node, return error if
4941/// there is no app node or there are multiple app nodes
4942#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
4943/// # Errors
4944///
4945/// Returns an error if the document has no `<html>` root node.
4946pub fn get_html_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
4947    let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
4948        if let XmlNodeChild::Element(node) = child {
4949            // HTML element names are case-insensitive (ASCII). NOT normalize_casing:
4950            // that inserts '_' before each uppercase letter for component-name
4951            // canonicalisation, so "HTML" would become "h_t_m_l" and never match.
4952            if node.node_type.as_str().eq_ignore_ascii_case("html") {
4953                Some(node)
4954            } else {
4955                None
4956            }
4957        } else {
4958            None
4959        }
4960    });
4961
4962    let html_node = html_node_iterator
4963        .next()
4964        .ok_or(DomXmlParseError::NoHtmlNode)?;
4965    if html_node_iterator.next().is_some() {
4966        Err(DomXmlParseError::MultipleHtmlRootNodes)
4967    } else {
4968        Ok(html_node)
4969    }
4970}
4971
4972/// Find the one and only `<body>` node, return error if
4973/// there is no app node or there are multiple app nodes
4974#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
4975/// # Errors
4976///
4977/// Returns an error if the document has no `<body>` node.
4978pub fn get_body_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
4979    fn find_body_recursive(nodes: &[XmlNodeChild], depth: usize) -> Option<&XmlNode> {
4980        // AUDIT 2026-07-08: bound recursion depth to avoid a stack overflow on
4981        // pathologically deep markup while hunting for the <body> element.
4982        if depth > MAX_XML_NESTING_DEPTH {
4983            return None;
4984        }
4985        for child in nodes {
4986            if let XmlNodeChild::Element(node) = child {
4987                // case-insensitive ASCII tag match; see get_html_node.
4988                if node.node_type.as_str().eq_ignore_ascii_case("body") {
4989                    return Some(node);
4990                }
4991                // Recurse into children
4992                if let Some(found) = find_body_recursive(node.children.as_ref(), depth + 1) {
4993                    return Some(found);
4994                }
4995            }
4996        }
4997        None
4998    }
4999
5000    // First try to find body as a direct child (proper HTML structure)
5001    let direct_body = root_nodes.iter().find_map(|child| {
5002        if let XmlNodeChild::Element(node) = child {
5003            // case-insensitive ASCII tag match; see get_html_node.
5004            if node.node_type.as_str().eq_ignore_ascii_case("body") {
5005                Some(node)
5006            } else {
5007                None
5008            }
5009        } else {
5010            None
5011        }
5012    });
5013
5014    if let Some(body) = direct_body {
5015        return Ok(body);
5016    }
5017
5018    // If not found as direct child, search recursively (for malformed HTML like example.com)
5019    // where <body> might be nested inside <head> due to missing </head> tag
5020    find_body_recursive(root_nodes, 0).ok_or(DomXmlParseError::NoBodyInHtml)
5021}
5022
5023/// Searches in the the `root_nodes` for a `node_type`, convenience function in order to
5024/// for example find the first <blah /> node in all these nodes.
5025/// This function searches recursively through the entire tree.
5026fn find_node_by_type<'a>(root_nodes: &'a [XmlNodeChild], node_type: &str) -> Option<&'a XmlNode> {
5027    // First check direct children
5028    for child in root_nodes {
5029        if let XmlNodeChild::Element(node) = child {
5030            // case-insensitive ASCII tag match; see get_html_node.
5031            if node.node_type.as_str().eq_ignore_ascii_case(node_type) {
5032                return Some(node);
5033            }
5034        }
5035    }
5036
5037    // If not found, search recursively (for malformed HTML)
5038    for child in root_nodes {
5039        if let XmlNodeChild::Element(node) = child {
5040            if let Some(found) = find_node_by_type(node.children.as_ref(), node_type) {
5041                return Some(found);
5042            }
5043        }
5044    }
5045
5046    None
5047}
5048
5049#[must_use] pub fn find_attribute<'a>(node: &'a XmlNode, attribute: &str) -> Option<&'a AzString> {
5050    node.attributes
5051        .iter()
5052        .find(|n| normalize_casing(n.key.as_str()).as_str() == attribute)
5053        .map(|s| &s.value)
5054}
5055
5056/// Normalizes input such as `abcDef`, `AbcDef`, `abc-def` to the normalized form of `abc_def`
5057#[must_use] pub fn normalize_casing(input: &str) -> String {
5058    let mut words: Vec<String> = Vec::new();
5059    let mut cur_str = Vec::new();
5060
5061    for ch in input.chars() {
5062        if ch.is_uppercase() || ch == '_' || ch == '-' {
5063            if !cur_str.is_empty() {
5064                words.push(cur_str.iter().collect());
5065                cur_str.clear();
5066            }
5067            if ch.is_uppercase() {
5068                cur_str.extend(ch.to_lowercase());
5069            }
5070        } else {
5071            cur_str.extend(ch.to_lowercase());
5072        }
5073    }
5074
5075    if !cur_str.is_empty() {
5076        words.push(cur_str.iter().collect());
5077        cur_str.clear();
5078    }
5079
5080    words.join("_")
5081}
5082
5083/// Given a root node, traverses along the hierarchy, and returns a
5084/// mutable reference to the last child node of the root node
5085#[allow(trivial_casts)]
5086pub fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> {
5087    let mut hierarchy = hierarchy.to_vec();
5088    hierarchy.reverse();
5089    let Some(item) = hierarchy.pop() else {
5090        return Some(root_node);
5091    };
5092    let child = root_node.children.as_mut().get_mut(item)?;
5093    match child {
5094        XmlNodeChild::Element(node) => get_item_internal(&mut hierarchy, node),
5095        XmlNodeChild::Text(_) => None, // Can't traverse into text nodes
5096    }
5097}
5098
5099fn get_item_internal<'a>(
5100    hierarchy: &mut Vec<usize>,
5101    root_node: &'a mut XmlNode,
5102) -> Option<&'a mut XmlNode> {
5103    if hierarchy.is_empty() {
5104        return Some(root_node);
5105    }
5106    let Some(cur_item) = hierarchy.pop() else {
5107        return Some(root_node);
5108    };
5109    let child = root_node.children.as_mut().get_mut(cur_item)?;
5110    match child {
5111        XmlNodeChild::Element(node) => get_item_internal(hierarchy, node),
5112        XmlNodeChild::Text(_) => None, // Can't traverse into text nodes
5113    }
5114}
5115
5116/// Parses an XML string and returns a `StyledDom` with the components instantiated in the
5117/// `<app></app>`
5118#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5119/// # Errors
5120///
5121/// Returns an error if the XML cannot be parsed into a DOM (malformed markup or an unknown component).
5122pub fn str_to_dom<'a>(
5123    root_nodes: &'a [XmlNodeChild],
5124    component_map: &'a ComponentMap,
5125    max_width: Option<f32>,
5126) -> Result<StyledDom, DomXmlParseError> {
5127    // Delegate to the fast path (Dom::Fast / CompactDom arena).
5128    str_to_dom_fast(root_nodes, component_map, max_width)
5129}
5130
5131/// Parse XML to `StyledDom` via arena-based `FastDom` (no tree intermediary).
5132///
5133/// **Note**: `str_to_dom()` now delegates to this function, so you can use
5134/// either one. This function is kept for backward compatibility.
5135#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5136fn str_to_dom_fast<'a>(
5137    root_nodes: &'a [XmlNodeChild],
5138    component_map: &'a ComponentMap,
5139    max_width: Option<f32>,
5140) -> Result<StyledDom, DomXmlParseError> {
5141    let html_node = get_html_node(root_nodes)?;
5142    let body_node = get_body_node(html_node.children.as_ref())?;
5143
5144    let mut global_style = None;
5145
5146    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5147        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5148            let text = style_node.get_text_content();
5149            if !text.is_empty() {
5150                let parsed_css = Css::from_string(text.into());
5151                global_style = Some(parsed_css);
5152            }
5153        }
5154    }
5155
5156    render_dom_from_body_node_fast(body_node, global_style, component_map, max_width)
5157        .map_err(Into::into)
5158}
5159
5160/// Parses XML nodes and returns a `Dom` with CSS stylesheets attached (but not applied).
5161///
5162/// Unlike `str_to_dom` which returns a fully styled `StyledDom`, this function
5163/// returns an unstyled `Dom` whose `css` field carries the parsed `<style>` rules.
5164/// The layout framework will apply the CSS during the cascade pass.
5165///
5166/// This is the correct function for building a `Dom` from XML in layout callbacks
5167/// (which must return `Dom`, not `StyledDom`).
5168#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5169/// # Errors
5170///
5171/// Returns an error if the XML cannot be parsed into a DOM (malformed markup or an unknown component).
5172pub fn str_to_dom_unstyled<'a>(
5173    root_nodes: &'a [XmlNodeChild],
5174    component_map: &'a ComponentMap,
5175) -> Result<Dom, DomXmlParseError> {
5176    let html_node = get_html_node(root_nodes)?;
5177    let body_node = get_body_node(html_node.children.as_ref())?;
5178
5179    let mut global_style = None;
5180
5181    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5182        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5183            let text = style_node.get_text_content();
5184            if !text.is_empty() {
5185                let parsed_css = Css::from_string(text.into());
5186                global_style = Some(parsed_css);
5187            }
5188        }
5189    }
5190
5191    // Build the DOM tree from the body node
5192    let body_dom = xml_node_to_dom_fast(body_node, component_map, false, 0)
5193        .map_err(DomXmlParseError::from)?;
5194
5195    // Wrap in proper HTML structure (NodeType is imported at module top)
5196    let root_node_type = body_dom.root.node_type.clone();
5197
5198    let mut full_dom = match root_node_type {
5199        NodeType::Html => body_dom,
5200        NodeType::Body => Dom::create_html().with_child(body_dom),
5201        _ => {
5202            let body_wrapper = Dom::create_body().with_child(body_dom);
5203            Dom::create_html().with_child(body_wrapper)
5204        }
5205    };
5206
5207    // Attach CSS to the Dom's css field instead of applying it immediately
5208    if let Some(css) = global_style {
5209        full_dom.css = alloc::vec![css].into();
5210    }
5211
5212    Ok(full_dom)
5213}
5214
5215/// Parses an XML string and returns a `String`, which contains the Rust source code
5216/// (i.e. it compiles the XML to valid Rust)
5217#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5218/// # Errors
5219///
5220/// Returns an error if the XML cannot be parsed or compiled to Rust code.
5221pub fn str_to_rust_code<'a>(
5222    root_nodes: &'a [XmlNodeChild],
5223    imports: &str,
5224    component_map: &'a ComponentMap,
5225) -> Result<String, CompileError> {
5226    let html_node = get_html_node(root_nodes)?;
5227    let body_node = get_body_node(html_node.children.as_ref())?;
5228    let mut global_style = Css::empty();
5229
5230    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5231        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5232            let text = style_node.get_text_content();
5233            if !text.is_empty() {
5234                let parsed_css = azul_css::parser2::new_from_str(&text).0;
5235                global_style = parsed_css;
5236            }
5237        }
5238    }
5239
5240    global_style.sort_by_specificity();
5241
5242    let mut css_blocks = BTreeMap::new();
5243    let mut extra_blocks = VecContents::default();
5244    let app_source = compile_body_node_to_rust_code(
5245        body_node,
5246        component_map,
5247        &mut extra_blocks,
5248        &mut css_blocks,
5249        &global_style,
5250        CssMatcher {
5251            path: Vec::new(),
5252            indices_in_parent: vec![0],
5253            children_length: vec![body_node.children.as_ref().len()],
5254        },
5255    )?;
5256
5257    let app_source = app_source
5258        .lines()
5259        .map(|l| format!("        {l}"))
5260        .collect::<Vec<String>>()
5261        .join("\r\n");
5262
5263    // NOTE: `css_blocks` / `extra_blocks` are no longer emitted — per-node styles
5264    // are now inlined as `.with_css("..")` strings (public API) rather than as
5265    // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` blocks (that API was
5266    // removed in 32d44ed8a). The maps stay in the signatures for compatibility.
5267    let _ = (&css_blocks, &extra_blocks);
5268
5269    let main_func = "
5270
5271use azul::{
5272    app::{App, AppConfig},
5273    dom::Dom,
5274    callbacks::{RefAny, LayoutCallbackInfo},
5275    window::WindowCreateOptions,
5276};
5277
5278struct Data { }
5279
5280extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> Dom {
5281    crate::ui::render()
5282}
5283
5284fn main() {
5285    let config = AppConfig::create();
5286    let app = App::create(RefAny::new(Data { }), config);
5287    let window = WindowCreateOptions::create(render);
5288    app.run(window);
5289}";
5290
5291    let ui_module = format!(
5292        "#[allow(unused_imports)]\r\npub mod ui {{
5293
5294    use azul::prelude::*;
5295    use azul::dom::{{NodeType, TabIndex, SmallAriaInfo}};
5296    use azul::str::String as AzString;
5297
5298    pub fn render() -> Dom {{\r\n{app_source}\r\n    }}\r\n}}"
5299    );
5300    let source_code = format!(
5301        "#![windows_subsystem = \"windows\"]\r\n//! Auto-generated UI source \
5302         code\r\n{}\r\n{}\r\n\r\n{}{}",
5303        imports,
5304        compile_components(Vec::new()), // no user-defined components to compile
5305        ui_module,
5306        main_func,
5307    );
5308
5309    Ok(source_code)
5310}
5311
5312// Compile all components to source code
5313#[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
5314fn compile_components(
5315    components: Vec<(
5316        ComponentName,
5317        CompiledComponent,
5318        ComponentArguments,
5319        BTreeMap<String, String>,
5320    )>,
5321) -> String {
5322    let cs = components
5323        .iter()
5324        .map(|(name, function_body, function_args, css_blocks)| {
5325            let name = &normalize_casing(name);
5326            let f = compile_component(name, function_args, function_body)
5327                .lines()
5328                .map(|l| format!("    {l}"))
5329                .collect::<Vec<String>>()
5330                .join("\r\n");
5331
5332            // let css_blocks = ...
5333
5334            format!(
5335                "#[allow(unused_imports)]\r\npub mod {name} {{\r\n    use azul::dom::Dom;\r\n    use \
5336                 azul::str::String as AzString;\r\n{f}\r\n}}"
5337            )
5338        })
5339        .collect::<Vec<String>>()
5340        .join("\r\n\r\n");
5341
5342    let cs = cs
5343        .lines()
5344        .map(|l| format!("    {l}"))
5345        .collect::<Vec<String>>()
5346        .join("\r\n");
5347
5348    if cs.is_empty() {
5349        cs
5350    } else {
5351        format!("pub mod components {{\r\n{cs}\r\n}}")
5352    }
5353}
5354
5355fn format_component_args(component_args: &ComponentArgumentVec) -> String {
5356    let mut args = component_args
5357        .iter()
5358        .map(|a| format!("{}: {}", a.name, a.arg_type))
5359        .collect::<Vec<String>>();
5360
5361    args.sort_by(|a, b| b.cmp(a));
5362
5363    args.join(", ")
5364}
5365
5366#[must_use] pub fn compile_component(
5367    component_name: &str,
5368    component_args: &ComponentArguments,
5369    component_function_body: &str,
5370) -> String {
5371    let component_name = &normalize_casing(component_name);
5372    let function_args = format_component_args(&component_args.args);
5373    let component_function_body = component_function_body
5374        .lines()
5375        .map(|l| format!("    {l}"))
5376        .collect::<Vec<String>>()
5377        .join("\r\n");
5378    let should_inline = component_function_body.lines().count() == 1;
5379    format!(
5380        "{}pub fn render({}{}{}) -> Dom {{\r\n{}\r\n}}",
5381        if should_inline { "#[inline]\r\n" } else { "" },
5382        // pass the text content as the first
5383        if component_args.accepts_text {
5384            "text: AzString"
5385        } else {
5386            ""
5387        },
5388        if function_args.is_empty() || !component_args.accepts_text {
5389            ""
5390        } else {
5391            ", "
5392        },
5393        function_args,
5394        component_function_body,
5395    )
5396}
5397
5398/// Parse an SVG numeric attribute value to f32.
5399fn parse_svg_float(attr: Option<&AzString>) -> Option<f32> {
5400    attr?.as_str().trim().parse::<f32>().ok()
5401}
5402
5403/// Parse an SVG `points` attribute (used by `<polygon>` and `<polyline>`).
5404fn parse_svg_points(pts: &str, close: bool) -> Option<crate::svg::SvgMultiPolygon> {
5405    let nums: Vec<f32> = pts
5406        .split(|c: char| c == ',' || c.is_ascii_whitespace())
5407        .filter(|s| !s.is_empty())
5408        .filter_map(|s| s.parse::<f32>().ok())
5409        .collect();
5410    if nums.len() < 4 || !nums.len().is_multiple_of(2) {
5411        return None;
5412    }
5413    let mut elements = Vec::new();
5414    let points: Vec<azul_css::props::basic::SvgPoint> = nums
5415        .chunks_exact(2)
5416        .map(|c| azul_css::props::basic::SvgPoint { x: c[0], y: c[1] })
5417        .collect();
5418    for w in points.windows(2) {
5419        elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5420            w[0], w[1],
5421        )));
5422    }
5423    if close && points.len() >= 2 {
5424        let first = points[0];
5425        let last = *points.last().unwrap();
5426        if (first.x - last.x).abs() > 0.001 || (first.y - last.y).abs() > 0.001 {
5427            elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5428                last, first,
5429            )));
5430        }
5431    }
5432    Some(crate::svg::SvgMultiPolygon {
5433        rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5434            items: crate::svg::SvgPathElementVec::from_vec(elements),
5435        }]),
5436    })
5437}
5438
5439/// Fast XML to Dom conversion that builds Dom tree directly without intermediate `StyledDom`
5440/// This is O(n) instead of O(n²) for large documents
5441/// Apply the shared set of XML attributes onto a single [`NodeData`] node.
5442///
5443/// Handles `<img src>` rebuild, `id`/`class`, `focusable`, `tabindex`, inline
5444/// `style`, and SVG-shape geometry — the block that was previously duplicated
5445/// verbatim between [`xml_node_to_dom_fast`] (operating on `dom.root`) and
5446/// [`xml_node_to_fast_dom`] (operating on the arena `NodeData`). `component_name`
5447/// must already be normalized (lowercased); the caller computes `child_inside_svg`.
5448#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
5449fn apply_xml_node_attributes(
5450    node: &mut crate::dom::NodeData,
5451    xml_node: &XmlNode,
5452    component_name: &str,
5453    inside_svg: bool,
5454) {
5455    use crate::dom::{IdOrClass, NodeType, TabIndex};
5456
5457    // `<img src="...">`: rebuild the placeholder Image node so its `NullImage`
5458    // carries the `src` string (as UTF-8 bytes in `tag`). The bytes are NOT
5459    // resolved here — a downstream renderer (printpdf, the compositor, ...) uses
5460    // the tag to look up and embed the actual image. Optional `width`/`height`
5461    // attributes set the intrinsic size used for layout (CSS still overrides).
5462    if component_name == "img" {
5463        if let Some(src) = xml_node.attributes.get_key("src") {
5464            let width = xml_node
5465                .attributes
5466                .get_key("width")
5467                .and_then(|w| {
5468                    w.as_str()
5469                        .trim()
5470                        .trim_end_matches("px")
5471                        .trim()
5472                        .parse::<usize>()
5473                        .ok()
5474                })
5475                .unwrap_or(0);
5476            let height = xml_node
5477                .attributes
5478                .get_key("height")
5479                .and_then(|h| {
5480                    h.as_str()
5481                        .trim()
5482                        .trim_end_matches("px")
5483                        .trim()
5484                        .parse::<usize>()
5485                        .ok()
5486                })
5487                .unwrap_or(0);
5488            let image_ref = crate::resources::ImageRef::null_image(
5489                width,
5490                height,
5491                crate::resources::RawImageFormat::RGBA8,
5492                src.as_str().as_bytes().to_vec(),
5493            );
5494            node
5495                .set_node_type(NodeType::Image(azul_css::css::BoxOrStatic::heap(image_ref)));
5496        }
5497    }
5498
5499    // Set id and class attributes
5500    let mut ids_and_classes = Vec::new();
5501    if let Some(id_str) = xml_node.attributes.get_key("id") {
5502        for id in id_str.split_whitespace() {
5503            ids_and_classes.push(IdOrClass::Id(id.into()));
5504        }
5505    }
5506    if let Some(class_str) = xml_node.attributes.get_key("class") {
5507        for class in class_str.split_whitespace() {
5508            ids_and_classes.push(IdOrClass::Class(class.into()));
5509        }
5510    }
5511    if !ids_and_classes.is_empty() {
5512        node.set_ids_and_classes(ids_and_classes.into());
5513    }
5514
5515    // Handle focusable attribute
5516    if let Some(focusable) = xml_node
5517        .attributes
5518        .get_key("focusable")
5519        .and_then(|f| parse_bool(f.as_str()))
5520    {
5521        if focusable { node.set_tab_index(TabIndex::Auto) } else { node.set_tab_index(TabIndex::NoKeyboardFocus) }
5522    }
5523
5524    // Handle tabindex attribute
5525    if let Some(tab_index) = xml_node
5526        .attributes
5527        .get_key("tabindex")
5528        .and_then(|val| val.parse::<isize>().ok())
5529    {
5530        match tab_index {
5531            0 => node.set_tab_index(TabIndex::Auto),
5532            i if i > 0 => node.set_tab_index(TabIndex::OverrideInParent(u32::try_from(i).unwrap_or(u32::MAX))),
5533            _ => node.set_tab_index(TabIndex::NoKeyboardFocus),
5534        }
5535    }
5536
5537    // Table cell span attributes (`colspan` / `rowspan`).
5538    apply_cell_span_attributes(node, xml_node);
5539
5540    // HTML `dir` attribute → the `direction` CSS property (dir="rtl"/"ltr"). Without
5541    // this, dir="rtl" (the common way to set RTL in HTML) had no effect. Appended
5542    // BEFORE the inline `style` below so author style still wins on equal specificity.
5543    let dir_prop = xml_node.attributes.get_key("dir").and_then(|d| {
5544        let v = d.as_str().trim();
5545        if v.eq_ignore_ascii_case("rtl") {
5546            Some(azul_css::props::style::StyleDirection::Rtl)
5547        } else if v.eq_ignore_ascii_case("ltr") {
5548            Some(azul_css::props::style::StyleDirection::Ltr)
5549        } else {
5550            None
5551        }
5552    });
5553
5554    // Handle inline style attribute (and the mapped `dir` attribute above)
5555    let style_attr = xml_node.attributes.get_key("style");
5556    if style_attr.is_some() || dir_prop.is_some() {
5557        use azul_css::dynamic_selector::CssPropertyWithConditions;
5558        let css_key_map = azul_css::props::property::get_css_key_map();
5559        let mut props: Vec<CssPropertyWithConditions> = Vec::new();
5560        if let Some(dir) = dir_prop {
5561            props.push(CssPropertyWithConditions::simple(
5562                azul_css::props::property::CssProperty::Direction(
5563                    azul_css::css::CssPropertyValue::Exact(dir),
5564                ),
5565            ));
5566        }
5567        if let Some(style) = style_attr {
5568            let mut attributes = Vec::new();
5569            for s in style.as_str().split(';') {
5570                let mut s = s.split(':');
5571                let Some(key) = s.next() else {
5572                    continue;
5573                };
5574                let Some(value) = s.next() else {
5575                    continue;
5576                };
5577                // Called for its side effect (writes parsed props into `attributes`);
5578                // the returned value is intentionally discarded.
5579                drop(azul_css::parser2::parse_css_declaration(
5580                    key.trim(),
5581                    value.trim(),
5582                    azul_css::parser2::ErrorLocationRange::default(),
5583                    &css_key_map,
5584                    &mut Vec::new(),
5585                    &mut attributes,
5586                ));
5587            }
5588            props.extend(attributes.into_iter().filter_map(|s| match s {
5589                CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
5590                CssDeclaration::Dynamic(_) => None,
5591            }));
5592        }
5593        if !props.is_empty() {
5594            node.set_css_props(props.into());
5595        }
5596    }
5597
5598    // Handle SVG shape elements when inside an <svg> context
5599    let tag = component_name;
5600    let is_svg_shape = inside_svg
5601        && matches!(
5602            tag,
5603            "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline"
5604        );
5605
5606    if is_svg_shape {
5607        let clip = match tag {
5608            "path" => xml_node
5609                .attributes
5610                .get_key("d")
5611                .and_then(|d| crate::path_parser::parse_svg_path_d(d.as_str()).ok()),
5612            "circle" => {
5613                let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
5614                let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
5615                let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
5616                if r > 0.0 {
5617                    Some(crate::svg::SvgMultiPolygon {
5618                        rings: crate::svg::SvgPathVec::from_vec(vec![
5619                            crate::path_parser::svg_circle_to_paths(cx, cy, r),
5620                        ]),
5621                    })
5622                } else {
5623                    None
5624                }
5625            }
5626            "rect" => {
5627                let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
5628                let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
5629                let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
5630                let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
5631                let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
5632                let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
5633                if w > 0.0 && h > 0.0 {
5634                    Some(crate::svg::SvgMultiPolygon {
5635                        rings: crate::svg::SvgPathVec::from_vec(vec![
5636                            crate::path_parser::svg_rect_to_path(x, y, w, h, rx, ry),
5637                        ]),
5638                    })
5639                } else {
5640                    None
5641                }
5642            }
5643            "ellipse" => {
5644                let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
5645                let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
5646                let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
5647                let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
5648                if rx > 0.0 && ry > 0.0 {
5649                    // Approximate ellipse with 4 cubic beziers (using rx for x-kappa, ry for y-kappa)
5650                    use azul_css::props::basic::{SvgCubicCurve, SvgPoint};
5651                    const KAPPA: f32 = 0.552_284_8;
5652                    let kx = rx * KAPPA;
5653                    let ky = ry * KAPPA;
5654                    let elements = vec![
5655                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5656                            start: SvgPoint { x: cx, y: cy - ry },
5657                            ctrl_1: SvgPoint {
5658                                x: cx + kx,
5659                                y: cy - ry,
5660                            },
5661                            ctrl_2: SvgPoint {
5662                                x: cx + rx,
5663                                y: cy - ky,
5664                            },
5665                            end: SvgPoint { x: cx + rx, y: cy },
5666                        }),
5667                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5668                            start: SvgPoint { x: cx + rx, y: cy },
5669                            ctrl_1: SvgPoint {
5670                                x: cx + rx,
5671                                y: cy + ky,
5672                            },
5673                            ctrl_2: SvgPoint {
5674                                x: cx + kx,
5675                                y: cy + ry,
5676                            },
5677                            end: SvgPoint { x: cx, y: cy + ry },
5678                        }),
5679                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5680                            start: SvgPoint { x: cx, y: cy + ry },
5681                            ctrl_1: SvgPoint {
5682                                x: cx - kx,
5683                                y: cy + ry,
5684                            },
5685                            ctrl_2: SvgPoint {
5686                                x: cx - rx,
5687                                y: cy + ky,
5688                            },
5689                            end: SvgPoint { x: cx - rx, y: cy },
5690                        }),
5691                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5692                            start: SvgPoint { x: cx - rx, y: cy },
5693                            ctrl_1: SvgPoint {
5694                                x: cx - rx,
5695                                y: cy - ky,
5696                            },
5697                            ctrl_2: SvgPoint {
5698                                x: cx - kx,
5699                                y: cy - ry,
5700                            },
5701                            end: SvgPoint { x: cx, y: cy - ry },
5702                        }),
5703                    ];
5704                    Some(crate::svg::SvgMultiPolygon {
5705                        rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5706                            items: crate::svg::SvgPathElementVec::from_vec(elements),
5707                        }]),
5708                    })
5709                } else {
5710                    None
5711                }
5712            }
5713            "line" => {
5714                let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
5715                let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
5716                let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
5717                let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
5718                Some(crate::svg::SvgMultiPolygon {
5719                    rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5720                        items: crate::svg::SvgPathElementVec::from_vec(vec![
5721                            crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5722                                azul_css::props::basic::SvgPoint { x: x1, y: y1 },
5723                                azul_css::props::basic::SvgPoint { x: x2, y: y2 },
5724                            )),
5725                        ]),
5726                    }]),
5727                })
5728            }
5729            "polygon" | "polyline" => xml_node
5730                .attributes
5731                .get_key("points")
5732                .and_then(|pts| parse_svg_points(pts.as_str(), tag == "polygon")),
5733            _ => None,
5734        };
5735
5736        if let Some(mp) = clip {
5737            node.set_svg_data(crate::dom::SvgNodeData::Path(mp));
5738        }
5739    }
5740}
5741
5742/// Parse the HTML `colspan` / `rowspan` presentational attributes into
5743/// `AttributeType`s on the node. The table layout reads them back via
5744/// `get_cell_spans`. Without this the XML→DOM conversion dropped them and every
5745/// cell defaulted to span 1, so `<th colspan="2">` only covered one column.
5746/// Parsed unconditionally — non-cell elements simply don't carry these attributes.
5747fn apply_cell_span_attributes(node: &mut crate::dom::NodeData, xml_node: &XmlNode) {
5748    let mut spans = Vec::new();
5749    if let Some(n) = xml_node
5750        .attributes
5751        .get_key("colspan")
5752        .and_then(|v| v.as_str().trim().parse::<i32>().ok())
5753    {
5754        spans.push(crate::dom::AttributeType::ColSpan(n));
5755    }
5756    if let Some(n) = xml_node
5757        .attributes
5758        .get_key("rowspan")
5759        .and_then(|v| v.as_str().trim().parse::<i32>().ok())
5760    {
5761        spans.push(crate::dom::AttributeType::RowSpan(n));
5762    }
5763    if !spans.is_empty() {
5764        let mut v = node.attributes().clone().into_library_owned_vec();
5765        v.extend(spans);
5766        node.set_attributes(v.into());
5767    }
5768}
5769
5770#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5771// component_map is threaded through the whole fast-DOM pipeline for parity with the
5772// component-expanding interpreter path (see ~xml.rs:2845); this fast path never expands
5773// components, so it only forwards the map into recursive calls. Removing it here would
5774// cascade unused-param removals up the entire pipeline.
5775#[allow(clippy::only_used_in_recursion)]
5776fn xml_node_to_dom_fast<'a>(
5777    xml_node: &'a XmlNode,
5778    component_map: &'a ComponentMap,
5779    inside_svg: bool,
5780    depth: usize,
5781) -> Result<Dom, RenderDomError> {
5782    use crate::dom::Dom;
5783
5784    let component_name = normalize_casing(&xml_node.node_type);
5785
5786    // Look up the component definition
5787    let node_type = tag_to_node_type(&component_name);
5788    let mut dom = Dom::create_node(node_type);
5789
5790    apply_xml_node_attributes(&mut dom.root, xml_node, &component_name, inside_svg);
5791
5792    let child_inside_svg = inside_svg || component_name == "svg";
5793
5794    // AUDIT 2026-07-08: bound recursion depth to avoid a native stack overflow on
5795    // pathologically deep markup. At the cap, this node is emitted without its
5796    // children (truncation) rather than crashing the process.
5797    // AUDIT-TODO: a worklist-based iterative builder would preserve deep subtrees.
5798    if depth >= MAX_XML_NESTING_DEPTH {
5799        return Ok(dom);
5800    }
5801
5802    // Recursively convert children
5803    let mut children = Vec::new();
5804    for child in xml_node.children.as_ref() {
5805        match child {
5806            XmlNodeChild::Element(child_node) => {
5807                let child_dom =
5808                    xml_node_to_dom_fast(child_node, component_map, child_inside_svg, depth + 1)?;
5809                children.push(child_dom);
5810            }
5811            XmlNodeChild::Text(text) => {
5812                let text_dom = Dom::create_text(AzString::from(text.as_str()));
5813                children.push(text_dom);
5814            }
5815        }
5816    }
5817
5818    if !children.is_empty() {
5819        dom = dom.with_children(children.into());
5820    }
5821
5822    Ok(dom)
5823}
5824
5825/// Builder for arena-based DOM construction (`FastDom`).
5826/// Builds two parallel Vecs (hierarchy + `node_data`) in a single DFS pass.
5827#[derive(Debug)]
5828pub struct CompactDomBuilder {
5829    hierarchy: Vec<crate::styled_dom::NodeHierarchyItem>,
5830    node_data: Vec<crate::dom::NodeData>,
5831    css: Vec<crate::dom::CssWithNodeId>,
5832    /// Stack of (`node_index`, `previous_child_index`) for open elements
5833    stack: Vec<(usize, Option<usize>)>,
5834}
5835
5836impl Default for CompactDomBuilder {
5837    fn default() -> Self {
5838        Self::new()
5839    }
5840}
5841
5842impl CompactDomBuilder {
5843    #[must_use] pub const fn new() -> Self {
5844        Self {
5845            hierarchy: Vec::new(),
5846            node_data: Vec::new(),
5847            css: Vec::new(),
5848            stack: Vec::new(),
5849        }
5850    }
5851
5852    #[must_use] pub fn with_capacity(cap: usize) -> Self {
5853        Self {
5854            hierarchy: Vec::with_capacity(cap),
5855            node_data: Vec::with_capacity(cap),
5856            css: Vec::new(),
5857            stack: Vec::new(),
5858        }
5859    }
5860
5861    /// Open a new element node. Must be paired with `close_node()`.
5862    pub fn open_node(&mut self, node_data: crate::dom::NodeData) {
5863        use crate::id::NodeId;
5864        use crate::styled_dom::NodeHierarchyItem;
5865
5866        let idx = self.hierarchy.len();
5867
5868        // Determine parent from stack
5869        let parent_raw = if let Some(&(parent_idx, _)) = self.stack.last() {
5870            NodeId::into_raw(&Some(NodeId::new(parent_idx)))
5871        } else {
5872            0 // No parent (root)
5873        };
5874
5875        // Determine previous sibling from parent's last child tracking
5876        let prev_sibling_raw = if let Some(&(_, prev_child)) = self.stack.last() {
5877            prev_child
5878                .map_or(0, |pi| NodeId::into_raw(&Some(NodeId::new(pi))))
5879        } else {
5880            0
5881        };
5882
5883        // If there's a previous sibling, set its next_sibling to us
5884        if let Some(&(_, Some(prev_idx))) = self.stack.last() {
5885            self.hierarchy[prev_idx].next_sibling = NodeId::into_raw(&Some(NodeId::new(idx)));
5886        }
5887
5888        // Update parent's "last seen child" to us
5889        if let Some(parent) = self.stack.last_mut() {
5890            parent.1 = Some(idx);
5891        }
5892
5893        // Push the hierarchy item (last_child will be set in close_node)
5894        self.hierarchy.push(NodeHierarchyItem {
5895            parent: parent_raw,
5896            previous_sibling: prev_sibling_raw,
5897            next_sibling: 0, // Will be set by next sibling's open_node
5898            last_child: 0,   // Will be set in close_node
5899        });
5900        self.node_data.push(node_data);
5901
5902        // Push onto stack: this node is now the "open" element, no children yet
5903        self.stack.push((idx, None));
5904    }
5905
5906    /// Close the current element. Sets the `last_child` pointer.
5907    pub fn close_node(&mut self) {
5908        use crate::id::NodeId;
5909
5910        if let Some((idx, last_child_idx)) = self.stack.pop() {
5911            // Set last_child on this node's hierarchy item
5912            self.hierarchy[idx].last_child = last_child_idx
5913                .map_or(0, |lc| NodeId::into_raw(&Some(NodeId::new(lc))));
5914        }
5915    }
5916
5917    /// Add a leaf node (text, br, hr, etc.) that has no children.
5918    pub fn add_leaf(&mut self, node_data: crate::dom::NodeData) {
5919        self.open_node(node_data);
5920        self.close_node();
5921    }
5922
5923    /// Add a CSS stylesheet scoped to a node ID.
5924    pub fn add_css(&mut self, node_id: usize, css: Css) {
5925        self.css.push(crate::dom::CssWithNodeId { node_id, css });
5926    }
5927
5928    /// Finish building and produce a `FastDom`.
5929    #[must_use] pub fn finish(self) -> crate::dom::FastDom {
5930        crate::dom::FastDom {
5931            node_hierarchy: self.hierarchy.into(),
5932            node_data: self.node_data.into(),
5933            css: self.css.into(),
5934        }
5935    }
5936}
5937
5938/// Convert an XML node tree into a `FastDom` (arena-based) in a single DFS pass.
5939/// This is the fast path equivalent of `xml_node_to_dom_fast`.
5940#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5941// See xml_node_to_dom_fast: component_map is forwarded for pipeline parity, not read here.
5942#[allow(clippy::only_used_in_recursion)]
5943fn xml_node_to_fast_dom<'a>(
5944    xml_node: &'a XmlNode,
5945    component_map: &'a ComponentMap,
5946    inside_svg: bool,
5947    builder: &mut CompactDomBuilder,
5948    depth: usize,
5949) -> Result<(), RenderDomError> {
5950    use crate::dom::NodeData;
5951
5952    let component_name = normalize_casing(&xml_node.node_type);
5953    let node_type = tag_to_node_type(&component_name);
5954    let mut node_data = NodeData::create_node(node_type);
5955
5956    apply_xml_node_attributes(&mut node_data, xml_node, &component_name, inside_svg);
5957
5958    let child_inside_svg = inside_svg || component_name == "svg";
5959
5960    // Open this node in the builder
5961    builder.open_node(node_data);
5962
5963    // AUDIT 2026-07-08: bound recursion depth to avoid a native stack overflow on
5964    // pathologically deep markup. At the cap, children are dropped (the node is
5965    // still opened+closed) rather than crashing the process.
5966    // AUDIT-TODO: a worklist-based iterative builder would preserve deep subtrees.
5967    if depth < MAX_XML_NESTING_DEPTH {
5968        // Recursively convert children
5969        for child in xml_node.children.as_ref() {
5970            match child {
5971                XmlNodeChild::Element(child_node) => {
5972                    xml_node_to_fast_dom(
5973                        child_node,
5974                        component_map,
5975                        child_inside_svg,
5976                        builder,
5977                        depth + 1,
5978                    )?;
5979                }
5980                XmlNodeChild::Text(text) => {
5981                    builder.add_leaf(NodeData::create_text(AzString::from(text.as_str())));
5982                }
5983            }
5984        }
5985    }
5986
5987    // Close this node
5988    builder.close_node();
5989
5990    Ok(())
5991}
5992
5993/// Render a DOM from an XML body node using the fast arena-based path.
5994/// Builds a `FastDom` directly (no tree intermediary), then creates `StyledDom`.
5995#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5996fn render_dom_from_body_node_fast<'a>(
5997    body_node: &'a XmlNode,
5998    mut global_css: Option<Css>,
5999    component_map: &'a ComponentMap,
6000    max_width: Option<f32>,
6001) -> Result<StyledDom, RenderDomError> {
6002    use crate::dom::{NodeData, NodeType};
6003
6004    let mut builder = CompactDomBuilder::new();
6005
6006    // Build the HTML > Body wrapper + body content in one pass
6007    // Open <html>
6008    builder.open_node(NodeData::create_node(NodeType::Html));
6009    // Open <body> (the body_node content goes inside)
6010    xml_node_to_fast_dom(body_node, component_map, false, &mut builder, 0)?;
6011    // Close <html>
6012    builder.close_node();
6013
6014    // Collect CSS rules from each source.
6015    let mut combined_rules: Vec<CssRuleBlock> = Vec::new();
6016    if let Some(max_width) = max_width {
6017        let max_width_css =
6018            Css::from_string(format!("html {{ max-width: {max_width}px; }}").into());
6019        combined_rules.extend(max_width_css.rules.into_library_owned_vec());
6020    }
6021    if let Some(css) = global_css.take() {
6022        combined_rules.extend(css.rules.into_library_owned_vec());
6023    }
6024    let combined_css = Css::new(combined_rules);
6025
6026    // Add CSS to the FastDom
6027    let mut fast_dom = builder.finish();
6028    fast_dom.css = vec![crate::dom::CssWithNodeId {
6029        node_id: 0, // Global scope (root)
6030        css: combined_css,
6031    }]
6032    .into();
6033
6034    // Create StyledDom via the fast path (no tree→arena conversion)
6035    let styled = StyledDom::create_from_fast_dom(fast_dom);
6036    Ok(styled)
6037}
6038
6039// render_dom_from_body_node() removed — use render_dom_from_body_node_fast() or str_to_dom()
6040
6041fn set_stringified_attributes(
6042    dom_string: &mut String,
6043    xml_attributes: &XmlAttributeMap,
6044    filtered_xml_attributes: &ComponentArgumentVec,
6045    tabs: usize,
6046) {
6047    let t0 = String::from("    ").repeat(tabs);
6048    let t = String::from("    ").repeat(tabs + 1);
6049
6050    // push ids and classes as chained `.with_id("..")` / `.with_class("..")`
6051    // calls (public builder API; both take `Into<AzString>`, so bare &str works).
6052    let _ = &t;
6053    for id in xml_attributes
6054        .get_key("id")
6055        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6056        .unwrap_or_default()
6057    {
6058        let _ = write!(
6059            dom_string,
6060            "\r\n{}.with_id(\"{}\")",
6061            t0,
6062            format_args_dynamic(id, filtered_xml_attributes)
6063        );
6064    }
6065
6066    for class in xml_attributes
6067        .get_key("class")
6068        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6069        .unwrap_or_default()
6070    {
6071        let _ = write!(
6072            dom_string,
6073            "\r\n{}.with_class(\"{}\")",
6074            t0,
6075            format_args_dynamic(class, filtered_xml_attributes)
6076        );
6077    }
6078
6079    if let Some(focusable) = xml_attributes
6080        .get_key("focusable")
6081        .map(|f| format_args_dynamic(f, filtered_xml_attributes))
6082        .and_then(|f| parse_bool(&f))
6083    {
6084        if focusable { let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)"); } else { let _ = write!(dom_string,
6085            "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6086        ); }
6087    }
6088
6089    if let Some(tab_index) = xml_attributes
6090        .get_key("tabindex")
6091        .map(|val| format_args_dynamic(val, filtered_xml_attributes))
6092        .and_then(|val| val.parse::<isize>().ok())
6093    {
6094        match tab_index {
6095            0 => { let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)"); },
6096            i if i > 0 => { let _ = write!(dom_string,
6097                "\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
6098                t, usize::try_from(i).unwrap_or(0)
6099            ); },
6100            _ => { let _ = write!(dom_string,
6101                "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6102            ); },
6103        }
6104    }
6105}
6106
6107/// Item of a split string - either a variable name (with optional format spec) or a string
6108#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6109pub enum DynamicItem {
6110    /// A variable reference, e.g. {counter} or {counter:?} or {price:.2}
6111    Var {
6112        name: String,
6113        /// Optional format specifier after the colon: "?" for debug, ".2" for precision, etc.
6114        format_spec: Option<String>,
6115    },
6116    Str(String),
6117}
6118
6119/// Splits a string into formatting arguments, supporting format specifiers like `{var:?}`
6120/// ```rust
6121/// # use azul_core::xml::DynamicItem::*;
6122/// # use azul_core::xml::split_dynamic_string;
6123/// let s = "hello {a}, {b}{{ {c} }}";
6124/// let split = split_dynamic_string(s);
6125/// let output = vec![
6126///     Str("hello ".to_string()),
6127///     Var { name: "a".to_string(), format_spec: None },
6128///     Str(", ".to_string()),
6129///     Var { name: "b".to_string(), format_spec: None },
6130///     Str("{ ".to_string()),
6131///     Var { name: "c".to_string(), format_spec: None },
6132///     Str(" }".to_string()),
6133/// ];
6134/// assert_eq!(output, split);
6135/// ```
6136#[must_use] pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
6137    use self::DynamicItem::{Str, Var};
6138
6139    let input: Vec<char> = input.chars().collect();
6140    let input_chars_len = input.len();
6141
6142    let mut items = Vec::new();
6143    let mut current_idx = 0;
6144    let mut last_idx = 0;
6145
6146    while current_idx < input_chars_len {
6147        let c = input[current_idx];
6148        match c {
6149            '{' if input.get(current_idx + 1).copied() != Some('{') => {
6150                // variable start, search until next closing brace or whitespace or end of string
6151                let mut start_offset = 1;
6152                let mut has_found_variable = false;
6153                while let Some(c) = input.get(current_idx + start_offset) {
6154                    if c.is_whitespace() {
6155                        break;
6156                    }
6157                    if *c == '}' && input.get(current_idx + start_offset + 1).copied() != Some('}')
6158                    {
6159                        start_offset += 1;
6160                        has_found_variable = true;
6161                        break;
6162                    }
6163                    start_offset += 1;
6164                }
6165
6166                // advance current_idx accordingly
6167                // on fail, set cursor to end
6168                // set last_idx accordingly
6169                if has_found_variable {
6170                    if last_idx != current_idx {
6171                        items.push(Str(input[last_idx..current_idx].iter().collect()));
6172                    }
6173
6174                    // subtract 1 from start for opening brace, one from end for closing brace
6175                    let var_content: String = input
6176                        [(current_idx + 1)..(current_idx + start_offset - 1)]
6177                        .iter()
6178                        .collect();
6179                    // Split on first ':' to separate variable name from format specifier
6180                    let (var_name, format_spec) = if let Some(colon_pos) = var_content.find(':') {
6181                        let name = var_content[..colon_pos].to_string();
6182                        let spec = var_content[(colon_pos + 1)..].to_string();
6183                        (name, Some(spec))
6184                    } else {
6185                        (var_content, None)
6186                    };
6187                    items.push(Var {
6188                        name: var_name,
6189                        format_spec,
6190                    });
6191                    current_idx += start_offset;
6192                    last_idx = current_idx;
6193                } else {
6194                    current_idx += start_offset;
6195                }
6196            }
6197            _ => {
6198                current_idx += 1;
6199            }
6200        }
6201    }
6202
6203    if current_idx != last_idx {
6204        items.push(Str(input[last_idx..].iter().collect()));
6205    }
6206
6207    for item in &mut items {
6208        // replace {{ with { in strings
6209        if let Str(s) = item {
6210            *s = s.replace("{{", "{").replace("}}", "}");
6211        }
6212    }
6213
6214    items
6215}
6216
6217/// Combines the split string back into its original form while replacing the variables with their
6218/// values
6219///
6220/// let variables = btreemap!{ "a" => "value1", "b" => "value2" };
6221/// [Str("hello "), Var("a"), Str(", "), Var("b"), Str("{ "), Var("c"), Str(" }}")]
6222/// => "hello value1, valuec{ {c} }"
6223fn combine_and_replace_dynamic_items(
6224    input: &[DynamicItem],
6225    variables: &ComponentArgumentVec,
6226) -> String {
6227    let mut s = String::new();
6228
6229    for item in input {
6230        match item {
6231            DynamicItem::Var { name, format_spec } => {
6232                let variable_name = normalize_casing(name.trim());
6233                if let Some(resolved_var) = variables
6234                    .iter()
6235                    .find(|s| s.name.as_str() == variable_name)
6236                    .map(|q| &q.arg_type) {
6237                    // Format specifiers are applied at compile time, not at runtime replacement
6238                    s.push_str(resolved_var);
6239                } else {
6240                    s.push('{');
6241                    s.push_str(name);
6242                    if let Some(spec) = format_spec {
6243                        s.push(':');
6244                        s.push_str(spec);
6245                    }
6246                    s.push('}');
6247                }
6248            }
6249            DynamicItem::Str(dynamic_str) => {
6250                s.push_str(dynamic_str);
6251            }
6252        }
6253    }
6254
6255    s
6256}
6257
6258/// Given a string and a key => value mapping, replaces parts of the string with the value, i.e.:
6259///
6260/// ```rust
6261/// # use azul_core::xml::{format_args_dynamic, ComponentArgument, ComponentArgumentVec};
6262/// # use azul_css::AzString;
6263/// let variables: ComponentArgumentVec = vec![
6264///     ComponentArgument { name: AzString::from("a"), arg_type: AzString::from("value1") },
6265///     ComponentArgument { name: AzString::from("b"), arg_type: AzString::from("value2") },
6266/// ].into();
6267///
6268/// let initial = "hello {a}, {b}{{ {c} }}";
6269/// let expected = "hello value1, value2{ {c} }".to_string();
6270/// assert_eq!(format_args_dynamic(initial, &variables), expected);
6271/// ```
6272///
6273/// Note: the number (0, 1, etc.) is the order of the argument, it is irrelevant for
6274/// runtime formatting, only important for keeping the component / function arguments
6275/// in order when compiling the arguments to Rust code
6276#[must_use] pub fn format_args_dynamic(input: &str, variables: &ComponentArgumentVec) -> String {
6277    let dynamic_str_items = split_dynamic_string(input);
6278    combine_and_replace_dynamic_items(&dynamic_str_items, variables)
6279}
6280
6281/// Decode a numeric character reference body (the part between `&` and `;`),
6282/// e.g. `"#65"` -> `'A'`, `"#x41"` -> `'A'`. Returns `None` if it is not a valid
6283/// numeric reference.
6284fn decode_numeric_entity(entity: &str) -> Option<char> {
6285    let num = entity.strip_prefix('#')?;
6286    let code = if let Some(hex) = num.strip_prefix(['x', 'X']) {
6287        u32::from_str_radix(hex, 16).ok()?
6288    } else {
6289        num.parse::<u32>().ok()?
6290    };
6291    char::from_u32(code)
6292}
6293
6294/// Decode the common HTML/XML entities in a single left-to-right pass.
6295///
6296/// Handles `&lt;` `&gt;` `&amp;` `&quot;` `&apos;` and numeric references
6297/// (`&#NN;` / `&#xHH;`). `&nbsp;` and any unrecognized `&...;` sequence are left
6298/// verbatim. The single pass guarantees `&amp;` never double-decodes a following
6299/// entity. See [`prepare_string`] for why `&nbsp;` is deliberately preserved.
6300fn decode_entities(input: &str) -> String {
6301    // Longest handled entity body is a hex numeric ref like `#x10FFFF` (8 bytes);
6302    // cap the `;` search window so a stray `&` far from a `;` stays cheap.
6303    const MAX_ENTITY_BODY: usize = 12;
6304
6305    let mut out = String::with_capacity(input.len());
6306    let bytes = input.as_bytes();
6307    let mut i = 0;
6308    while i < input.len() {
6309        if bytes[i] == b'&' {
6310            if let Some(semi_rel) = input[i + 1..].find(';') {
6311                if semi_rel <= MAX_ENTITY_BODY {
6312                    let body = &input[i + 1..i + 1 + semi_rel];
6313                    let end = i + 1 + semi_rel; // index of ';'
6314                    // Leave &nbsp; for the per-line pass in prepare_string.
6315                    if body.eq_ignore_ascii_case("nbsp") {
6316                        out.push_str(&input[i..=end]);
6317                        i = end + 1;
6318                        continue;
6319                    }
6320                    let decoded = match body {
6321                        "lt" => Some('<'),
6322                        "gt" => Some('>'),
6323                        "amp" => Some('&'),
6324                        "quot" => Some('"'),
6325                        "apos" => Some('\''),
6326                        _ => decode_numeric_entity(body),
6327                    };
6328                    if let Some(c) = decoded {
6329                        out.push(c);
6330                        i = end + 1;
6331                        continue;
6332                    }
6333                }
6334            }
6335            // Not a recognized entity: emit the '&' literally.
6336            out.push('&');
6337            i += 1;
6338        } else {
6339            // Copy one whole UTF-8 char (i is always on a char boundary here).
6340            let ch = input[i..].chars().next().unwrap_or('\u{FFFD}');
6341            out.push(ch);
6342            i += ch.len_utf8();
6343        }
6344    }
6345    out
6346}
6347
6348// NOTE: Two sequential returns count as a single return, while single returns get ignored.
6349#[must_use] pub fn prepare_string(input: &str) -> String {
6350    const SPACE: &str = " ";
6351    const RETURN: &str = "\n";
6352
6353    let input = input.trim();
6354
6355    if input.is_empty() {
6356        return String::new();
6357    }
6358
6359    // AUDIT 2026-07-08: previously only `&lt;`/`&gt;` were decoded. Decode the full
6360    // common named-entity set (`&lt;` `&gt;` `&amp;` `&quot;` `&apos;`) plus numeric
6361    // references (`&#NN;` decimal and `&#xHH;` hex) in a single left-to-right pass.
6362    // A single pass is used deliberately so `&amp;` cannot double-decode a following
6363    // entity (e.g. "&amp;lt;" -> literal "&lt;", not "<"). `&nbsp;` is intentionally
6364    // left untouched here so the per-line pass below (which runs AFTER trimming) can
6365    // still turn it into a space that survives leading/trailing trim.
6366    let input = decode_entities(input);
6367
6368    let input_len = input.len();
6369    let mut final_lines: Vec<String> = Vec::new();
6370    let mut last_line_was_empty = false;
6371
6372    for line in input.lines() {
6373        let line = line.trim();
6374        let line = line.replace("&nbsp;", " ");
6375        let current_line_is_empty = line.is_empty();
6376
6377        if !current_line_is_empty {
6378            if last_line_was_empty {
6379                final_lines.push(format!("{RETURN}{line}"));
6380            } else {
6381                final_lines.push(line.to_string());
6382            }
6383        }
6384
6385        last_line_was_empty = current_line_is_empty;
6386    }
6387
6388    let mut target = String::with_capacity(input_len);
6389    for (line_idx, line) in final_lines.iter().enumerate() {
6390        // A joining space goes before every line EXCEPT the first (idx 0) and a
6391        // paragraph break (RETURN-prefixed). The old code also skipped the LAST line,
6392        // which dropped the word boundary for a soft-wrapped final line
6393        // ("Hello\nworld" -> "Helloworld").
6394        if !(line.starts_with(RETURN) || line_idx == 0) {
6395            target.push_str(SPACE);
6396        }
6397        target.push_str(line);
6398    }
6399    target
6400}
6401
6402/// Parses a string ("true" or "false")
6403#[must_use] pub fn parse_bool(input: &str) -> Option<bool> {
6404    match input {
6405        "true" => Some(true),
6406        "false" => Some(false),
6407        _ => None,
6408    }
6409}
6410
6411#[derive(Debug, Clone)]
6412pub struct CssMatcher {
6413    path: Vec<CssPathSelector>,
6414    indices_in_parent: Vec<usize>,
6415    children_length: Vec<usize>,
6416}
6417
6418impl CssMatcher {
6419    fn get_hash(&self) -> u64 {
6420        use core::hash::Hash;
6421
6422        use core::hash::Hasher;
6423
6424        let mut hasher = crate::hash::DefaultHasher::new();
6425        for p in &self.path {
6426            p.hash(&mut hasher);
6427        }
6428        hasher.finish()
6429    }
6430}
6431
6432impl CssMatcher {
6433    fn matches(&self, path: &CssPath) -> bool {
6434        use azul_css::css::CssPathSelector::*;
6435
6436        use crate::style::{CssGroupIterator, CssGroupSplitReason};
6437
6438        if self.path.is_empty() {
6439            return false;
6440        }
6441        if path.selectors.as_ref().is_empty() {
6442            return false;
6443        }
6444
6445        // self_matcher is only ever going to contain "Children" selectors, never "DirectChildren"
6446        let mut path_groups = CssGroupIterator::new(path.selectors.as_ref()).collect::<Vec<_>>();
6447        path_groups.reverse();
6448
6449        if path_groups.is_empty() {
6450            return false;
6451        }
6452        let mut self_groups = CssGroupIterator::new(self.path.as_ref()).collect::<Vec<_>>();
6453        self_groups.reverse();
6454        if self_groups.is_empty() {
6455            return false;
6456        }
6457
6458        if self.indices_in_parent.len() != self_groups.len() {
6459            return false;
6460        }
6461        if self.children_length.len() != self_groups.len() {
6462            return false;
6463        }
6464
6465        // self_groups = [ // HTML
6466        //     "body",
6467        //     "div.__azul_native-ribbon-container"
6468        //     "div.__azul_native-ribbon-tabs"
6469        //     "p.home"
6470        // ]
6471        //
6472        // path_groups = [ // CSS
6473        //     ".__azul_native-ribbon-tabs"
6474        //     "div.after-tabs"
6475        // ]
6476
6477        // get the first path group and see if it matches anywhere in the self group
6478        let mut cur_selfgroup_scan = 0;
6479        let mut cur_pathgroup_scan = 0;
6480        let mut valid = false;
6481        let mut path_group = path_groups[cur_pathgroup_scan].clone();
6482
6483        while cur_selfgroup_scan < self_groups.len() {
6484            let mut advance = None;
6485
6486            // scan all remaining path groups
6487            for (id, cg) in self_groups[cur_selfgroup_scan..].iter().enumerate() {
6488                let gm = group_matches(
6489                    &path_group.0,
6490                    &self_groups[cur_selfgroup_scan + id].0,
6491                    self.indices_in_parent[cur_selfgroup_scan + id],
6492                    self.children_length[cur_selfgroup_scan + id],
6493                );
6494
6495                if gm {
6496                    // ok: ".__azul_native-ribbon-tabs" was found within self_groups
6497                    // advance the self_groups by n
6498                    advance = Some(id);
6499                    break;
6500                }
6501            }
6502
6503            match advance {
6504                Some(n) => {
6505                    // group was found in remaining items
6506                    // advance cur_pathgroup_scan by 1 and cur_selfgroup_scan by n
6507                    if cur_pathgroup_scan == path_groups.len() - 1 {
6508                        // last path group
6509                        return cur_selfgroup_scan + n == self_groups.len() - 1;
6510                    }
6511                    cur_pathgroup_scan += 1;
6512                    cur_selfgroup_scan += n;
6513                    path_group = path_groups[cur_pathgroup_scan].clone();
6514                }
6515                None => return false, // group was not found in remaining items
6516            }
6517        }
6518
6519        // only return true if all path_groups matched
6520        cur_pathgroup_scan == path_groups.len() - 1
6521    }
6522}
6523
6524// does p.home match div.after-tabs?
6525// a: div.after-tabs
6526fn group_matches(
6527    a: &[&CssPathSelector],
6528    b: &[&CssPathSelector],
6529    idx_in_parent: usize,
6530    parent_children: usize,
6531) -> bool {
6532    use azul_css::css::{CssNthChildSelector, CssPathPseudoSelector, CssPathSelector::{Global, PseudoSelector, Type, Class, Id}};
6533
6534    for selector in a {
6535        match selector {
6536            // always matches
6537            Global |
6538PseudoSelector(CssPathPseudoSelector::Hover | CssPathPseudoSelector::Active |
6539CssPathPseudoSelector::Focus) => {}
6540
6541            Type(tag) => {
6542                if !b.iter().any(|t| **t == Type(*tag)) {
6543                    return false;
6544                }
6545            }
6546            Class(class) => {
6547                if !b.iter().any(|t| **t == Class(class.clone())) {
6548                    return false;
6549                }
6550            }
6551            Id(id) => {
6552                if !b.iter().any(|t| **t == Id(id.clone())) {
6553                    return false;
6554                }
6555            }
6556            PseudoSelector(CssPathPseudoSelector::First) => {
6557                if idx_in_parent != 0 {
6558                    return false;
6559                }
6560            }
6561            PseudoSelector(CssPathPseudoSelector::Last) => {
6562                if idx_in_parent != parent_children.saturating_sub(1) {
6563                    return false;
6564                }
6565            }
6566            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(i))) => {
6567                if idx_in_parent != *i as usize {
6568                    return false;
6569                }
6570            }
6571            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even)) => {
6572                if !idx_in_parent.is_multiple_of(2) {
6573                    return false;
6574                }
6575            }
6576            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
6577                if idx_in_parent.is_multiple_of(2) {
6578                    return false;
6579                }
6580            }
6581            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
6582                if !idx_in_parent.saturating_sub(p.offset as usize).is_multiple_of(p.pattern_repeat as usize)
6583                {
6584                    return false;
6585                }
6586            }
6587
6588            _ => return false, // can't happen
6589        }
6590    }
6591
6592    true
6593}
6594
6595struct CssBlock {
6596    ending: Option<CssPathPseudoSelector>,
6597    block: CssRuleBlock,
6598}
6599
6600#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
6601/// # Errors
6602///
6603/// Returns an error if the body node cannot be compiled to Rust code.
6604pub fn compile_body_node_to_rust_code<'a>(
6605    body_node: &'a XmlNode,
6606    component_map: &'a ComponentMap,
6607    extra_blocks: &mut VecContents,
6608    css_blocks: &mut BTreeMap<String, String>,
6609    css: &Css,
6610    mut matcher: CssMatcher,
6611) -> Result<String, CompileError> {
6612    use azul_css::css::CssDeclaration;
6613
6614    let t = "";
6615    let t2 = "    ";
6616    let mut dom_string = String::from("Dom::create_body()");
6617    let node_type = CssPathSelector::Type(NodeTypeTag::Body);
6618    matcher.path.push(node_type);
6619
6620    let ids = body_node
6621        .attributes
6622        .get_key("id")
6623        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6624        .unwrap_or_default();
6625    matcher.path.extend(
6626        ids.into_iter()
6627            .map(|id| CssPathSelector::Id(id.to_string().into())),
6628    );
6629    let classes = body_node
6630        .attributes
6631        .get_key("class")
6632        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6633        .unwrap_or_default();
6634    matcher.path.extend(
6635        classes
6636            .into_iter()
6637            .map(|class| CssPathSelector::Class(class.to_string().into())),
6638    );
6639
6640    let matcher_hash = matcher.get_hash();
6641    let css_blocks_for_this_node = get_css_blocks(css, &matcher);
6642    if !css_blocks_for_this_node.is_empty() {
6643        // Track property types for the helper-const machinery, then emit the
6644        // matched declarations as an inline CSS string. (The old path emitted a
6645        // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` + `.with_inline_css_props`,
6646        // but that API was removed in 32d44ed8a; `.with_css(<str>)` is the
6647        // current equivalent and parses pseudo blocks too.)
6648        for css_block in &css_blocks_for_this_node {
6649            for declaration in css_block.block.declarations.as_ref() {
6650                let prop = match declaration {
6651                    CssDeclaration::Static(s) => s,
6652                    CssDeclaration::Dynamic(d) => &d.default_value,
6653                };
6654                extra_blocks.insert_from_css_property(prop);
6655            }
6656        }
6657
6658        let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
6659        if !inline_css.is_empty() {
6660            let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
6661            let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
6662        }
6663        let _ = (&mut *css_blocks, matcher_hash); // retained for signature compat
6664    }
6665
6666    if !body_node.children.as_ref().is_empty() {
6667        use azul_css::codegen::format::GetHash;
6668        let children_hash = body_node.children.as_ref().get_hash();
6669        dom_string.push_str("\r\n.with_children(vec![\r\n");
6670
6671        for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
6672            match child {
6673                XmlNodeChild::Element(child_node) => {
6674                    let mut matcher = matcher.clone();
6675                    matcher.path.push(CssPathSelector::Children);
6676                    matcher.indices_in_parent.push(child_idx);
6677                    matcher.children_length.push(body_node.children.len());
6678
6679                    let _ = write!(dom_string,
6680                        "{}{},\r\n",
6681                        t,
6682                        compile_node_to_rust_code_inner(
6683                            child_node,
6684                            component_map,
6685                            1,
6686                            extra_blocks,
6687                            css_blocks,
6688                            css,
6689                            matcher,
6690                        )?
6691                    );
6692                }
6693                XmlNodeChild::Text(text) => {
6694                    let text = text.trim();
6695                    if !text.is_empty() {
6696                        let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
6697                        let _ = write!(dom_string,
6698                            "{t}Dom::create_text(\"{escaped}\"),\r\n"
6699                        );
6700                    }
6701                }
6702            }
6703        }
6704        let _ = write!(dom_string, "\r\n{t}])");
6705    }
6706
6707    let dom_string = dom_string.trim();
6708    Ok(dom_string.to_string())
6709}
6710
6711/// Serialize the CSS blocks matched for a node into one inline CSS string for
6712/// `Dom::with_css(...)`. `with_css` parses via `Css::parse_inline`, which runs
6713/// the full selector+nesting machinery, so `:hover`/`:active`/`:focus` are
6714/// emitted as nested pseudo blocks and round-trip faithfully; plain rules are
6715/// emitted flat as `key: value;` (via `CssProperty::key()` / `value()`).
6716fn css_blocks_to_inline_string(blocks: &[CssBlock]) -> String {
6717    fn decls_of(block: &CssBlock) -> Vec<String> {
6718        block
6719            .block
6720            .declarations
6721            .as_ref()
6722            .iter()
6723            .map(|d| {
6724                let prop = match d {
6725                    CssDeclaration::Static(s) => s,
6726                    CssDeclaration::Dynamic(dy) => &dy.default_value,
6727                };
6728                format!("{}: {};", prop.key(), prop.value())
6729            })
6730            .collect()
6731    }
6732
6733    let mut normal: Vec<String> = Vec::new();
6734    let mut pseudo: Vec<String> = Vec::new();
6735    for block in blocks {
6736        let pseudo_sel = match block.ending {
6737            Some(CssPathPseudoSelector::Hover) => Some(":hover"),
6738            Some(CssPathPseudoSelector::Active) => Some(":active"),
6739            Some(CssPathPseudoSelector::Focus) => Some(":focus"),
6740            _ => None,
6741        };
6742        match pseudo_sel {
6743            None => normal.extend(decls_of(block)),
6744            Some(sel) => pseudo.push(format!("{} {{ {} }}", sel, decls_of(block).join(" "))),
6745        }
6746    }
6747
6748    let mut parts = normal;
6749    parts.extend(pseudo);
6750    parts.join(" ")
6751}
6752
6753fn get_css_blocks(css: &Css, matcher: &CssMatcher) -> Vec<CssBlock> {
6754    let mut blocks = Vec::new();
6755
6756    for css_block in css.rules.as_ref() {
6757        if matcher.matches(&css_block.path) {
6758            let mut ending = None;
6759
6760            if let Some(CssPathSelector::PseudoSelector(p)) =
6761                css_block.path.selectors.as_ref().last()
6762            {
6763                ending = Some(p.clone());
6764            }
6765
6766            blocks.push(CssBlock {
6767                ending,
6768                block: css_block.clone(),
6769            });
6770        }
6771    }
6772
6773    blocks
6774}
6775
6776fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
6777    use self::DynamicItem::{Var, Str};
6778    if input.is_empty() {
6779        String::from("AzString::from_const_str(\"\")")
6780    } else if input.len() == 1 {
6781        // common: there is only one "dynamic item" - skip the "format!()" macro
6782        match &input[0] {
6783            Var { name, format_spec } => {
6784                let var_name = normalize_casing(name.trim());
6785                if let Some(spec) = format_spec {
6786                    format!("format!(\"{{:{spec}}}\", {var_name}).into()")
6787                } else {
6788                    var_name
6789                }
6790            }
6791            Str(s) => format!("AzString::from_const_str(\"{s}\")"),
6792        }
6793    } else {
6794        // build a "format!("{var}, blah", var)" string
6795        let mut formatted_str = String::from("format!(\"");
6796        let mut variables = Vec::new();
6797        for item in input {
6798            match item {
6799                Var { name, format_spec } => {
6800                    let variable_name = normalize_casing(name.trim());
6801                    if let Some(spec) = format_spec {
6802                        let _ = write!(formatted_str, "{{{variable_name}:{spec}}}");
6803                    } else {
6804                        let _ = write!(formatted_str, "{{{variable_name}}}");
6805                    }
6806                    variables.push(variable_name.clone());
6807                }
6808                Str(s) => {
6809                    let s = s.replace('"', "\\\"");
6810                    formatted_str.push_str(&s);
6811                }
6812            }
6813        }
6814
6815        formatted_str.push('\"');
6816        if !variables.is_empty() {
6817            formatted_str.push_str(", ");
6818        }
6819
6820        formatted_str.push_str(&variables.join(", "));
6821        formatted_str.push_str(").into()");
6822        formatted_str
6823    }
6824}
6825
6826fn format_args_for_rust_code(input: &str) -> String {
6827    let dynamic_str_items = split_dynamic_string(input);
6828    compile_and_format_dynamic_items(&dynamic_str_items)
6829}
6830
6831#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
6832// component_map is forwarded through the codegen recursion for parity with the
6833// component-expanding path; this Rust-codegen path only threads it into recursive calls.
6834#[allow(clippy::only_used_in_recursion)]
6835#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
6836fn compile_node_to_rust_code_inner(
6837    node: &XmlNode,
6838    component_map: &ComponentMap,
6839    tabs: usize,
6840    extra_blocks: &mut VecContents,
6841    css_blocks: &mut BTreeMap<String, String>,
6842    css: &Css,
6843    mut matcher: CssMatcher,
6844) -> Result<String, CompileError> {
6845    use azul_css::css::CssDeclaration;
6846
6847    let t = String::from("    ").repeat(tabs - 1);
6848    let t2 = String::from("    ").repeat(tabs);
6849
6850    let component_name = normalize_casing(&node.node_type);
6851
6852    // Look up the CSS NodeTypeTag
6853    let node_type_tag = tag_to_node_type_tag(&component_name);
6854    let node_type = CssPathSelector::Type(node_type_tag);
6855
6856    // Emit a plain `create_node(<Tag>)` for the base node. Do NOT route through
6857    // the component `compile_fn`: its Rust arm bakes inline text into a
6858    // `.with_children(..)`, which the child-walk below would then OVERWRITE with
6859    // a second `.with_children(..)` — silently dropping the text on any node
6860    // that has BOTH text and element children. The child-walk handles ALL
6861    // children (text + elements) in order, so the base node must stay childless.
6862    // Interactive/data tags (Button/Input/…) whose NodeType carries data fall
6863    // back to `div`, matching the C/C++/Python walkers (`safe_container_tag`).
6864    let ctor = analyze_node_ctor(&component_name, node);
6865    let mut dom_string = ctor.render_rust().map_or_else(|| {
6866        let tag = safe_container_tag(&format!("{:?}", tag_to_node_type(&component_name)));
6867        format!("{t2}Dom::create_node(NodeType::{tag})")
6868    }, |expr| format!("{t2}{expr}"));
6869
6870    matcher.path.push(node_type);
6871    let ids = node
6872        .attributes
6873        .get_key("id")
6874        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6875        .unwrap_or_default();
6876
6877    matcher.path.extend(
6878        ids.into_iter()
6879            .map(|id| CssPathSelector::Id(id.to_string().into())),
6880    );
6881
6882    let classes = node
6883        .attributes
6884        .get_key("class")
6885        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6886        .unwrap_or_default();
6887
6888    matcher.path.extend(
6889        classes
6890            .into_iter()
6891            .map(|class| CssPathSelector::Class(class.to_string().into())),
6892    );
6893
6894    let matcher_hash = matcher.get_hash();
6895    let css_blocks_for_this_node = get_css_blocks(css, &matcher);
6896    if !css_blocks_for_this_node.is_empty() {
6897        // Track property types for the helper-const machinery, then emit the
6898        // matched declarations as an inline CSS string. (The old path emitted a
6899        // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` + `.with_inline_css_props`,
6900        // but that API was removed in 32d44ed8a; `.with_css(<str>)` is the
6901        // current equivalent and parses pseudo blocks too.)
6902        for css_block in &css_blocks_for_this_node {
6903            for declaration in css_block.block.declarations.as_ref() {
6904                let prop = match declaration {
6905                    CssDeclaration::Static(s) => s,
6906                    CssDeclaration::Dynamic(d) => &d.default_value,
6907                };
6908                extra_blocks.insert_from_css_property(prop);
6909            }
6910        }
6911
6912        let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
6913        if !inline_css.is_empty() {
6914            let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
6915            let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
6916        }
6917        let _ = (&mut *css_blocks, matcher_hash); // retained for signature compat
6918    }
6919
6920    set_stringified_attributes(
6921        &mut dom_string,
6922        &node.attributes,
6923        &ComponentArgumentVec::new(),
6924        tabs,
6925    );
6926
6927    // Text folded into the ctor (Tier A/C) is skipped, as is a `<caption>`
6928    // already injected by `create_table`.
6929    let mut caption_skipped = false;
6930    let mut children_string = node
6931        .children
6932        .as_ref()
6933        .iter()
6934        .enumerate()
6935        .filter_map(|(child_idx, c)| match c {
6936            XmlNodeChild::Element(child_node) => {
6937                if ctor.skip_caption()
6938                    && !caption_skipped
6939                    && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
6940                {
6941                    caption_skipped = true;
6942                    return None;
6943                }
6944                let mut matcher = matcher.clone();
6945                matcher.path.push(CssPathSelector::Children);
6946                matcher.indices_in_parent.push(child_idx);
6947                matcher.children_length.push(node.children.len());
6948
6949                Some(compile_node_to_rust_code_inner(
6950                    child_node,
6951                    component_map,
6952                    tabs + 1,
6953                    extra_blocks,
6954                    css_blocks,
6955                    css,
6956                    matcher,
6957                ))
6958            }
6959            XmlNodeChild::Text(text) => {
6960                if ctor.consumes_text() {
6961                    return None;
6962                }
6963                let text = text.trim();
6964                if text.is_empty() {
6965                    None
6966                } else {
6967                    let t2 = String::from("    ").repeat(tabs);
6968                    let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
6969                    Some(Ok(format!(
6970                        "{t2}Dom::create_text(\"{escaped}\")"
6971                    )))
6972                }
6973            }
6974        })
6975        .collect::<Result<Vec<_>, _>>()?
6976        .join(",\r\n");
6977
6978    if !children_string.is_empty() {
6979        let _ = write!(dom_string,
6980            "\r\n{t2}.with_children(vec![\r\n{children_string}\r\n{t2}])"
6981        );
6982    }
6983
6984    Ok(dom_string)
6985}
6986
6987// ───────────────────────────────────────────────────────────────────────────
6988// Generic FLUENT DOM-builder emitter (C++ / Python).
6989//
6990// Rust has its own dedicated walker above (`compile_*_to_rust_code`). C++ and
6991// Python share this generic walker because their builder APIs are also fluent
6992// (`Dom::create_*().with_css(..).with_child(..)`); only the surface tokens
6993// differ, captured in `FluentSyntax`. Plain C is imperative and has its own
6994// walker (`compile_*_to_c_code`).
6995// ───────────────────────────────────────────────────────────────────────────
6996
6997/// Tags with a zero-arg per-tag creator (`create_<tag>()` / `AzDom_create<Tag>()`
6998/// / `create_node(NodeType::<Tag>)`). Interactive / data elements (Button, Input,
6999/// Img, Select, Textarea, Label, A, Table, …) take constructor arguments, so an
7000/// exported page maps them to a plain `div` container (structure preserved; the
7001/// user re-wires behavior). Keep these CamelCase to match `NodeTypeTag` debug names.
7002const SAFE_CONTAINER_TAGS: &[&str] = &[
7003    // These must match the real `NodeType` Debug names exactly (the lookup below is a
7004    // string compare against `{:?}`). Six used to be mis-cased — "Blockquote",
7005    // "Colgroup", "Figcaption", "Tbody", "Tfoot", "Thead" — so those tags silently
7006    // degraded to "Div".
7007    "Abbr", "Acronym", "Address", "Article", "Aside", "B", "Bdi", "Bdo", "Big",
7008    "BlockQuote", "Body", "Br", "Caption", "Cite", "Code", "ColGroup", "Dd",
7009    "Del", "Dfn", "Dir", "Div", "Dl", "Dt", "Em", "Embed", "FigCaption",
7010    "Figure", "Footer", "H1", "H2", "H3", "H4", "H5", "H6", "Head", "Header",
7011    "Hr", "Html", "I", "Ins", "Kbd", "Li", "Link", "Main", "Map", "Mark",
7012    "Meta", "Nav", "Object", "Ol", "P", "Pre", "Q", "Rp", "Rt", "Rtc", "Ruby",
7013    "S", "Samp", "Script", "Section", "Small", "Span", "Strong", "Style", "Sub",
7014    "Sup", "Svg", "TBody", "Td", "TFoot", "Th", "THead", "Title", "Tr", "U",
7015    "Ul", "Var", "Wbr",
7016];
7017
7018/// The CamelCase tag to actually emit a creator for: the tag itself if it has a
7019/// zero-arg creator, else `"Div"`.
7020fn safe_container_tag(tag_dbg: &str) -> &'static str {
7021    SAFE_CONTAINER_TAGS.iter().copied().find(|t| *t == tag_dbg).unwrap_or("Div")
7022}
7023
7024// ───────────────────────────────────────────────────────────────────────────
7025// Semantic / accessibility-aware constructor selection.
7026//
7027// Instead of mapping every element to a plain `div`, an exported live page
7028// picks the *most specific* Azul constructor so the generated app keeps the
7029// page's semantics + accessibility tree:
7030//
7031//   • Tier A  `create_<tag>_with_text(text)` — a tag with a single text child
7032//             and no element children (P, Span, H1-H6, Li, Td, Code, …).
7033//   • Tier B  aria-only / void widgets (Details, Summary, Form, Canvas, Area,
7034//             …) — `create_<tag>(SmallAriaInfo::label(..))` when `aria-label`
7035//             is present, else `create_<tag>_no_a11y()`.
7036//   • Tier C  multi-arg widgets (Button, A, Label, Input, Select, Option,
7037//             Optgroup, Textarea, Table) — args pulled from HTML attributes.
7038//   • Tier D  scalar-driven widgets (Progress, Meter, Dialog) — the `*_no_a11y`
7039//             form with extracted numeric args (the full aria structs are
7040//             complex; the NoA11y form is simplest + correct).
7041//
7042// Every symbol emitted here is verified to exist in `target/codegen/azul.h` (C)
7043// and `azul20.hpp` (C++); anything else falls back to `safe_container_tag`
7044// (`div`). The four walkers share `analyze_node_ctor` and each renders the
7045// result with its own surface tokens.
7046// ───────────────────────────────────────────────────────────────────────────
7047
7048/// A single positional argument of a semantic constructor. String payloads are
7049/// RAW — escaping happens at render time (matching the walkers).
7050#[derive(Debug, Clone)]
7051enum CtorArg {
7052    /// Plain string literal (`AzString` / `String` / `"…"`).
7053    Str(String),
7054    /// `SmallAriaInfo` built from an accessible label.
7055    Aria(String),
7056    /// `f32` numeric literal.
7057    Float(f32),
7058    /// `OptionString::Some(text)`.
7059    OptSome(String),
7060    /// `OptionString::None`.
7061    OptNone,
7062}
7063
7064/// The constructor chosen for an element node.
7065enum NodeCtor {
7066    /// Plain container — keep each walker's existing `create_<tag>()` path.
7067    Plain,
7068    /// A specific semantic constructor.
7069    Semantic {
7070        /// Canonical CamelCase suffix after `create` / `AzDom_create`
7071        /// (e.g. `Button`, `ButtonNoA11y`, `PWithText`, `A`, `ANoA11y`).
7072        suffix: String,
7073        args: Vec<CtorArg>,
7074        /// The node's direct text is folded into the ctor — skip text children
7075        /// in the walk so it isn't emitted twice.
7076        consumes_text: bool,
7077        /// The table aria form injects its own `<caption>` child — drop the
7078        /// first literal `<caption>` element so it isn't duplicated.
7079        skip_caption: bool,
7080    },
7081}
7082
7083/// Uppercase the first character (`button` → `Button`, `h1` → `H1`). HTML tags
7084/// are single lowercase tokens, so this yields the exact `AzDom_create<Suffix>`
7085/// spelling.
7086fn cap_first(tag: &str) -> String {
7087    let mut c = tag.chars();
7088    c.next().map_or_else(String::new, |f| f.to_uppercase().collect::<String>() + c.as_str())
7089}
7090
7091/// CamelCase → `snake_case` for the C++/Python/Rust method names
7092/// (`ButtonNoA11y` → `button_no_a11y`, `PWithText` → `p_with_text`,
7093/// `ANoA11y` → `a_no_a11y`, `H1WithText` → `h1_with_text`).
7094fn camel_to_snake(s: &str) -> String {
7095    let chars: Vec<char> = s.chars().collect();
7096    let mut out = String::new();
7097    for (i, &ch) in chars.iter().enumerate() {
7098        if ch.is_ascii_uppercase() && i > 0 {
7099            let prev = chars[i - 1];
7100            let next_lower = chars.get(i + 1).is_some_and(char::is_ascii_lowercase);
7101            if prev.is_ascii_lowercase()
7102                || prev.is_ascii_digit()
7103                || (prev.is_ascii_uppercase() && next_lower)
7104            {
7105                out.push('_');
7106            }
7107        }
7108        out.extend(ch.to_lowercase());
7109    }
7110    out
7111}
7112
7113/// Escape `\` and `"` for a double-quoted string literal.
7114fn esc_lit(s: &str) -> String {
7115    s.replace('\\', "\\\\").replace('"', "\\\"")
7116}
7117
7118/// Format an `f32` as a valid float literal with a decimal point (`1` → `1.0`).
7119fn fmt_f32_lit(f: f32) -> String {
7120    let s = format!("{f}");
7121    if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") {
7122        s
7123    } else {
7124        format!("{s}.0")
7125    }
7126}
7127
7128/// Joined, trimmed text of a node's *direct* text children (`"  Go  "` → `"Go"`).
7129fn node_direct_text(node: &XmlNode) -> String {
7130    node.children
7131        .as_ref()
7132        .iter()
7133        .filter_map(|c| match c {
7134            XmlNodeChild::Text(t) => {
7135                let t = t.trim();
7136                if t.is_empty() { None } else { Some(t.to_string()) }
7137            }
7138            XmlNodeChild::Element(_) => None,
7139        })
7140        .collect::<Vec<_>>()
7141        .join(" ")
7142}
7143
7144/// Non-empty `aria-label` attribute value, if present.
7145fn node_aria_label(node: &XmlNode) -> Option<String> {
7146    node.attributes.get_key("aria-label").and_then(|v| {
7147        let v = v.as_str().trim();
7148        if v.is_empty() { None } else { Some(v.to_string()) }
7149    })
7150}
7151
7152/// Attribute value, or `default` when absent.
7153fn node_attr_or(node: &XmlNode, key: &str, default: &str) -> String {
7154    node.attributes
7155        .get_key(key).map_or_else(|| default.to_string(), |v| v.as_str().to_string())
7156}
7157
7158/// Attribute parsed as `f32`, or `default` when absent / unparsable.
7159fn node_attr_f32(node: &XmlNode, key: &str, default: f32) -> f32 {
7160    node.attributes
7161        .get_key(key)
7162        .and_then(|v| v.as_str().trim().parse::<f32>().ok())
7163        .unwrap_or(default)
7164}
7165
7166/// Text of the node's first `<caption>` element child, if any (non-empty).
7167fn first_caption_text(node: &XmlNode) -> Option<String> {
7168    node.children.as_ref().iter().find_map(|c| match c {
7169        XmlNodeChild::Element(e) if e.node_type.as_str().eq_ignore_ascii_case("caption") => {
7170            let t = e.get_text_content();
7171            let t = t.trim();
7172            if t.is_empty() { None } else { Some(t.to_string()) }
7173        }
7174        _ => None,
7175    })
7176}
7177
7178/// Tags with a single-arg `create_<tag>_with_text(text)` constructor (Tier A).
7179const WITH_TEXT_TAGS: &[&str] = &[
7180    "acronym", "b", "bdi", "bdo", "big", "blockquote", "cite", "code", "del",
7181    "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "ins", "kbd", "li",
7182    "mark", "p", "pre", "rp", "rt", "s", "samp", "small", "span", "strong",
7183    "style", "sub", "sup", "td", "th", "title", "u", "var",
7184];
7185
7186/// Pick the semantic constructor for `tag` (lowercase HTML tag) + `node`.
7187#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
7188fn analyze_node_ctor(tag: &str, node: &XmlNode) -> NodeCtor {
7189    // Helper for the common "no caption skip" case.
7190    fn sem(suffix: impl Into<String>, args: Vec<CtorArg>, consumes_text: bool) -> NodeCtor {
7191        NodeCtor::Semantic {
7192            suffix: suffix.into(),
7193            args,
7194            consumes_text,
7195            skip_caption: false,
7196        }
7197    }
7198
7199    let aria = node_aria_label(node);
7200    let has_aria = aria.is_some();
7201    let label = aria.unwrap_or_default();
7202    // `has_only_text_children()` is also true for childless nodes; pair it with
7203    // `has_text` so empty elements stay plain containers.
7204    let pure_text = node.has_only_text_children();
7205    let text = node_direct_text(node);
7206    let has_text = !text.is_empty();
7207    let cap = cap_first(tag);
7208
7209    // Tier A — *_with_text (single text child, no element children).
7210    if WITH_TEXT_TAGS.contains(&tag) {
7211        if pure_text && has_text {
7212            return sem(format!("{cap}WithText"), vec![CtorArg::Str(text)], true);
7213        }
7214        return NodeCtor::Plain;
7215    }
7216
7217    match tag {
7218        // Tier B — aria-only / void widgets.
7219        "details" | "form" | "fieldset" | "legend" | "menu" | "output"
7220        | "datalist" | "canvas" | "audio" | "video" | "area" => {
7221            if has_aria {
7222                sem(cap, vec![CtorArg::Aria(label)], false)
7223            } else {
7224                sem(format!("{cap}NoA11y"), vec![], false)
7225            }
7226        }
7227        // Summary is Tier B but also has a WithText form for a single text child.
7228        "summary" => {
7229            if pure_text && has_text {
7230                if has_aria {
7231                    sem("SummaryWithText", vec![CtorArg::Str(text), CtorArg::Aria(label)], true)
7232                } else {
7233                    sem("SummaryWithTextNoA11y", vec![CtorArg::Str(text)], true)
7234                }
7235            } else if has_aria {
7236                sem("Summary", vec![CtorArg::Aria(label)], false)
7237            } else {
7238                sem("SummaryNoA11y", vec![], false)
7239            }
7240        }
7241
7242        // Tier C — multi-arg widgets (args from HTML attributes).
7243        "button" => {
7244            if has_aria {
7245                sem("Button", vec![CtorArg::Str(text), CtorArg::Aria(label)], true)
7246            } else {
7247                sem("ButtonNoA11y", vec![CtorArg::Str(text)], true)
7248            }
7249        }
7250        "a" => {
7251            let href = node_attr_or(node, "href", "");
7252            if has_aria {
7253                sem("A", vec![CtorArg::Str(href), CtorArg::Str(text), CtorArg::Aria(label)], true)
7254            } else {
7255                let lbl = if has_text { CtorArg::OptSome(text) } else { CtorArg::OptNone };
7256                sem("ANoA11y", vec![CtorArg::Str(href), lbl], true)
7257            }
7258        }
7259        "label" => {
7260            let for_id = node_attr_or(node, "for", "");
7261            if has_aria {
7262                sem("Label", vec![CtorArg::Str(for_id), CtorArg::Str(text), CtorArg::Aria(label)], true)
7263            } else {
7264                sem("LabelNoA11y", vec![CtorArg::Str(for_id), CtorArg::Str(text)], true)
7265            }
7266        }
7267        "input" => {
7268            let ty = node_attr_or(node, "type", "text");
7269            let name = node_attr_or(node, "name", "");
7270            if has_aria {
7271                sem("Input", vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7272            } else {
7273                sem("InputNoA11y", vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label)], false)
7274            }
7275        }
7276        "textarea" => {
7277            let name = node_attr_or(node, "name", "");
7278            if has_aria {
7279                sem("Textarea", vec![CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7280            } else {
7281                sem("TextareaNoA11y", vec![CtorArg::Str(name), CtorArg::Str(label)], false)
7282            }
7283        }
7284        "select" => {
7285            let name = node_attr_or(node, "name", "");
7286            if has_aria {
7287                sem("Select", vec![CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7288            } else {
7289                sem("SelectNoA11y", vec![CtorArg::Str(name), CtorArg::Str(label)], false)
7290            }
7291        }
7292        "option" => {
7293            let value = node_attr_or(node, "value", "");
7294            if has_aria {
7295                sem("Option", vec![CtorArg::Str(value), CtorArg::Str(text), CtorArg::Aria(label)], true)
7296            } else {
7297                sem("OptionNoA11y", vec![CtorArg::Str(value), CtorArg::Str(text)], true)
7298            }
7299        }
7300        "optgroup" => {
7301            let lbl = node_attr_or(node, "label", "");
7302            if has_aria {
7303                sem("Optgroup", vec![CtorArg::Str(lbl), CtorArg::Aria(label)], false)
7304            } else {
7305                sem("OptgroupNoA11y", vec![CtorArg::Str(lbl)], false)
7306            }
7307        }
7308        "table" => {
7309            if has_aria {
7310                // The aria form injects a caption child, so take the caption from
7311                // the literal <caption> (or the aria label) and drop the literal.
7312                let caption = first_caption_text(node).unwrap_or_else(|| label.clone());
7313                NodeCtor::Semantic {
7314                    suffix: "Table".to_string(),
7315                    args: vec![CtorArg::Str(caption), CtorArg::Aria(label)],
7316                    consumes_text: false,
7317                    skip_caption: true,
7318                }
7319            } else {
7320                sem("TableNoA11y", vec![], false)
7321            }
7322        }
7323
7324        // Tier D — scalar-driven widgets (NoA11y form with extracted numbers).
7325        "progress" => sem(
7326            "ProgressNoA11y",
7327            vec![
7328                CtorArg::Float(node_attr_f32(node, "value", 0.0)),
7329                CtorArg::Float(node_attr_f32(node, "max", 1.0)),
7330            ],
7331            false,
7332        ),
7333        "meter" => sem(
7334            "MeterNoA11y",
7335            vec![
7336                CtorArg::Float(node_attr_f32(node, "value", 0.0)),
7337                CtorArg::Float(node_attr_f32(node, "min", 0.0)),
7338                CtorArg::Float(node_attr_f32(node, "max", 1.0)),
7339            ],
7340            false,
7341        ),
7342        "dialog" => sem("DialogNoA11y", vec![], false),
7343
7344        _ => NodeCtor::Plain,
7345    }
7346}
7347
7348impl CtorArg {
7349    /// Rust expression for this argument (`AzString::from(..)` works for both the
7350    /// `Into<AzString>` and the concrete `AzString` parameter forms).
7351    fn render_rust(&self) -> String {
7352        match self {
7353            Self::Str(s) => format!("AzString::from(\"{}\")", esc_lit(s)),
7354            Self::Aria(s) => format!("SmallAriaInfo::label(AzString::from(\"{}\"))", esc_lit(s)),
7355            Self::Float(f) => fmt_f32_lit(*f),
7356            Self::OptSome(s) => format!("OptionString::Some(AzString::from(\"{}\"))", esc_lit(s)),
7357            Self::OptNone => "OptionString::None".to_string(),
7358        }
7359    }
7360    fn render_c(&self) -> String {
7361        match self {
7362            Self::Str(s) => format!("AZ_STR(\"{}\")", esc_lit(s)),
7363            Self::Aria(s) => format!("AzSmallAriaInfo_label(AZ_STR(\"{}\"))", esc_lit(s)),
7364            Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
7365            Self::OptSome(s) => format!("AzOptionString_some(AZ_STR(\"{}\"))", esc_lit(s)),
7366            Self::OptNone => "AzOptionString_none()".to_string(),
7367        }
7368    }
7369    fn render_cpp(&self) -> String {
7370        match self {
7371            Self::Str(s) => format!("String(\"{}\")", esc_lit(s)),
7372            Self::Aria(s) => format!("SmallAriaInfo::label(String(\"{}\"))", esc_lit(s)),
7373            Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
7374            Self::OptSome(s) => format!("OptionString::some(String(\"{}\"))", esc_lit(s)),
7375            Self::OptNone => "OptionString::none()".to_string(),
7376        }
7377    }
7378    fn render_python(&self) -> String {
7379        match self {
7380            Self::Str(s) => format!("\"{}\"", esc_lit(s)),
7381            Self::Aria(s) => format!("azul.SmallAriaInfo.label(\"{}\")", esc_lit(s)),
7382            Self::Float(f) => fmt_f32_lit(*f),
7383            Self::OptSome(s) => format!("azul.OptionString.some(\"{}\")", esc_lit(s)),
7384            Self::OptNone => "azul.OptionString.none()".to_string(),
7385        }
7386    }
7387}
7388
7389impl NodeCtor {
7390    const fn consumes_text(&self) -> bool {
7391        matches!(self, Self::Semantic { consumes_text: true, .. })
7392    }
7393    const fn skip_caption(&self) -> bool {
7394        matches!(self, Self::Semantic { skip_caption: true, .. })
7395    }
7396    /// `Dom::create_…(args)` for Rust, or `None` for a plain container.
7397    fn render_rust(&self) -> Option<String> {
7398        match self {
7399            Self::Plain => None,
7400            Self::Semantic { suffix, args, .. } => Some(format!(
7401                "Dom::create_{}({})",
7402                camel_to_snake(suffix),
7403                args.iter().map(CtorArg::render_rust).collect::<Vec<_>>().join(", ")
7404            )),
7405        }
7406    }
7407    /// `AzDom_create…(args)` for C, or `None` for a plain container.
7408    fn render_c(&self) -> Option<String> {
7409        match self {
7410            Self::Plain => None,
7411            Self::Semantic { suffix, args, .. } => Some(format!(
7412                "AzDom_create{}({})",
7413                suffix,
7414                args.iter().map(CtorArg::render_c).collect::<Vec<_>>().join(", ")
7415            )),
7416        }
7417    }
7418    /// Fluent `Dom::create_…` (C++) / `azul.Dom.create_…` (Python), or `None`.
7419    fn render_fluent(&self, target: &CompileTarget) -> Option<String> {
7420        match self {
7421            Self::Plain => None,
7422            Self::Semantic { suffix, args, .. } => {
7423                let snake = camel_to_snake(suffix);
7424                let (prefix, rendered) = match target {
7425                    CompileTarget::Cpp => (
7426                        format!("Dom::create_{snake}"),
7427                        args.iter().map(CtorArg::render_cpp).collect::<Vec<_>>(),
7428                    ),
7429                    CompileTarget::Python => (
7430                        format!("azul.Dom.create_{snake}"),
7431                        args.iter().map(CtorArg::render_python).collect::<Vec<_>>(),
7432                    ),
7433                    _ => return None,
7434                };
7435                Some(format!("{}({})", prefix, rendered.join(", ")))
7436            }
7437        }
7438    }
7439}
7440
7441/// Per-language token hooks for the fluent walker. The `&str` args are already
7442/// escaped for a double-quoted string literal.
7443struct FluentSyntax {
7444    target: CompileTarget,
7445    /// tag debug-name (e.g. "Div") -> full create expression
7446    create_node: fn(&str) -> String,
7447    /// escaped text -> create-text expression
7448    create_text: fn(&str) -> String,
7449    /// escaped css -> `.with_css(..)` call
7450    with_css: fn(&str) -> String,
7451    /// escaped class -> `.with_class(..)` call
7452    with_class: fn(&str) -> String,
7453    /// escaped id -> `.with_id(..)` call
7454    with_id: fn(&str) -> String,
7455    /// escaped child expression -> `.with_child(..)` call (children are chained)
7456    with_child: fn(&str) -> String,
7457}
7458
7459const CPP_SYNTAX: FluentSyntax = FluentSyntax {
7460    target: CompileTarget::Cpp,
7461    // Use per-tag creators (Dom::create_div(), create_p(), create_body(), …)
7462    // — `NodeType` is a tagged union, so `create_node` would need union
7463    // construction; the per-tag creators exist for every common HTML element.
7464    create_node: |tag| alloc::format!("Dom::create_{}()", tag.to_lowercase()),
7465    create_text: |s| alloc::format!("Dom::create_text(String(\"{s}\"))"),
7466    with_css: |s| alloc::format!(".with_css(String(\"{s}\"))"),
7467    with_class: |s| alloc::format!(".with_class(String(\"{s}\"))"),
7468    with_id: |s| alloc::format!(".with_id(String(\"{s}\"))"),
7469    with_child: |c| alloc::format!(".with_child({c})"),
7470};
7471
7472const PYTHON_SYNTAX: FluentSyntax = FluentSyntax {
7473    target: CompileTarget::Python,
7474    // Per-tag creators (azul.Dom.create_div(), …) — see CPP_SYNTAX note.
7475    create_node: |tag| alloc::format!("azul.Dom.create_{}()", tag.to_lowercase()),
7476    create_text: |s| alloc::format!("azul.Dom.create_text(\"{s}\")"),
7477    with_css: |s| alloc::format!(".with_css(\"{s}\")"),
7478    with_class: |s| alloc::format!(".with_class(\"{s}\")"),
7479    with_id: |s| alloc::format!(".with_id(\"{s}\")"),
7480    with_child: |c| alloc::format!(".with_child({c})"),
7481};
7482
7483/// Walk one element node, emitting a fluent create-expression for `syntax`'s
7484/// language. Mirrors `compile_node_to_rust_code_inner` but token-parameterized.
7485#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7486// See compile_node_to_rust_code_inner: component_map is forwarded for codegen-path parity.
7487#[allow(clippy::only_used_in_recursion)]
7488fn compile_node_fluent(
7489    node: &XmlNode,
7490    syntax: &FluentSyntax,
7491    component_map: &ComponentMap,
7492    css: &Css,
7493    mut matcher: CssMatcher,
7494) -> Result<String, CompileError> {
7495    use azul_css::css::CssDeclaration;
7496
7497    let component_name = normalize_casing(&node.node_type);
7498    let node_type_tag = tag_to_node_type_tag(&component_name);
7499    let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
7500
7501    // Base create-expression. For an exported live page every node is a plain
7502    // HTML element, so emit a per-tag creator directly via the language hooks
7503    // (universal + verified) rather than the per-component `compile_fn`, whose
7504    // C++/Python arms emit stale placeholder syntax (`Dom.div()` etc.).
7505    // Interactive/data tags (whose creators need args) fall back to `div`. Any
7506    // element text shows up as a Text child below and is handled there.
7507    let ctor = analyze_node_ctor(&component_name, node);
7508    let mut s = ctor.render_fluent(&syntax.target).map_or_else(|| (syntax.create_node)(safe_container_tag(&tag_dbg)), |expr| expr);
7509
7510    matcher.path.push(CssPathSelector::Type(node_type_tag));
7511    let ids: Vec<String> = node.attributes.get_key("id")
7512        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7513        .unwrap_or_default();
7514    matcher.path.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
7515    let classes: Vec<String> = node.attributes.get_key("class")
7516        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7517        .unwrap_or_default();
7518    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7519
7520    // Inline CSS (matched rules -> `.with_css("..")`, pseudo blocks included).
7521    let blocks = get_css_blocks(css, &matcher);
7522    if !blocks.is_empty() {
7523        let inline_css = css_blocks_to_inline_string(&blocks);
7524        if !inline_css.is_empty() {
7525            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7526            s.push_str(&(syntax.with_css)(&esc));
7527        }
7528    }
7529    for id in &ids {
7530        s.push_str(&(syntax.with_id)(&id.replace('\\', "\\\\").replace('"', "\\\"")));
7531    }
7532    for class in &classes {
7533        s.push_str(&(syntax.with_class)(&class.replace('\\', "\\\\").replace('"', "\\\"")));
7534    }
7535
7536    // Children (chained `.with_child(..)`). Text folded into the ctor (Tier A/C)
7537    // is skipped here, as is a `<caption>` already injected by `create_table`.
7538    let mut caption_skipped = false;
7539    for (child_idx, child) in node.children.as_ref().iter().enumerate() {
7540        match child {
7541            XmlNodeChild::Element(child_node) => {
7542                if ctor.skip_caption()
7543                    && !caption_skipped
7544                    && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
7545                {
7546                    caption_skipped = true;
7547                    continue;
7548                }
7549                let mut m = matcher.clone();
7550                m.path.push(CssPathSelector::Children);
7551                m.indices_in_parent.push(child_idx);
7552                m.children_length.push(node.children.len());
7553                let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
7554                s.push_str(&(syntax.with_child)(&child_src));
7555            }
7556            XmlNodeChild::Text(text) => {
7557                if ctor.consumes_text() {
7558                    continue;
7559                }
7560                let text = text.trim();
7561                if !text.is_empty() {
7562                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7563                    s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
7564                }
7565            }
7566        }
7567    }
7568
7569    Ok(s)
7570}
7571
7572/// Build the `<body>` render-expression for `syntax`'s language.
7573#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7574fn compile_body_fluent<'a>(
7575    body_node: &'a XmlNode,
7576    syntax: &FluentSyntax,
7577    component_map: &'a ComponentMap,
7578    css: &Css,
7579    mut matcher: CssMatcher,
7580) -> Result<String, CompileError> {
7581    let mut s = (syntax.create_node)("Body");
7582    matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
7583    let classes: Vec<String> = body_node.attributes.get_key("class")
7584        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7585        .unwrap_or_default();
7586    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7587
7588    let blocks = get_css_blocks(css, &matcher);
7589    if !blocks.is_empty() {
7590        let inline_css = css_blocks_to_inline_string(&blocks);
7591        if !inline_css.is_empty() {
7592            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7593            s.push_str(&(syntax.with_css)(&esc));
7594        }
7595    }
7596    for class in &classes {
7597        s.push_str(&(syntax.with_class)(&class.replace('\\', "\\\\").replace('"', "\\\"")));
7598    }
7599
7600    for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
7601        match child {
7602            XmlNodeChild::Element(child_node) => {
7603                let mut m = matcher.clone();
7604                m.path.push(CssPathSelector::Children);
7605                m.indices_in_parent.push(child_idx);
7606                m.children_length.push(body_node.children.len());
7607                let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
7608                s.push_str(&(syntax.with_child)(&child_src));
7609            }
7610            XmlNodeChild::Text(text) => {
7611                let text = text.trim();
7612                if !text.is_empty() {
7613                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7614                    s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
7615                }
7616            }
7617        }
7618    }
7619    Ok(s)
7620}
7621
7622/// Parse the page's `<style>` and seed a matcher rooted at `<body>`. Shared by
7623/// the C++/Python/C entry points (mirrors the head of `str_to_rust_code`).
7624#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7625fn parse_page_style_and_body(
7626    root_nodes: &[XmlNodeChild],
7627) -> Result<(Css, &XmlNode), CompileError> {
7628    let html_node = get_html_node(root_nodes)?;
7629    let body_node = get_body_node(html_node.children.as_ref())?;
7630    let mut global_style = Css::empty();
7631    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
7632        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
7633            let text = style_node.get_text_content();
7634            if !text.is_empty() {
7635                global_style = azul_css::parser2::new_from_str(&text).0;
7636            }
7637        }
7638    }
7639    global_style.sort_by_specificity();
7640    Ok((global_style, body_node))
7641}
7642
7643fn body_matcher(body_node: &XmlNode) -> CssMatcher {
7644    CssMatcher {
7645        path: Vec::new(),
7646        indices_in_parent: vec![0],
7647        children_length: vec![body_node.children.as_ref().len()],
7648    }
7649}
7650
7651/// Compile a full HTML page to a compilable **C++** Azul app.
7652#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7653/// # Errors
7654///
7655/// Returns an error if the XML cannot be parsed or compiled to C++ code.
7656pub fn str_to_cpp_code<'a>(
7657    root_nodes: &'a [XmlNodeChild],
7658    component_map: &'a ComponentMap,
7659) -> Result<String, CompileError> {
7660    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7661    let render = compile_body_fluent(body_node, &CPP_SYNTAX, component_map, &global_style, body_matcher(body_node))?;
7662    Ok(alloc::format!(
7663        "// Auto-generated UI source code (C++). Build:\n\
7664         //   clang++ -std=c++20 -I <azul>/target/codegen main.cpp -lazul\n\
7665         #include \"azul20.hpp\"\n\
7666         using namespace azul;\n\n\
7667         struct Data {{}};\n\n\
7668         AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n    \
7669         return {render};\n}}\n\n\
7670         int main() {{\n    \
7671         RefAny data = RefAny::create(Data{{}});\n    \
7672         WindowCreateOptions window = WindowCreateOptions::create(render);\n    \
7673         App app = App::create(std::move(data), AppConfig::default_());\n    \
7674         app.run(std::move(window));\n    \
7675         return 0;\n}}\n"
7676    ))
7677}
7678
7679/// Compile a full HTML page to a compilable **Python** Azul app.
7680#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7681/// # Errors
7682///
7683/// Returns an error if the XML cannot be parsed or compiled to Python code.
7684pub fn str_to_python_code<'a>(
7685    root_nodes: &'a [XmlNodeChild],
7686    component_map: &'a ComponentMap,
7687) -> Result<String, CompileError> {
7688    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7689    let render = compile_body_fluent(body_node, &PYTHON_SYNTAX, component_map, &global_style, body_matcher(body_node))?;
7690    Ok(alloc::format!(
7691        "# Auto-generated UI source code (Python). Run: python3 main.py\n\
7692         import azul\n\n\
7693         class Data:\n    pass\n\n\
7694         def render(data, info):\n    return (\n        {}\n    )\n\n\
7695         def main():\n    \
7696         app = azul.App.create(Data(), azul.AppConfig.create())\n    \
7697         window = azul.WindowCreateOptions.create(render)\n    \
7698         app.run(window)\n\n\
7699         if __name__ == \"__main__\":\n    main()\n",
7700        render.replace("\r\n", "\n        ")
7701    ))
7702}
7703
7704// ───────────────────────────────────────────────────────────────────────────
7705// Imperative C emitter. C has no fluent builder: each node is a statement that
7706// creates an `AzDom` local, applies css/class (by-value, returns), and pushes
7707// children via `AzDom_addChild(&parent, child)`. A recursive walk emits the
7708// statements bottom-up and returns the variable name holding each node.
7709// ───────────────────────────────────────────────────────────────────────────
7710
7711/// C per-tag creator suffix: `NodeTypeTag` debug name with first char kept and
7712/// the rest lowercased (`Div`->`Div`, `BlockQuote`->`Blockquote`, `H1`->`H1`),
7713/// matching `AzDom_create<Suffix>` in azul.h.
7714fn c_creator_suffix(tag_dbg: &str) -> String {
7715    let mut chars = tag_dbg.chars();
7716    chars.next().map_or_else(|| "Div".to_string(), |first| {
7717            let rest: String = chars.as_str().to_lowercase();
7718            alloc::format!("{first}{rest}")
7719        })
7720}
7721
7722#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7723fn compile_node_c(
7724    node: &XmlNode,
7725    component_map: &ComponentMap,
7726    css: &Css,
7727    mut matcher: CssMatcher,
7728    counter: &mut usize,
7729    out: &mut String,
7730) -> Result<String, CompileError> {
7731    let _ = component_map;
7732    let component_name = normalize_casing(&node.node_type);
7733    let node_type_tag = tag_to_node_type_tag(&component_name);
7734    let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
7735
7736    let var = alloc::format!("n{}", *counter);
7737    *counter += 1;
7738    let ctor = analyze_node_ctor(&component_name, node);
7739    match ctor.render_c() {
7740        Some(expr) => { let _ = writeln!(out, "    AzDom {var} = {expr};"); },
7741        None => { let _ = writeln!(out,
7742            "    AzDom {} = AzDom_create{}();",
7743            var,
7744            c_creator_suffix(safe_container_tag(&tag_dbg))
7745        ); },
7746    }
7747
7748    matcher.path.push(CssPathSelector::Type(node_type_tag));
7749    let ids: Vec<String> = node.attributes.get_key("id")
7750        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7751        .unwrap_or_default();
7752    matcher.path.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
7753    let classes: Vec<String> = node.attributes.get_key("class")
7754        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7755        .unwrap_or_default();
7756    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7757
7758    let blocks = get_css_blocks(css, &matcher);
7759    if !blocks.is_empty() {
7760        let inline_css = css_blocks_to_inline_string(&blocks);
7761        if !inline_css.is_empty() {
7762            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7763            let _ = writeln!(out, "    {var} = AzDom_withCss({var}, AZ_STR(\"{esc}\"));");
7764        }
7765    }
7766    for id in &ids {
7767        let esc = id.replace('\\', "\\\\").replace('"', "\\\"");
7768        let _ = writeln!(out, "    {var} = AzDom_withId({var}, AZ_STR(\"{esc}\"));");
7769    }
7770    for class in &classes {
7771        let esc = class.replace('\\', "\\\\").replace('"', "\\\"");
7772        let _ = writeln!(out, "    {var} = AzDom_withClass({var}, AZ_STR(\"{esc}\"));");
7773    }
7774
7775    let mut caption_skipped = false;
7776    for (child_idx, child) in node.children.as_ref().iter().enumerate() {
7777        match child {
7778            XmlNodeChild::Element(child_node) => {
7779                if ctor.skip_caption()
7780                    && !caption_skipped
7781                    && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
7782                {
7783                    caption_skipped = true;
7784                    continue;
7785                }
7786                let mut m = matcher.clone();
7787                m.path.push(CssPathSelector::Children);
7788                m.indices_in_parent.push(child_idx);
7789                m.children_length.push(node.children.len());
7790                let child_var = compile_node_c(child_node, component_map, css, m, counter, out)?;
7791                let _ = writeln!(out, "    AzDom_addChild(&{var}, {child_var});");
7792            }
7793            XmlNodeChild::Text(text) => {
7794                if ctor.consumes_text() {
7795                    continue;
7796                }
7797                let text = text.trim();
7798                if !text.is_empty() {
7799                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7800                    let _ = writeln!(out,
7801                        "    AzDom_addChild(&{var}, AzDom_createText(AZ_STR(\"{esc}\")));"
7802                    );
7803                }
7804            }
7805        }
7806    }
7807    Ok(var)
7808}
7809
7810/// Compile a full HTML page to a compilable **C** Azul app.
7811#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7812/// # Errors
7813///
7814/// Returns an error if the XML cannot be parsed or compiled to C code.
7815pub fn str_to_c_code<'a>(
7816    root_nodes: &'a [XmlNodeChild],
7817    component_map: &'a ComponentMap,
7818) -> Result<String, CompileError> {
7819    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7820    let mut body = String::new();
7821    let mut counter = 0usize;
7822
7823    // Emit the body as the root node, then its children.
7824    let root = alloc::format!("n{counter}");
7825    counter += 1;
7826    let _ = writeln!(body, "    AzDom {root} = AzDom_createBody();");
7827
7828    let mut matcher = body_matcher(body_node);
7829    matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
7830    let classes: Vec<String> = body_node.attributes.get_key("class")
7831        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7832        .unwrap_or_default();
7833    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7834    let blocks = get_css_blocks(&global_style, &matcher);
7835    if !blocks.is_empty() {
7836        let inline_css = css_blocks_to_inline_string(&blocks);
7837        if !inline_css.is_empty() {
7838            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7839            let _ = writeln!(body, "    {root} = AzDom_withCss({root}, AZ_STR(\"{esc}\"));");
7840        }
7841    }
7842    for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
7843        match child {
7844            XmlNodeChild::Element(child_node) => {
7845                let mut m = matcher.clone();
7846                m.path.push(CssPathSelector::Children);
7847                m.indices_in_parent.push(child_idx);
7848                m.children_length.push(body_node.children.len());
7849                let child_var = compile_node_c(child_node, component_map, &global_style, m, &mut counter, &mut body)?;
7850                let _ = writeln!(body, "    AzDom_addChild(&{root}, {child_var});");
7851            }
7852            XmlNodeChild::Text(text) => {
7853                let text = text.trim();
7854                if !text.is_empty() {
7855                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7856                    let _ = writeln!(body,
7857                        "    AzDom_addChild(&{root}, AzDom_createText(AZ_STR(\"{esc}\")));"
7858                    );
7859                }
7860            }
7861        }
7862    }
7863
7864    Ok(alloc::format!(
7865        "/* Auto-generated UI source code (C). Build:\n\
7866         *   clang -I <azul>/target/codegen main.c -lazul\n */\n\
7867         #include \"azul.h\"\n\
7868         #include <string.h>\n\
7869         #define AZ_STR(s) AzString_copyFromBytes((const uint8_t*)(s), 0, strlen(s))\n\n\
7870         AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n\
7871         {body}    return {root};\n}}\n\n\
7872         int main(void) {{\n    \
7873         AzString data_type = AZ_STR(\"Data\");\n    \
7874         AzRefAny data = AzRefAny_newC((AzGlVoidPtrConst){{ .ptr = NULL }}, 0, 1, 0, data_type, NULL, 0, 0);\n    \
7875         AzApp app = AzApp_create(data, AzAppConfig_create());\n    \
7876         AzWindowCreateOptions window = AzWindowCreateOptions_create(render);\n    \
7877         AzApp_run(&app, window);\n    \
7878         AzApp_delete(&app);\n    \
7879         return 0;\n}}\n"
7880    ))
7881}
7882
7883#[cfg(test)]
7884mod tests {
7885    use super::*;
7886    use crate::dom::{Dom, NodeType};
7887
7888    #[test]
7889    fn test_inline_span_parsing() {
7890        // This test verifies that HTML with inline spans is parsed correctly
7891        // The DOM structure should preserve text nodes before, inside, and after the span
7892
7893        let html = r#"<p>Text before <span class="highlight">inline text</span> text after.</p>"#;
7894
7895        // Expected DOM structure:
7896        // <p>
7897        //   ├─ TextNode: "Text before "
7898        //   ├─ <span class="highlight">
7899        //   │   └─ TextNode: "inline text"
7900        //   └─ TextNode: " text after."
7901
7902        // For this test, we'll create the DOM structure manually
7903        // since we're testing the parsing logic
7904        let expected_dom = Dom::create_p().with_children(
7905            vec![
7906                Dom::create_text("Text before "),
7907                Dom::create_node(NodeType::Span)
7908                    .with_children(vec![Dom::create_text("inline text")].into()),
7909                Dom::create_text(" text after."),
7910            ]
7911            .into(),
7912        );
7913
7914        // Verify the structure has 3 children at the top level
7915        assert_eq!(expected_dom.children.as_ref().len(), 3);
7916
7917        // Verify the middle child is a span
7918        match &expected_dom.children.as_ref()[1].root.node_type {
7919            NodeType::Span => {}
7920            other => panic!("Expected Span, got {:?}", other),
7921        }
7922
7923        // Verify the span has 1 child (the text node)
7924        assert_eq!(expected_dom.children.as_ref()[1].children.as_ref().len(), 1);
7925
7926        println!("Test passed: Inline span parsing structure is correct");
7927    }
7928
7929    #[test]
7930    fn test_xml_node_structure() {
7931        // Test the basic XmlNode structure to ensure text content is preserved
7932        // Updated to use XmlNodeChild enum (Text/Element)
7933
7934        let node = XmlNode {
7935            node_type: "p".into(),
7936            attributes: XmlAttributeMap {
7937                inner: StringPairVec::from_const_slice(&[]),
7938            },
7939            children: vec![
7940                XmlNodeChild::Text("Before ".into()),
7941                XmlNodeChild::Element(XmlNode {
7942                    node_type: "span".into(),
7943                    children: vec![XmlNodeChild::Text("inline".into())].into(),
7944                    ..Default::default()
7945                }),
7946                XmlNodeChild::Text(" after".into()),
7947            ]
7948            .into(),
7949        };
7950
7951        // Verify structure
7952        assert_eq!(node.children.as_ref().len(), 3);
7953        assert_eq!(node.children.as_ref()[0].as_text(), Some("Before "));
7954        assert_eq!(
7955            node.children.as_ref()[1]
7956                .as_element()
7957                .unwrap()
7958                .node_type
7959                .as_str(),
7960            "span"
7961        );
7962        assert_eq!(node.children.as_ref()[2].as_text(), Some(" after"));
7963
7964        // Verify span's child
7965        let span = node.children.as_ref()[1].as_element().unwrap();
7966        assert_eq!(span.children.as_ref().len(), 1);
7967        assert_eq!(span.children.as_ref()[0].as_text(), Some("inline"));
7968
7969        println!("Test passed: XmlNode structure preserves text nodes correctly");
7970    }
7971
7972    #[test]
7973    fn test_img_tag_becomes_image_node_with_src_tag() {
7974        // `<img src="cat.jpg" width="300" height="169">` must become a
7975        // `NodeType::Image` whose `NullImage` carries the `src` string as its
7976        // `tag` (so a renderer can resolve the bytes later), plus the declared
7977        // intrinsic size.
7978        use crate::resources::DecodedImage;
7979        use crate::window::{AzStringPair, StringPairVec};
7980
7981        let img_node = XmlNode {
7982            node_type: "img".into(),
7983            attributes: XmlAttributeMap::from(StringPairVec::from_vec(alloc::vec![
7984                AzStringPair {
7985                    key: "src".into(),
7986                    value: "cat.jpg".into()
7987                },
7988                AzStringPair {
7989                    key: "width".into(),
7990                    value: "300".into()
7991                },
7992                AzStringPair {
7993                    key: "height".into(),
7994                    value: "169".into()
7995                },
7996            ])),
7997            children: Vec::new().into(),
7998        };
7999
8000        let component_map = ComponentMap::default();
8001        let dom = xml_node_to_dom_fast(&img_node, &component_map, false, 0)
8002            .expect("xml_node_to_dom_fast for <img> should succeed");
8003
8004        match dom.root.get_node_type() {
8005            NodeType::Image(image_ref) => match image_ref.as_ref().get_data() {
8006                DecodedImage::NullImage {
8007                    tag, width, height, ..
8008                } => {
8009                    assert_eq!(
8010                        core::str::from_utf8(tag).unwrap(),
8011                        "cat.jpg",
8012                        "image tag must carry the src string"
8013                    );
8014                    assert_eq!(*width, 300, "width attribute should set intrinsic width");
8015                    assert_eq!(*height, 169, "height attribute should set intrinsic height");
8016                }
8017                other => panic!("expected NullImage carrying the src tag, got {:?}", other),
8018            },
8019            other => panic!("expected NodeType::Image for <img>, got {:?}", other),
8020        }
8021
8022        println!("Test passed: <img src=\"cat.jpg\"> -> NodeType::Image tagged \"cat.jpg\"");
8023    }
8024
8025    #[test]
8026    fn test_tag_to_node_type_img_is_image() {
8027        // The bare tag mapping should also yield an Image (placeholder, empty tag).
8028        match tag_to_node_type("img") {
8029            NodeType::Image(_) => {}
8030            other => panic!("tag_to_node_type(\"img\") should be Image, got {:?}", other),
8031        }
8032    }
8033
8034    /// Build a `<div>` nested `depth` levels deep, innermost first.
8035    fn nested_divs(depth: usize) -> XmlNode {
8036        let mut node = XmlNode {
8037            node_type: "div".into(),
8038            ..Default::default()
8039        };
8040        for _ in 0..depth {
8041            node = XmlNode {
8042                node_type: "div".into(),
8043                children: vec![XmlNodeChild::Element(node)].into(),
8044                ..Default::default()
8045            };
8046        }
8047        node
8048    }
8049
8050    /// AUDIT 2026-07-08: `extract_css_urls` used to slice the original string with
8051    /// a byte offset computed in a `to_lowercase()` temporary. On `'İ'` (whose
8052    /// lowercase is longer in bytes) that offset was misaligned. This must no
8053    /// longer panic and must still find the `@import` target.
8054    #[test]
8055    fn extract_css_urls_unicode_import_no_panic() {
8056        let mut res = Vec::new();
8057        Xml::extract_css_urls("İ@import 'x'", &mut res);
8058        assert_eq!(res.len(), 1, "should find the one @import target");
8059        assert_eq!(res[0].url.as_str(), "x");
8060    }
8061
8062    /// The `url(` and `@import` scans are case-insensitive after the audit fix.
8063    #[test]
8064    fn extract_css_urls_is_case_insensitive() {
8065        let mut res = Vec::new();
8066        Xml::extract_css_urls("body { background: URL(http://e.com/a.png); }", &mut res);
8067        assert_eq!(res.len(), 1);
8068        assert_eq!(res[0].url.as_str(), "http://e.com/a.png");
8069
8070        let mut res2 = Vec::new();
8071        Xml::extract_css_urls("@IMPORT \"theme.css\";", &mut res2);
8072        assert_eq!(res2.len(), 1);
8073        assert_eq!(res2[0].url.as_str(), "theme.css");
8074    }
8075
8076    /// AUDIT 2026-07-08: the resource scan recurses per nesting level; deep markup
8077    /// must not overflow the stack (deeper-than-cap subtrees are just not scanned).
8078    #[test]
8079    fn scan_external_resources_deep_nesting_ok() {
8080        let xml = Xml {
8081            root: vec![XmlNodeChild::Element(nested_divs(2000))].into(),
8082        };
8083        // Must simply return (no stack overflow); no resources in a plain tree.
8084        drop(xml.scan_external_resources());
8085    }
8086
8087    /// AUDIT 2026-07-08: the fast + tree DOM builders recurse per nesting level;
8088    /// deep markup must not overflow the stack (children beyond the cap are
8089    /// dropped, but the call returns `Ok`).
8090    #[test]
8091    fn xml_node_to_dom_fast_deep_nesting_ok() {
8092        let deep = nested_divs(2000);
8093        let component_map = ComponentMap::default();
8094        let dom = xml_node_to_dom_fast(&deep, &component_map, false, 0);
8095        assert!(dom.is_ok(), "deep DOM build must not overflow the stack");
8096
8097        let mut builder = CompactDomBuilder::new();
8098        let fast = xml_node_to_fast_dom(&deep, &component_map, false, &mut builder, 0);
8099        assert!(fast.is_ok(), "deep FastDom build must not overflow the stack");
8100    }
8101
8102    /// AUDIT 2026-07-08: `ComponentFieldType::parse` recurses through `Option<..>`
8103    /// / `Vec<..>` wrappers; an over-deep type string is rejected rather than
8104    /// overflowing the stack, while ordinary nesting still parses.
8105    #[test]
8106    fn component_field_type_parse_depth_capped() {
8107        let deep = format!("{}Bool{}", "Option<".repeat(4000), ">".repeat(4000));
8108        assert!(
8109            ComponentFieldType::parse(&deep).is_none(),
8110            "over-deep type string must be rejected, not overflow"
8111        );
8112
8113        let shallow = format!("{}Bool{}", "Option<".repeat(8), ">".repeat(8));
8114        assert!(
8115            ComponentFieldType::parse(&shallow).is_some(),
8116            "ordinary nesting must still parse"
8117        );
8118    }
8119
8120    /// AUDIT 2026-07-08: `prepare_string` now decodes the full common entity set
8121    /// plus numeric references, in a single pass so `&amp;` cannot double-decode.
8122    #[test]
8123    fn prepare_string_entity_decoding() {
8124        assert_eq!(prepare_string("a &amp; b"), "a & b");
8125        // `&amp;lt;` must yield the literal text "&lt;", not "<".
8126        assert_eq!(prepare_string("&amp;lt;"), "&lt;");
8127        assert_eq!(prepare_string("&quot;hi&quot;"), "\"hi\"");
8128        assert_eq!(prepare_string("&#65;&#66;"), "AB");
8129        assert_eq!(prepare_string("&#x41;"), "A");
8130        // Existing behavior preserved.
8131        assert_eq!(prepare_string("&lt;tag&gt;"), "<tag>");
8132    }
8133}
8134
8135#[cfg(test)]
8136#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
8137mod autotest_generated {
8138    use super::*;
8139    use crate::dom::{NodeData, NodeType};
8140    use azul_css::css::{CssNthChildPattern, CssNthChildSelector};
8141
8142    // ----------------------------------------------------------------- helpers
8143
8144    fn attrs(kv: &[(&str, &str)]) -> XmlAttributeMap {
8145        XmlAttributeMap::from(StringPairVec::from_vec(
8146            kv.iter()
8147                .map(|(k, v)| AzStringPair {
8148                    key: AzString::from(*k),
8149                    value: AzString::from(*v),
8150                })
8151                .collect::<Vec<_>>(),
8152        ))
8153    }
8154
8155    fn node(tag: &str, kv: &[(&str, &str)], children: Vec<XmlNodeChild>) -> XmlNode {
8156        XmlNode {
8157            node_type: tag.into(),
8158            attributes: attrs(kv),
8159            children: children.into(),
8160        }
8161    }
8162
8163    fn txt(s: &str) -> XmlNodeChild {
8164        XmlNodeChild::Text(AzString::from(s))
8165    }
8166
8167    fn elem(n: XmlNode) -> XmlNodeChild {
8168        XmlNodeChild::Element(n)
8169    }
8170
8171    /// `<html><head><style>{css}</style></head><body>{children}</body></html>`
8172    fn doc(css: &str, body_children: Vec<XmlNodeChild>) -> Vec<XmlNodeChild> {
8173        let style = node("style", &[], vec![txt(css)]);
8174        let head = node("head", &[], vec![elem(style)]);
8175        let body = node("body", &[], body_children);
8176        vec![elem(node("html", &[], vec![elem(head), elem(body)]))]
8177    }
8178
8179    fn no_args() -> ComponentArgumentVec {
8180        ComponentArgumentVec::from_const_slice(&[])
8181    }
8182
8183    fn dm(name: &str, fields: Vec<ComponentDataField>) -> ComponentDataModel {
8184        ComponentDataModel {
8185            name: AzString::from(name),
8186            description: AzString::from_const_str(""),
8187            fields: fields.into(),
8188        }
8189    }
8190
8191    fn user_def(css: &str, fields: Vec<ComponentDataField>) -> ComponentDef {
8192        ComponentDef {
8193            id: ComponentId::new("mylib", "widget"),
8194            display_name: AzString::from_const_str("Widget"),
8195            description: AzString::from_const_str(""),
8196            css: AzString::from(css),
8197            source: ComponentSource::UserDefined,
8198            data_model: dm("WidgetData", fields),
8199            render_fn: user_defined_render_fn,
8200            compile_fn: user_defined_compile_fn,
8201            render_fn_source: None.into(),
8202            compile_fn_source: None.into(),
8203        }
8204    }
8205
8206    /// A string that is long enough to smoke out O(n^2) / allocation blowups but
8207    /// still finishes fast in a debug-profile test run.
8208    const LONG: usize = 200_000;
8209
8210    // ================================================================
8211    // Xml::extract_url_value  (parser)
8212    // ================================================================
8213
8214    #[test]
8215    fn extract_url_value_empty_and_whitespace() {
8216        assert_eq!(Xml::extract_url_value(""), None);
8217        assert_eq!(Xml::extract_url_value("   "), None);
8218        assert_eq!(Xml::extract_url_value("\t\n\r "), None);
8219    }
8220
8221    #[test]
8222    fn extract_url_value_valid_minimal() {
8223        assert_eq!(
8224            Xml::extract_url_value("a.png)"),
8225            Some("a.png".to_string()),
8226            "unquoted url terminated by ')'"
8227        );
8228        assert_eq!(
8229            Xml::extract_url_value("\"a.png\")"),
8230            Some("a.png".to_string()),
8231            "double-quoted url"
8232        );
8233        assert_eq!(
8234            Xml::extract_url_value("'a.png')"),
8235            Some("a.png".to_string()),
8236            "single-quoted url"
8237        );
8238    }
8239
8240    #[test]
8241    fn extract_url_value_leading_trailing_junk_is_trimmed() {
8242        // Leading whitespace is trimmed by `trim_start`, inner padding by `trim`.
8243        assert_eq!(
8244            Xml::extract_url_value("   a.png   )tail"),
8245            Some("a.png".to_string())
8246        );
8247    }
8248
8249    #[test]
8250    fn extract_url_value_garbage_returns_none() {
8251        // Unterminated quote / no closing paren => None, never a panic.
8252        assert_eq!(Xml::extract_url_value("\"unterminated"), None);
8253        assert_eq!(Xml::extract_url_value("'unterminated"), None);
8254        assert_eq!(Xml::extract_url_value("no-closing-paren"), None);
8255        assert_eq!(Xml::extract_url_value("\u{0}\u{1}\u{7f}"), None);
8256    }
8257
8258    #[test]
8259    fn extract_url_value_boundary_numbers() {
8260        for s in [
8261            "0)",
8262            "-0)",
8263            "9223372036854775807)",
8264            "-9223372036854775808)",
8265            "NaN)",
8266            "inf)",
8267            "1e400)",
8268        ] {
8269            let got = Xml::extract_url_value(s);
8270            assert!(got.is_some(), "numeric-looking url {s:?} is still a url");
8271        }
8272        assert_eq!(Xml::extract_url_value("0)"), Some("0".to_string()));
8273    }
8274
8275    #[test]
8276    fn extract_url_value_unicode_no_panic() {
8277        // The ')' scan must land on a char boundary of the ORIGINAL string.
8278        assert_eq!(
8279            Xml::extract_url_value("\u{1F600}\u{0301})"),
8280            Some("\u{1F600}\u{0301}".to_string())
8281        );
8282        assert_eq!(Xml::extract_url_value("\"\u{130}\")"), Some("\u{130}".to_string()));
8283        assert_eq!(Xml::extract_url_value("\u{1F600}"), None);
8284    }
8285
8286    #[test]
8287    fn extract_url_value_extremely_long_terminates() {
8288        let s = "a".repeat(LONG);
8289        assert_eq!(Xml::extract_url_value(&s), None, "no ')' anywhere => None");
8290        let s2 = format!("{})", "b".repeat(LONG));
8291        assert_eq!(Xml::extract_url_value(&s2).map(|v| v.len()), Some(LONG));
8292    }
8293
8294    #[test]
8295    fn extract_url_value_nested_brackets_no_stack_overflow() {
8296        // Not recursive, but confirm deeply "nested" input is handled iteratively.
8297        let s = "(".repeat(10_000);
8298        assert_eq!(Xml::extract_url_value(&s), None);
8299        let s2 = format!("{}{}", "(".repeat(10_000), ")");
8300        assert_eq!(Xml::extract_url_value(&s2), Some("(".repeat(10_000)));
8301    }
8302
8303    // ================================================================
8304    // Xml::extract_quoted_string  (parser)
8305    // ================================================================
8306
8307    #[test]
8308    fn extract_quoted_string_empty_whitespace_garbage() {
8309        assert_eq!(Xml::extract_quoted_string(""), None);
8310        assert_eq!(Xml::extract_quoted_string("   "), None);
8311        assert_eq!(Xml::extract_quoted_string("\t\n"), None);
8312        assert_eq!(Xml::extract_quoted_string("bare"), None);
8313        // Leading whitespace is NOT trimmed here (unlike extract_url_value).
8314        assert_eq!(Xml::extract_quoted_string("  \"x\""), None);
8315    }
8316
8317    #[test]
8318    fn extract_quoted_string_valid_minimal_and_empty_quotes() {
8319        assert_eq!(Xml::extract_quoted_string("\"x\""), Some("x".to_string()));
8320        assert_eq!(Xml::extract_quoted_string("'x'"), Some("x".to_string()));
8321        // An empty quoted string is Some(""), not None.
8322        assert_eq!(Xml::extract_quoted_string("\"\""), Some(String::new()));
8323        assert_eq!(Xml::extract_quoted_string("''"), Some(String::new()));
8324    }
8325
8326    #[test]
8327    fn extract_quoted_string_unterminated_is_none() {
8328        assert_eq!(Xml::extract_quoted_string("\"abc"), None);
8329        assert_eq!(Xml::extract_quoted_string("'abc"), None);
8330        // Mismatched quotes do not pair up.
8331        assert_eq!(Xml::extract_quoted_string("\"abc'"), None);
8332    }
8333
8334    #[test]
8335    fn extract_quoted_string_boundary_numbers_and_unicode() {
8336        assert_eq!(Xml::extract_quoted_string("\"0\""), Some("0".to_string()));
8337        assert_eq!(Xml::extract_quoted_string("\"-0\""), Some("-0".to_string()));
8338        assert_eq!(Xml::extract_quoted_string("\"NaN\""), Some("NaN".to_string()));
8339        assert_eq!(
8340            Xml::extract_quoted_string("\"\u{1F600}\u{0301}\""),
8341            Some("\u{1F600}\u{0301}".to_string())
8342        );
8343    }
8344
8345    #[test]
8346    fn extract_quoted_string_extremely_long_terminates() {
8347        let unterminated = format!("\"{}", "x".repeat(LONG));
8348        assert_eq!(Xml::extract_quoted_string(&unterminated), None);
8349        let terminated = format!("\"{}\"", "x".repeat(LONG));
8350        assert_eq!(
8351            Xml::extract_quoted_string(&terminated).map(|s| s.len()),
8352            Some(LONG)
8353        );
8354    }
8355
8356    // ================================================================
8357    // Xml::parse_srcset  (parser)
8358    // ================================================================
8359
8360    #[test]
8361    fn parse_srcset_empty_and_whitespace_yield_no_urls() {
8362        assert!(Xml::parse_srcset("").is_empty());
8363        assert!(Xml::parse_srcset("   ").is_empty());
8364        assert!(Xml::parse_srcset("\t\n").is_empty());
8365        assert!(Xml::parse_srcset(",,,").is_empty(), "all-empty entries dropped");
8366    }
8367
8368    #[test]
8369    fn parse_srcset_valid_minimal() {
8370        assert_eq!(
8371            Xml::parse_srcset("a.png 1x, b.png 2x"),
8372            vec!["a.png".to_string(), "b.png".to_string()]
8373        );
8374        // No descriptor at all is still a valid single entry.
8375        assert_eq!(Xml::parse_srcset("a.png"), vec!["a.png".to_string()]);
8376    }
8377
8378    #[test]
8379    fn parse_srcset_garbage_and_boundary_numbers() {
8380        assert_eq!(Xml::parse_srcset("0, -0, NaN"), vec!["0", "-0", "NaN"]);
8381        // Garbage bytes still round out to "first whitespace-delimited token".
8382        assert_eq!(Xml::parse_srcset("\u{0}\u{7f} 1x"), vec!["\u{0}\u{7f}".to_string()]);
8383    }
8384
8385    #[test]
8386    fn parse_srcset_unicode_no_panic() {
8387        assert_eq!(
8388            Xml::parse_srcset("\u{1F600}.png 1x, \u{130}.png 2x"),
8389            vec!["\u{1F600}.png".to_string(), "\u{130}.png".to_string()]
8390        );
8391    }
8392
8393    #[test]
8394    fn parse_srcset_extremely_long_terminates() {
8395        let s = "a.png 1x,".repeat(20_000);
8396        assert_eq!(Xml::parse_srcset(&s).len(), 20_000);
8397        let one_huge = "a".repeat(LONG);
8398        assert_eq!(Xml::parse_srcset(&one_huge).len(), 1);
8399    }
8400
8401    // ================================================================
8402    // Xml::looks_like_resource / guess_kind_from_url / guess_mime_from_url
8403    // ================================================================
8404
8405    #[test]
8406    fn looks_like_resource_edges() {
8407        assert!(!Xml::looks_like_resource(""));
8408        assert!(!Xml::looks_like_resource("   "));
8409        assert!(!Xml::looks_like_resource("/about"));
8410        assert!(Xml::looks_like_resource("/a.PNG"), "case-insensitive");
8411        assert!(Xml::looks_like_resource("x.pdf"));
8412        // A query string defeats the extension check (documented consequence of
8413        // matching on `ends_with`).
8414        assert!(!Xml::looks_like_resource("x.png?v=1"));
8415        assert!(!Xml::looks_like_resource(&"a".repeat(LONG)));
8416    }
8417
8418    #[test]
8419    fn guess_kind_from_url_covers_every_bucket() {
8420        use ExternalResourceKind::*;
8421        assert_eq!(Xml::guess_kind_from_url(""), Unknown);
8422        assert_eq!(Xml::guess_kind_from_url("a.PNG"), Image);
8423        assert_eq!(Xml::guess_kind_from_url("a.woff2"), Font);
8424        assert_eq!(Xml::guess_kind_from_url("a.css"), Stylesheet);
8425        assert_eq!(Xml::guess_kind_from_url("a.mjs"), Script);
8426        assert_eq!(Xml::guess_kind_from_url("a.webm"), Video);
8427        assert_eq!(Xml::guess_kind_from_url("a.flac"), Audio);
8428        assert_eq!(Xml::guess_kind_from_url("a.ico"), Icon);
8429        // Query strings ARE stripped here (unlike looks_like_resource).
8430        assert_eq!(Xml::guess_kind_from_url("a.png?v=1"), Image);
8431        assert_eq!(Xml::guess_kind_from_url("\u{1F600}"), Unknown);
8432    }
8433
8434    #[test]
8435    fn guess_mime_from_url_empty_and_garbage() {
8436        assert_eq!(Xml::guess_mime_from_url("", ""), None);
8437        assert_eq!(Xml::guess_mime_from_url("   ", ""), None);
8438        assert_eq!(Xml::guess_mime_from_url("\u{0}\u{7f}", ""), None);
8439        assert_eq!(Xml::guess_mime_from_url("\u{1F600}", ""), None);
8440    }
8441
8442    #[test]
8443    fn guess_mime_from_url_valid_minimal_and_category_fallback() {
8444        let m = Xml::guess_mime_from_url("a.PNG", "").expect("png is a known extension");
8445        assert_eq!(m.inner.as_str(), "image/png");
8446        let m = Xml::guess_mime_from_url("a.png?v=1", "").expect("query string stripped");
8447        assert_eq!(m.inner.as_str(), "image/png");
8448        // Unknown extension + a category hint => the category wildcard.
8449        let m = Xml::guess_mime_from_url("/no-ext", "image").expect("category fallback");
8450        assert_eq!(m.inner.as_str(), "image/*");
8451        // Unknown category => None.
8452        assert_eq!(Xml::guess_mime_from_url("/no-ext", "bogus"), None);
8453    }
8454
8455    #[test]
8456    fn guess_mime_from_url_boundary_numbers_and_long() {
8457        assert_eq!(Xml::guess_mime_from_url("0", ""), None);
8458        assert_eq!(Xml::guess_mime_from_url("-0", ""), None);
8459        assert_eq!(Xml::guess_mime_from_url("NaN", ""), None);
8460        assert_eq!(Xml::guess_mime_from_url("inf", ""), None);
8461        let long = format!("{}.png", "a".repeat(LONG));
8462        assert_eq!(
8463            Xml::guess_mime_from_url(&long, "").map(|m| m.inner.as_str().to_string()),
8464            Some("image/png".to_string())
8465        );
8466    }
8467
8468    // ================================================================
8469    // Xml::extract_css_urls / scan_node / scan_external_resources
8470    // ================================================================
8471
8472    #[test]
8473    fn extract_css_urls_empty_and_garbage_no_panic() {
8474        let mut v = Vec::new();
8475        Xml::extract_css_urls("", &mut v);
8476        Xml::extract_css_urls("   ", &mut v);
8477        Xml::extract_css_urls("\u{0}\u{7f}\u{1F600}", &mut v);
8478        Xml::extract_css_urls("url(", &mut v);
8479        Xml::extract_css_urls("@import", &mut v);
8480        Xml::extract_css_urls("@import url(", &mut v);
8481        assert!(v.is_empty(), "no well-formed url in any of those inputs");
8482    }
8483
8484    #[test]
8485    fn extract_css_urls_valid_minimal() {
8486        let mut v = Vec::new();
8487        Xml::extract_css_urls("a { background: url('x.png'); }", &mut v);
8488        assert_eq!(v.len(), 1);
8489        assert_eq!(v[0].url.as_str(), "x.png");
8490        assert_eq!(v[0].kind, ExternalResourceKind::Image);
8491        assert_eq!(v[0].source_attribute.as_str(), "url()");
8492    }
8493
8494    #[test]
8495    fn extract_css_urls_import_is_tagged_as_stylesheet() {
8496        let mut v = Vec::new();
8497        Xml::extract_css_urls("@import url(theme.css);", &mut v);
8498        assert_eq!(v.len(), 1);
8499        assert_eq!(v[0].url.as_str(), "theme.css");
8500        assert_eq!(v[0].kind, ExternalResourceKind::Stylesheet);
8501        assert_eq!(v[0].source_attribute.as_str(), "@import");
8502    }
8503
8504    #[test]
8505    fn extract_css_urls_multibyte_before_url_no_panic() {
8506        // ASCII-only lowercasing keeps byte offsets 1:1 with the original.
8507        let mut v = Vec::new();
8508        Xml::extract_css_urls("\u{130}\u{1F600} URL(\"a.css\") \u{0301}", &mut v);
8509        assert_eq!(v.len(), 1);
8510        assert_eq!(v[0].url.as_str(), "a.css");
8511    }
8512
8513    #[test]
8514    fn extract_css_urls_extremely_long_terminates() {
8515        // Each iteration advances search_from past the "url(" it just matched, so
8516        // this must terminate (and not spin).
8517        let mut v = Vec::new();
8518        Xml::extract_css_urls(&"url(".repeat(2_000), &mut v);
8519        // No ')' anywhere => nothing extractable, but the scan still terminates.
8520        assert!(v.is_empty());
8521
8522        let mut v2 = Vec::new();
8523        Xml::extract_css_urls(&"url(a.png)".repeat(2_000), &mut v2);
8524        assert_eq!(v2.len(), 2_000);
8525    }
8526
8527    #[test]
8528    fn scan_node_on_empty_and_extreme_nodes_no_panic() {
8529        let mut v = Vec::new();
8530        Xml::scan_node(&XmlNode::default(), &mut v);
8531        Xml::scan_node(&node("", &[], vec![]), &mut v);
8532        Xml::scan_node(&node(&"a".repeat(10_000), &[("style", "url(x.png)")], vec![]), &mut v);
8533        assert_eq!(v.len(), 1, "only the inline style url()");
8534        assert_eq!(v[0].url.as_str(), "x.png");
8535    }
8536
8537    #[test]
8538    fn scan_node_img_srcset_and_background() {
8539        let mut v = Vec::new();
8540        Xml::scan_node(
8541            &node(
8542                "IMG",
8543                &[("src", "a.png"), ("srcset", "b.png 1x, c.png 2x"), ("background", "d.gif")],
8544                vec![],
8545            ),
8546            &mut v,
8547        );
8548        let urls: Vec<&str> = v.iter().map(|r| r.url.as_str()).collect();
8549        assert_eq!(urls, vec!["a.png", "b.png", "c.png", "d.gif"]);
8550        assert!(v.iter().all(|r| r.kind == ExternalResourceKind::Image));
8551    }
8552
8553    #[test]
8554    fn scan_external_resources_on_empty_document() {
8555        let xml = Xml {
8556            root: Vec::new().into(),
8557        };
8558        assert_eq!(xml.scan_external_resources().as_ref().len(), 0);
8559    }
8560
8561    #[test]
8562    fn scan_external_resources_finds_every_element_kind() {
8563        let xml = Xml {
8564            root: vec![
8565                elem(node("img", &[("src", "i.png")], vec![])),
8566                elem(node("link", &[("href", "s.css"), ("rel", "stylesheet")], vec![])),
8567                elem(node("script", &[("src", "s.js")], vec![])),
8568                elem(node("video", &[("src", "v.mp4"), ("poster", "p.jpg")], vec![])),
8569                elem(node("audio", &[("src", "a.mp3")], vec![])),
8570                elem(node("a", &[("href", "f.pdf")], vec![])),
8571                elem(node("a", &[("href", "/page")], vec![])),
8572            ]
8573            .into(),
8574        };
8575        let res = xml.scan_external_resources();
8576        let mut urls: Vec<&str> = res.as_ref().iter().map(|r| r.url.as_str()).collect();
8577        urls.sort_unstable();
8578        assert_eq!(
8579            urls,
8580            vec!["a.mp3", "f.pdf", "i.png", "p.jpg", "s.css", "s.js", "v.mp4"],
8581            "`/page` is not a resource and must be skipped"
8582        );
8583    }
8584
8585    // ================================================================
8586    // MimeTypeHint  (constructor)
8587    // ================================================================
8588
8589    #[test]
8590    fn mime_type_hint_new_no_panic_and_fields_match_args() {
8591        for s in ["", "   ", "text/css", "\u{1F600}", "\u{0}"] {
8592            assert_eq!(MimeTypeHint::new(s).inner.as_str(), s, "new() stores verbatim");
8593        }
8594        let long = "x".repeat(LONG);
8595        assert_eq!(MimeTypeHint::new(&long).inner.as_str().len(), LONG);
8596    }
8597
8598    #[test]
8599    fn mime_type_hint_from_extension_edges() {
8600        assert_eq!(
8601            MimeTypeHint::from_extension("").inner.as_str(),
8602            "application/octet-stream"
8603        );
8604        assert_eq!(
8605            MimeTypeHint::from_extension("PnG").inner.as_str(),
8606            "image/png",
8607            "extension match is case-insensitive"
8608        );
8609        assert_eq!(MimeTypeHint::from_extension("jpeg").inner.as_str(), "image/jpeg");
8610        assert_eq!(MimeTypeHint::from_extension("woff2").inner.as_str(), "font/woff2");
8611        assert_eq!(
8612            MimeTypeHint::from_extension("\u{1F600}").inner.as_str(),
8613            "application/octet-stream"
8614        );
8615        assert_eq!(
8616            MimeTypeHint::from_extension(&"z".repeat(LONG)).inner.as_str(),
8617            "application/octet-stream"
8618        );
8619    }
8620
8621    // ================================================================
8622    // ComponentId  (constructor / getter)
8623    // ================================================================
8624
8625    #[test]
8626    fn component_id_builtin_and_new_invariants() {
8627        let b = ComponentId::builtin("div");
8628        assert_eq!(b.collection.as_str(), "builtin");
8629        assert_eq!(b.name.as_str(), "div");
8630
8631        let c = ComponentId::new("", "");
8632        assert_eq!(c.collection.as_str(), "");
8633        assert_eq!(c.name.as_str(), "");
8634
8635        let u = ComponentId::new("\u{1F600}", "\u{130}");
8636        assert_eq!(u.collection.as_str(), "\u{1F600}");
8637        assert_eq!(u.name.as_str(), "\u{130}");
8638    }
8639
8640    #[test]
8641    fn component_id_qualified_name_roundtrips_through_the_map_lookup() {
8642        assert_eq!(ComponentId::builtin("div").qualified_name(), "builtin:div");
8643        assert_eq!(ComponentId::new("", "").qualified_name(), ":");
8644        // A name that itself contains ':' makes the qualified name ambiguous —
8645        // pin the (lossy) behavior so a change is noticed.
8646        assert_eq!(ComponentId::new("a", "b:c").qualified_name(), "a:b:c");
8647    }
8648
8649    // ================================================================
8650    // ComponentFieldTypeBox / ComponentFieldValueBox  (constructor / getter)
8651    // ================================================================
8652
8653    #[test]
8654    fn component_field_type_box_new_as_ref_and_clone() {
8655        let b = ComponentFieldTypeBox::new(ComponentFieldType::Bool);
8656        assert!(!b.ptr.is_null());
8657        assert_eq!(*b.as_ref(), ComponentFieldType::Bool);
8658
8659        let c = b.clone();
8660        assert_eq!(*c.as_ref(), ComponentFieldType::Bool);
8661        assert_ne!(b.ptr, c.ptr, "clone must deep-copy, not alias");
8662        assert_eq!(b, c, "PartialEq compares pointees");
8663        drop(c);
8664        assert_eq!(*b.as_ref(), ComponentFieldType::Bool, "original survives");
8665    }
8666
8667    #[test]
8668    fn component_field_type_box_nested_deeply_drops_cleanly() {
8669        let mut t = ComponentFieldType::Bool;
8670        for _ in 0..64 {
8671            t = ComponentFieldType::OptionType(ComponentFieldTypeBox::new(t));
8672        }
8673        assert_eq!(t.format(), format!("{}Bool{}", "Option<".repeat(64), ">".repeat(64)));
8674        drop(t);
8675    }
8676
8677    #[test]
8678    fn component_field_value_box_new_as_ref_and_clone() {
8679        let v = ComponentFieldValueBox::new(ComponentFieldValue::I32(i32::MIN));
8680        assert!(!v.ptr.is_null());
8681        assert_eq!(*v.as_ref(), ComponentFieldValue::I32(i32::MIN));
8682        let c = v.clone();
8683        assert_ne!(v.ptr, c.ptr);
8684        assert_eq!(v, c);
8685    }
8686
8687    // ================================================================
8688    // ComponentFieldType::parse / parse_depth / format  (round-trip)
8689    // ================================================================
8690
8691    #[test]
8692    fn component_field_type_parse_empty_whitespace_garbage() {
8693        assert_eq!(ComponentFieldType::parse(""), None);
8694        assert_eq!(ComponentFieldType::parse("   "), None);
8695        assert_eq!(ComponentFieldType::parse("\t\n"), None);
8696        assert_eq!(ComponentFieldType::parse("lowercase"), None);
8697        assert_eq!(ComponentFieldType::parse("\u{0}\u{7f}"), None);
8698        assert_eq!(ComponentFieldType::parse("Option<>"), None, "empty inner rejected");
8699        assert_eq!(ComponentFieldType::parse("Vec<>"), None);
8700        assert_eq!(ComponentFieldType::parse("Option<lowercase>"), None);
8701    }
8702
8703    #[test]
8704    fn component_field_type_parse_valid_minimal_and_trimming() {
8705        assert_eq!(ComponentFieldType::parse("String"), Some(ComponentFieldType::String));
8706        assert_eq!(
8707            ComponentFieldType::parse("  String  "),
8708            Some(ComponentFieldType::String),
8709            "leading/trailing whitespace is trimmed"
8710        );
8711        assert_eq!(ComponentFieldType::parse("bool"), Some(ComponentFieldType::Bool));
8712        assert_eq!(ComponentFieldType::parse("usize"), Some(ComponentFieldType::Usize));
8713        assert_eq!(
8714            ComponentFieldType::parse("StructRef(Foo)"),
8715            Some(ComponentFieldType::StructRef(AzString::from("Foo")))
8716        );
8717        assert_eq!(
8718            ComponentFieldType::parse("EnumRef(Foo)"),
8719            Some(ComponentFieldType::EnumRef(AzString::from("Foo")))
8720        );
8721        assert_eq!(
8722            ComponentFieldType::parse("RefAny"),
8723            Some(ComponentFieldType::RefAny(AzString::from("")))
8724        );
8725    }
8726
8727    #[test]
8728    fn component_field_type_parse_leading_trailing_junk_is_rejected_or_absorbed() {
8729        // Trailing junk after a known keyword falls through to the
8730        // "starts uppercase => StructRef" catch-all rather than being rejected.
8731        assert_eq!(
8732            ComponentFieldType::parse("String;garbage"),
8733            Some(ComponentFieldType::StructRef(AzString::from("String;garbage")))
8734        );
8735        // Lowercase junk has no uppercase first char => rejected.
8736        assert_eq!(ComponentFieldType::parse("string;garbage"), None);
8737    }
8738
8739    #[test]
8740    fn component_field_type_parse_boundary_numbers() {
8741        for s in ["0", "-0", "9223372036854775807", "-9223372036854775808", "1e400", "inf"] {
8742            assert_eq!(
8743                ComponentFieldType::parse(s),
8744                None,
8745                "numeric literal {s:?} is not a type name"
8746            );
8747        }
8748        // ...but anything starting with an uppercase letter hits the StructRef
8749        // catch-all, so "NaN" parses as a struct reference rather than failing.
8750        assert_eq!(
8751            ComponentFieldType::parse("NaN"),
8752            Some(ComponentFieldType::StructRef(AzString::from("NaN")))
8753        );
8754    }
8755
8756    #[test]
8757    fn component_field_type_parse_unicode_no_panic() {
8758        assert_eq!(ComponentFieldType::parse("\u{1F600}"), None, "emoji is not uppercase");
8759        // A real uppercase non-ASCII letter hits the StructRef catch-all.
8760        assert_eq!(
8761            ComponentFieldType::parse("\u{0391}bc"),
8762            Some(ComponentFieldType::StructRef(AzString::from("\u{0391}bc")))
8763        );
8764    }
8765
8766    #[test]
8767    fn component_field_type_parse_depth_boundary_is_exact() {
8768        // MAX_TYPE_PARSE_DEPTH wrappers parse; one more is rejected.
8769        let ok = format!(
8770            "{}Bool{}",
8771            "Option<".repeat(MAX_TYPE_PARSE_DEPTH),
8772            ">".repeat(MAX_TYPE_PARSE_DEPTH)
8773        );
8774        assert!(
8775            ComponentFieldType::parse(&ok).is_some(),
8776            "exactly MAX_TYPE_PARSE_DEPTH wrappers must still parse"
8777        );
8778
8779        let too_deep = format!(
8780            "{}Bool{}",
8781            "Option<".repeat(MAX_TYPE_PARSE_DEPTH + 1),
8782            ">".repeat(MAX_TYPE_PARSE_DEPTH + 1)
8783        );
8784        assert_eq!(
8785            ComponentFieldType::parse(&too_deep),
8786            None,
8787            "one wrapper past the cap must be rejected, not overflow"
8788        );
8789    }
8790
8791    #[test]
8792    fn component_field_type_parse_depth_direct_call_honors_start_depth() {
8793        assert_eq!(
8794            ComponentFieldType::parse_depth("Bool", MAX_TYPE_PARSE_DEPTH),
8795            Some(ComponentFieldType::Bool),
8796            "depth == cap is still allowed"
8797        );
8798        assert_eq!(
8799            ComponentFieldType::parse_depth("Bool", MAX_TYPE_PARSE_DEPTH + 1),
8800            None
8801        );
8802        assert_eq!(
8803            ComponentFieldType::parse_depth("Bool", usize::MAX),
8804            None,
8805            "usize::MAX start depth must not overflow, just refuse"
8806        );
8807    }
8808
8809    #[test]
8810    fn component_field_type_parse_nested_recursion_does_not_stack_overflow() {
8811        let bomb = format!("{}Bool{}", "Vec<".repeat(50_000), ">".repeat(50_000));
8812        assert_eq!(ComponentFieldType::parse(&bomb), None);
8813    }
8814
8815    #[test]
8816    fn component_field_type_parse_extremely_long_terminates() {
8817        // A single 200k-char uppercase token becomes a StructRef of that name.
8818        let long = format!("A{}", "b".repeat(LONG));
8819        assert_eq!(
8820            ComponentFieldType::parse(&long),
8821            Some(ComponentFieldType::StructRef(AzString::from(long.as_str())))
8822        );
8823    }
8824
8825    #[test]
8826    fn component_field_type_round_trip_representative() {
8827        let representative = vec![
8828            ComponentFieldType::String,
8829            ComponentFieldType::Bool,
8830            ComponentFieldType::I32,
8831            ComponentFieldType::I64,
8832            ComponentFieldType::U32,
8833            ComponentFieldType::U64,
8834            ComponentFieldType::Usize,
8835            ComponentFieldType::F32,
8836            ComponentFieldType::F64,
8837            ComponentFieldType::ColorU,
8838            ComponentFieldType::CssProperty,
8839            ComponentFieldType::ImageRef,
8840            ComponentFieldType::FontRef,
8841            ComponentFieldType::StyledDom,
8842            ComponentFieldType::StructRef(AzString::from("Foo")),
8843            ComponentFieldType::OptionType(ComponentFieldTypeBox::new(ComponentFieldType::Bool)),
8844            ComponentFieldType::VecType(ComponentFieldTypeBox::new(ComponentFieldType::I32)),
8845            ComponentFieldType::RefAny(AzString::from("")),
8846            ComponentFieldType::RefAny(AzString::from("Hint")),
8847            ComponentFieldType::Callback(ComponentCallbackSignature {
8848                return_type: AzString::from("Update"),
8849                args: Vec::new().into(),
8850            }),
8851        ];
8852        for x in representative {
8853            let s = x.format();
8854            assert_eq!(
8855                ComponentFieldType::parse(&s),
8856                Some(x.clone()),
8857                "parse(format({x:?})) must round-trip"
8858            );
8859        }
8860    }
8861
8862    #[test]
8863    fn component_field_type_round_trip_edge_values() {
8864        // Empty / unicode-bearing payloads.
8865        for x in [
8866            ComponentFieldType::StructRef(AzString::from("\u{0391}\u{1F600}")),
8867            ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
8868                ComponentFieldType::VecType(ComponentFieldTypeBox::new(
8869                    ComponentFieldType::StructRef(AzString::from("Foo")),
8870                )),
8871            )),
8872        ] {
8873            assert_eq!(ComponentFieldType::parse(&x.format()), Some(x.clone()));
8874        }
8875    }
8876
8877    #[test]
8878    fn component_field_type_format_is_an_idempotent_normalization() {
8879        // `EnumRef` and `StructRef` share the same canonical spelling, so parse()
8880        // collapses EnumRef -> StructRef. The normalization is still STABLE:
8881        // format(parse(format(x))) == format(x).
8882        let e = ComponentFieldType::EnumRef(AzString::from("Role"));
8883        let once = e.format();
8884        assert_eq!(once, "Role");
8885        let reparsed = ComponentFieldType::parse(&once).expect("parses");
8886        assert_eq!(
8887            reparsed,
8888            ComponentFieldType::StructRef(AzString::from("Role")),
8889            "EnumRef is lossy through format() — it comes back as StructRef"
8890        );
8891        assert_eq!(reparsed.format(), once, "but the normalization is idempotent");
8892    }
8893
8894    #[test]
8895    fn component_field_type_display_matches_format() {
8896        let t = ComponentFieldType::OptionType(ComponentFieldTypeBox::new(ComponentFieldType::F64));
8897        assert_eq!(format!("{t}"), t.format());
8898        assert_eq!(format!("{t}"), "Option<F64>");
8899    }
8900
8901    #[test]
8902    fn component_field_type_format_no_panic_on_empty_payloads() {
8903        let t = ComponentFieldType::Callback(ComponentCallbackSignature {
8904            return_type: AzString::from(""),
8905            args: Vec::new().into(),
8906        });
8907        assert_eq!(t.format(), "Callback()");
8908        // Callback() with an empty signature round-trips.
8909        assert_eq!(ComponentFieldType::parse("Callback()"), Some(t));
8910    }
8911
8912    // ================================================================
8913    // ComponentFieldNamedValueVec::get_field / get_string  (parser-ish lookup)
8914    // ================================================================
8915
8916    fn named(name: &str, v: ComponentFieldValue) -> ComponentFieldNamedValue {
8917        ComponentFieldNamedValue {
8918            name: AzString::from(name),
8919            value: v,
8920        }
8921    }
8922
8923    fn named_vec() -> ComponentFieldNamedValueVec {
8924        vec![
8925            named("a", ComponentFieldValue::String(AzString::from("x"))),
8926            named("b", ComponentFieldValue::Bool(true)),
8927            named("", ComponentFieldValue::U64(u64::MAX)),
8928            named("\u{1F600}", ComponentFieldValue::String(AzString::from("emoji"))),
8929        ]
8930        .into()
8931    }
8932
8933    #[test]
8934    fn named_value_vec_get_field_valid_minimal() {
8935        let v = named_vec();
8936        assert_eq!(
8937            v.get_field("a"),
8938            Some(&ComponentFieldValue::String(AzString::from("x")))
8939        );
8940        assert_eq!(v.get_field("b"), Some(&ComponentFieldValue::Bool(true)));
8941    }
8942
8943    #[test]
8944    fn named_value_vec_get_field_empty_whitespace_garbage_unicode() {
8945        let v = named_vec();
8946        // An empty NAME is a legal key here — it matches the field literally named "".
8947        assert_eq!(v.get_field(""), Some(&ComponentFieldValue::U64(u64::MAX)));
8948        assert_eq!(v.get_field("   "), None);
8949        assert_eq!(v.get_field("\t\n"), None);
8950        assert_eq!(v.get_field("\u{0}\u{7f}"), None);
8951        assert!(v.get_field("\u{1F600}").is_some());
8952        assert_eq!(v.get_field(" a "), None, "no trimming: lookup is exact");
8953        assert_eq!(v.get_field("a;garbage"), None);
8954    }
8955
8956    #[test]
8957    fn named_value_vec_get_field_on_empty_vec_and_long_key() {
8958        let empty = ComponentFieldNamedValueVec::from_const_slice(&[]);
8959        assert_eq!(empty.get_field("a"), None);
8960        assert_eq!(empty.get_string("a"), None);
8961        assert_eq!(named_vec().get_field(&"z".repeat(LONG)), None);
8962    }
8963
8964    #[test]
8965    fn named_value_vec_get_string_only_matches_string_variant() {
8966        let v = named_vec();
8967        assert_eq!(v.get_string("a").map(AzString::as_str), Some("x"));
8968        assert_eq!(v.get_string("b"), None, "Bool is not a String");
8969        assert_eq!(v.get_string(""), None, "U64 is not a String");
8970        assert_eq!(v.get_string("missing"), None);
8971    }
8972
8973    #[test]
8974    fn named_value_vec_boundary_numeric_keys() {
8975        let v: ComponentFieldNamedValueVec = vec![
8976            named("0", ComponentFieldValue::I32(0)),
8977            named("-0", ComponentFieldValue::I32(i32::MIN)),
8978            named("9223372036854775807", ComponentFieldValue::I64(i64::MAX)),
8979            named("NaN", ComponentFieldValue::F32(f32::NAN)),
8980        ]
8981        .into();
8982        assert_eq!(v.get_field("0"), Some(&ComponentFieldValue::I32(0)));
8983        assert_eq!(v.get_field("-0"), Some(&ComponentFieldValue::I32(i32::MIN)));
8984        assert_eq!(
8985            v.get_field("9223372036854775807"),
8986            Some(&ComponentFieldValue::I64(i64::MAX))
8987        );
8988        // NaN != NaN, so only check the variant, not equality.
8989        assert!(matches!(v.get_field("NaN"), Some(ComponentFieldValue::F32(f)) if f.is_nan()));
8990    }
8991
8992    // ================================================================
8993    // ComponentDataModel::get_field / get_default_string / with_default
8994    // ================================================================
8995
8996    fn model_with_text() -> ComponentDataModel {
8997        dm(
8998            "M",
8999            vec![
9000                data_field(
9001                    "text",
9002                    ComponentFieldType::String,
9003                    Some(ComponentDefaultValue::String(AzString::from("hi"))),
9004                    "",
9005                ),
9006                data_field("count", ComponentFieldType::U32, Some(ComponentDefaultValue::U32(3)), ""),
9007                data_field("required_one", ComponentFieldType::String, None, ""),
9008            ],
9009        )
9010    }
9011
9012    #[test]
9013    fn data_model_get_field_valid_minimal_and_missing() {
9014        let m = model_with_text();
9015        assert!(m.get_field("text").is_some());
9016        assert!(m.get_field("count").is_some());
9017        assert!(m.get_field("missing").is_none());
9018        assert!(m.get_field("").is_none());
9019        assert!(m.get_field("   ").is_none());
9020        assert!(m.get_field(" text ").is_none(), "exact match, no trimming");
9021        assert!(m.get_field("\u{1F600}").is_none());
9022        assert!(m.get_field(&"z".repeat(LONG)).is_none());
9023    }
9024
9025    #[test]
9026    fn data_model_get_field_on_empty_model() {
9027        let m = dm("Empty", Vec::new());
9028        assert!(m.get_field("anything").is_none());
9029        assert!(m.get_default_string("anything").is_none());
9030    }
9031
9032    #[test]
9033    fn data_model_get_default_string_only_for_string_defaults() {
9034        let m = model_with_text();
9035        assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("hi"));
9036        assert_eq!(m.get_default_string("count"), None, "U32 default is not a String");
9037        assert_eq!(m.get_default_string("required_one"), None, "no default at all");
9038        assert_eq!(m.get_default_string("missing"), None);
9039    }
9040
9041    #[test]
9042    fn data_model_required_flag_follows_default_presence() {
9043        let m = model_with_text();
9044        assert!(!m.get_field("text").unwrap().required);
9045        assert!(
9046            m.get_field("required_one").unwrap().required,
9047            "a field with no default must be marked required"
9048        );
9049    }
9050
9051    #[test]
9052    fn data_model_with_default_overrides_and_preserves_len() {
9053        let m = model_with_text();
9054        let before = m.fields.as_ref().len();
9055        let m = m.with_default("text", ComponentDefaultValue::String(AzString::from("bye")));
9056        assert_eq!(m.fields.as_ref().len(), before, "len is preserved");
9057        assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("bye"));
9058    }
9059
9060    #[test]
9061    fn data_model_with_default_on_missing_field_is_a_no_op() {
9062        let m = model_with_text();
9063        let m = m.with_default("nope", ComponentDefaultValue::Bool(true));
9064        assert_eq!(m.fields.as_ref().len(), 3);
9065        assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("hi"));
9066        assert!(m.get_field("nope").is_none(), "no field is inserted");
9067    }
9068
9069    #[test]
9070    fn data_model_with_default_extreme_names_and_values_no_panic() {
9071        let m = model_with_text()
9072            .with_default("", ComponentDefaultValue::None)
9073            .with_default(&"z".repeat(10_000), ComponentDefaultValue::F64(f64::NAN))
9074            .with_default("count", ComponentDefaultValue::Usize(usize::MAX))
9075            .with_default("text", ComponentDefaultValue::I64(i64::MIN));
9076        assert_eq!(m.fields.as_ref().len(), 3);
9077        assert!(matches!(
9078            m.get_field("count").unwrap().default_value,
9079            OptionComponentDefaultValue::Some(ComponentDefaultValue::Usize(usize::MAX))
9080        ));
9081        assert_eq!(
9082            m.get_default_string("text"),
9083            None,
9084            "text is now an I64 default, no longer a String"
9085        );
9086    }
9087
9088    #[test]
9089    fn data_model_with_default_fills_only_the_first_match() {
9090        // Duplicate field names: `with_default` breaks after the first hit.
9091        let m = dm(
9092            "Dup",
9093            vec![
9094                data_field("x", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from("1"))), ""),
9095                data_field("x", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from("2"))), ""),
9096            ],
9097        )
9098        .with_default("x", ComponentDefaultValue::String(AzString::from("3")));
9099        let vals: Vec<&str> = m
9100            .fields
9101            .as_ref()
9102            .iter()
9103            .filter_map(|f| match &f.default_value {
9104                OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s.as_str()),
9105                _ => None,
9106            })
9107            .collect();
9108        assert_eq!(vals, vec!["3", "2"], "only the first duplicate is overridden");
9109    }
9110
9111    // ================================================================
9112    // ComponentSource / ComponentMap  (constructor / getter / lookup)
9113    // ================================================================
9114
9115    #[test]
9116    fn component_source_create_is_user_defined() {
9117        assert_eq!(ComponentSource::create(), ComponentSource::UserDefined);
9118        assert_eq!(ComponentSource::default(), ComponentSource::UserDefined);
9119    }
9120
9121    #[test]
9122    fn component_map_create_is_empty_and_all_lookups_return_none() {
9123        let m = ComponentMap::create();
9124        assert_eq!(m.libraries.as_ref().len(), 0);
9125        assert!(m.get("builtin", "div").is_none());
9126        assert!(m.get_unqualified("div").is_none());
9127        assert!(m.get_by_qualified_name("builtin:div").is_none());
9128        assert!(m.all_components().is_empty());
9129        assert!(m.get_exportable_libraries().is_empty());
9130    }
9131
9132    #[test]
9133    fn component_map_with_builtin_invariants() {
9134        let m = ComponentMap::with_builtin();
9135        assert_eq!(m.libraries.as_ref().len(), 1);
9136        let lib = &m.libraries.as_ref()[0];
9137        assert_eq!(lib.name.as_str(), "builtin");
9138        assert!(!lib.exportable, "builtins must never be exportable");
9139        assert!(!lib.modifiable, "builtins must never be modifiable");
9140        assert_eq!(
9141            m.all_components().len(),
9142            lib.components.as_ref().len(),
9143            "all_components must see every registered component"
9144        );
9145        assert!(
9146            m.get_exportable_libraries().is_empty(),
9147            "the builtin library is not exportable"
9148        );
9149    }
9150
9151    #[test]
9152    fn component_map_get_valid_minimal() {
9153        let m = ComponentMap::with_builtin();
9154        assert!(m.get("builtin", "div").is_some());
9155        assert!(m.get("builtin", "if").is_some());
9156        assert!(m.get("builtin", "for").is_some());
9157        assert!(m.get("builtin", "map").is_some());
9158        assert_eq!(
9159            m.get("builtin", "div").unwrap().id.qualified_name(),
9160            "builtin:div"
9161        );
9162    }
9163
9164    #[test]
9165    fn component_map_get_empty_whitespace_garbage_unicode() {
9166        let m = ComponentMap::with_builtin();
9167        assert!(m.get("", "").is_none());
9168        assert!(m.get("builtin", "").is_none());
9169        assert!(m.get("", "div").is_none());
9170        assert!(m.get("builtin", "   ").is_none());
9171        assert!(m.get("builtin", " div ").is_none(), "no trimming");
9172        assert!(m.get("builtin", "DIV").is_none(), "lookup is case-sensitive");
9173        assert!(m.get("builtin", "\u{1F600}").is_none());
9174        assert!(m.get("builtin", "\u{0}\u{7f}").is_none());
9175        assert!(m.get("builtin", "div;garbage").is_none());
9176    }
9177
9178    #[test]
9179    fn component_map_get_extremely_long_name_terminates() {
9180        let m = ComponentMap::with_builtin();
9181        assert!(m.get(&"a".repeat(LONG), &"b".repeat(LONG)).is_none());
9182        assert!(m.get_unqualified(&"b".repeat(LONG)).is_none());
9183        assert!(m.get_by_qualified_name(&"c".repeat(LONG)).is_none());
9184    }
9185
9186    #[test]
9187    fn component_map_get_unqualified_only_searches_builtin() {
9188        let lib = ComponentLibrary {
9189            name: AzString::from("mylib"),
9190            version: AzString::from("1.0.0"),
9191            description: AzString::from(""),
9192            components: vec![user_def("", Vec::new())].into(),
9193            exportable: true,
9194            modifiable: true,
9195            data_models: Vec::new().into(),
9196            enum_models: Vec::new().into(),
9197        };
9198        let libs: ComponentLibraryVec = vec![lib].into();
9199        let m = ComponentMap::from_libraries(&libs);
9200
9201        assert!(m.get("mylib", "widget").is_some());
9202        assert!(
9203            m.get_unqualified("widget").is_none(),
9204            "unqualified lookup must NOT reach non-builtin libraries"
9205        );
9206        assert!(m.get_by_qualified_name("mylib:widget").is_some());
9207        assert_eq!(m.get_exportable_libraries().len(), 1);
9208        assert_eq!(m.all_components().len(), 1);
9209    }
9210
9211    #[test]
9212    fn component_map_get_by_qualified_name_boundary_forms() {
9213        let m = ComponentMap::with_builtin();
9214        // No colon => falls back to the builtin library.
9215        assert!(m.get_by_qualified_name("div").is_some());
9216        // Exactly one colon.
9217        assert!(m.get_by_qualified_name("builtin:div").is_some());
9218        // Splits on the FIRST colon, so the remainder (incl. colons) is the name.
9219        assert!(m.get_by_qualified_name("builtin:div:extra").is_none());
9220        assert!(m.get_by_qualified_name(":").is_none());
9221        assert!(m.get_by_qualified_name(":div").is_none());
9222        assert!(m.get_by_qualified_name("builtin:").is_none());
9223        assert!(m.get_by_qualified_name("").is_none());
9224        assert!(m.get_by_qualified_name("   ").is_none());
9225    }
9226
9227    #[test]
9228    fn component_map_from_libraries_clones_without_losing_entries() {
9229        let src = ComponentMap::with_builtin();
9230        let copy = ComponentMap::from_libraries(&src.libraries);
9231        assert_eq!(copy.all_components().len(), src.all_components().len());
9232        assert!(copy.get_unqualified("div").is_some());
9233    }
9234
9235    #[test]
9236    fn register_builtin_components_is_stable_across_calls() {
9237        let a = register_builtin_components();
9238        let b = register_builtin_components();
9239        assert_eq!(a.components.as_ref().len(), b.components.as_ref().len());
9240        assert!(a.components.as_ref().len() > 50);
9241        assert_eq!(a.name.as_str(), "builtin");
9242        assert_eq!(a.version.as_str(), "1.0.0");
9243        // Every component must be namespaced into "builtin".
9244        assert!(a
9245            .components
9246            .as_ref()
9247            .iter()
9248            .all(|c| c.id.collection.as_str() == "builtin"));
9249        assert!(a
9250            .components
9251            .as_ref()
9252            .iter()
9253            .all(|c| c.source == ComponentSource::Builtin));
9254    }
9255
9256    // ================================================================
9257    // XmlNodeChild / XmlNode  (getter / predicate / constructor)
9258    // ================================================================
9259
9260    #[test]
9261    fn xml_node_child_as_text_and_as_element_are_mutually_exclusive() {
9262        let t = txt("hello");
9263        assert_eq!(t.as_text(), Some("hello"));
9264        assert!(t.as_element().is_none());
9265
9266        let e = elem(node("div", &[], vec![]));
9267        assert!(e.as_text().is_none());
9268        assert_eq!(e.as_element().map(|n| n.node_type.as_str()), Some("div"));
9269    }
9270
9271    #[test]
9272    fn xml_node_child_as_text_edge_values() {
9273        assert_eq!(txt("").as_text(), Some(""), "an empty text node is still text");
9274        assert_eq!(txt("   ").as_text(), Some("   "), "no trimming in the getter");
9275        assert_eq!(txt("\u{1F600}\u{0301}").as_text(), Some("\u{1F600}\u{0301}"));
9276        assert_eq!(txt("\u{0}").as_text(), Some("\u{0}"));
9277    }
9278
9279    #[test]
9280    fn xml_node_child_as_element_mut_allows_mutation_and_rejects_text() {
9281        let mut e = elem(node("div", &[], vec![]));
9282        e.as_element_mut().expect("is an element").node_type = "span".into();
9283        assert_eq!(e.as_element().map(|n| n.node_type.as_str()), Some("span"));
9284
9285        let mut t = txt("x");
9286        assert!(t.as_element_mut().is_none(), "text nodes have no element");
9287    }
9288
9289    #[test]
9290    fn xml_node_create_and_with_children_invariants() {
9291        let n = XmlNode::create("div");
9292        assert_eq!(n.node_type.as_str(), "div");
9293        assert_eq!(n.children.as_ref().len(), 0);
9294        assert_eq!(n.attributes.as_ref().len(), 0);
9295
9296        let n = n.with_children(vec![txt("a"), elem(XmlNode::create("b"))]);
9297        assert_eq!(n.children.as_ref().len(), 2);
9298        assert_eq!(n.node_type.as_str(), "div", "tag survives with_children");
9299
9300        // with_children REPLACES, it does not append.
9301        let n = n.with_children(Vec::new());
9302        assert_eq!(n.children.as_ref().len(), 0);
9303    }
9304
9305    #[test]
9306    fn xml_node_create_extreme_tag_names_no_panic() {
9307        assert_eq!(XmlNode::create("").node_type.as_str(), "");
9308        assert_eq!(XmlNode::create("\u{1F600}").node_type.as_str(), "\u{1F600}");
9309        assert_eq!(
9310            XmlNode::create(&*"a".repeat(10_000)).node_type.as_str().len(),
9311            10_000
9312        );
9313    }
9314
9315    #[test]
9316    fn xml_node_get_text_content_concatenates_only_direct_text() {
9317        let n = node(
9318            "p",
9319            &[],
9320            vec![
9321                txt("a "),
9322                elem(node("span", &[], vec![txt("IGNORED")])),
9323                txt("b"),
9324            ],
9325        );
9326        assert_eq!(
9327            n.get_text_content(),
9328            "a b",
9329            "only DIRECT text children, nested element text is not included"
9330        );
9331        assert_eq!(XmlNode::default().get_text_content(), "");
9332        assert_eq!(node("p", &[], vec![txt(""), txt("")]).get_text_content(), "");
9333    }
9334
9335    #[test]
9336    fn xml_node_has_only_text_children_true_false_and_empty() {
9337        assert!(
9338            XmlNode::default().has_only_text_children(),
9339            "vacuously true for a childless node — callers must pair this with a text check"
9340        );
9341        assert!(node("p", &[], vec![txt("a"), txt("b")]).has_only_text_children());
9342        assert!(!node("p", &[], vec![txt("a"), elem(XmlNode::create("b"))]).has_only_text_children());
9343        assert!(!node("p", &[], vec![elem(XmlNode::create("b"))]).has_only_text_children());
9344    }
9345
9346    // ================================================================
9347    // get_html_node / get_body_node / find_node_by_type / find_attribute
9348    // ================================================================
9349
9350    #[test]
9351    fn get_html_node_empty_input_is_no_html_node() {
9352        assert_eq!(get_html_node(&[]), Err(DomXmlParseError::NoHtmlNode));
9353        assert_eq!(get_html_node(&[txt("just text")]), Err(DomXmlParseError::NoHtmlNode));
9354        assert_eq!(
9355            get_html_node(&[elem(XmlNode::create("div"))]),
9356            Err(DomXmlParseError::NoHtmlNode)
9357        );
9358    }
9359
9360    #[test]
9361    fn get_html_node_valid_minimal_and_case_insensitive() {
9362        let roots = vec![elem(XmlNode::create("HTML"))];
9363        assert!(get_html_node(&roots).is_ok(), "tag casing is normalized");
9364    }
9365
9366    #[test]
9367    fn get_html_node_rejects_multiple_roots() {
9368        let roots = vec![elem(XmlNode::create("html")), elem(XmlNode::create("html"))];
9369        assert_eq!(
9370            get_html_node(&roots),
9371            Err(DomXmlParseError::MultipleHtmlRootNodes)
9372        );
9373    }
9374
9375    #[test]
9376    fn get_body_node_empty_and_missing() {
9377        assert_eq!(get_body_node(&[]), Err(DomXmlParseError::NoBodyInHtml));
9378        assert_eq!(
9379            get_body_node(&[elem(XmlNode::create("head"))]),
9380            Err(DomXmlParseError::NoBodyInHtml)
9381        );
9382    }
9383
9384    #[test]
9385    fn get_body_node_direct_and_nested() {
9386        let direct = vec![elem(XmlNode::create("body"))];
9387        assert!(get_body_node(&direct).is_ok());
9388
9389        // Malformed markup: <body> buried inside <head>.
9390        let nested = vec![elem(node(
9391            "head",
9392            &[],
9393            vec![elem(node("div", &[], vec![elem(XmlNode::create("BODY"))]))],
9394        ))];
9395        assert!(
9396            get_body_node(&nested).is_ok(),
9397            "the recursive fallback finds a nested body"
9398        );
9399    }
9400
9401    /// Build a `<div>` chain `depth` levels deep with `inner` at the bottom.
9402    fn wrap_divs(depth: usize, inner: XmlNode) -> XmlNode {
9403        let mut n = inner;
9404        for _ in 0..depth {
9405            n = node("div", &[], vec![elem(n)]);
9406        }
9407        n
9408    }
9409
9410    #[test]
9411    fn get_body_node_deep_nesting_is_depth_capped_not_stack_overflowing() {
9412        // Body sits just inside the cap => found.
9413        let ok = vec![elem(wrap_divs(
9414            MAX_XML_NESTING_DEPTH - 2,
9415            XmlNode::create("body"),
9416        ))];
9417        assert!(get_body_node(&ok).is_ok(), "body within the depth cap is found");
9418
9419        // Body far below the cap => reported missing, but MUST NOT overflow.
9420        let too_deep = vec![elem(wrap_divs(2_000, XmlNode::create("body")))];
9421        assert_eq!(
9422            get_body_node(&too_deep),
9423            Err(DomXmlParseError::NoBodyInHtml),
9424            "past MAX_XML_NESTING_DEPTH the search gives up instead of crashing"
9425        );
9426    }
9427
9428    #[test]
9429    fn find_node_by_type_empty_garbage_unicode() {
9430        assert!(find_node_by_type(&[], "div").is_none());
9431        assert!(find_node_by_type(&[txt("x")], "div").is_none());
9432
9433        let roots = vec![elem(XmlNode::create("div"))];
9434        assert!(find_node_by_type(&roots, "").is_none());
9435        assert!(find_node_by_type(&roots, "   ").is_none());
9436        assert!(find_node_by_type(&roots, "\u{1F600}").is_none());
9437        assert!(find_node_by_type(&roots, &"z".repeat(LONG)).is_none());
9438        // Tag matching is ASCII case-insensitive (HTML tags are), so "DIV" matches a
9439        // <div> node. (It used to compare against the normalize_casing'd tag, which was
9440        // case-sensitive on the needle AND mangled uppercase tags to "d_i_v".)
9441        assert!(find_node_by_type(&roots, "DIV").is_some());
9442    }
9443
9444    #[test]
9445    fn find_node_by_type_valid_minimal_and_recursive() {
9446        let roots = vec![elem(node(
9447            "html",
9448            &[],
9449            vec![elem(node("head", &[], vec![elem(XmlNode::create("STYLE"))]))],
9450        ))];
9451        assert!(find_node_by_type(&roots, "html").is_some());
9452        assert!(
9453            find_node_by_type(&roots, "style").is_some(),
9454            "search recurses into the whole tree"
9455        );
9456        assert!(find_node_by_type(&roots, "body").is_none());
9457    }
9458
9459    #[test]
9460    fn find_node_by_type_prefers_the_shallowest_match() {
9461        let roots = vec![
9462            elem(node("div", &[], vec![elem(XmlNode::create("span"))])),
9463            elem(node("span", &[("id", "shallow")], vec![])),
9464        ];
9465        let found = find_node_by_type(&roots, "span").expect("found");
9466        assert_eq!(
9467            found.attributes.get_key("id").map(AzString::as_str),
9468            Some("shallow"),
9469            "direct children are scanned before recursing"
9470        );
9471    }
9472
9473    #[test]
9474    fn find_attribute_valid_minimal_and_missing() {
9475        let n = node("a", &[("href", "x"), ("id", "y")], vec![]);
9476        assert_eq!(find_attribute(&n, "href").map(AzString::as_str), Some("x"));
9477        assert_eq!(find_attribute(&n, "id").map(AzString::as_str), Some("y"));
9478        assert!(find_attribute(&n, "missing").is_none());
9479        assert!(find_attribute(&n, "").is_none());
9480        assert!(find_attribute(&n, "   ").is_none());
9481        assert!(find_attribute(&XmlNode::default(), "href").is_none());
9482        assert!(find_attribute(&n, &"z".repeat(LONG)).is_none());
9483    }
9484
9485    #[test]
9486    fn find_attribute_compares_against_the_normalized_key() {
9487        // `normalize_casing` turns `aria-label` into `aria_label`, so callers must
9488        // pass the NORMALIZED spelling — the raw HTML spelling does not match.
9489        let n = node("button", &[("aria-label", "Save")], vec![]);
9490        assert_eq!(
9491            find_attribute(&n, "aria_label").map(AzString::as_str),
9492            Some("Save")
9493        );
9494        assert!(
9495            find_attribute(&n, "aria-label").is_none(),
9496            "the hyphenated spelling never matches (keys are normalized first)"
9497        );
9498    }
9499
9500    #[test]
9501    fn find_attribute_unicode_keys_no_panic() {
9502        let n = node("div", &[("\u{130}", "v"), ("\u{1F600}", "w")], vec![]);
9503        assert!(find_attribute(&n, "\u{1F600}").is_some(), "emoji keys pass through");
9504        // 'İ' lowercases to 2 chars, so the normalized key is not 'İ'.
9505        assert!(find_attribute(&n, "\u{130}").is_none());
9506    }
9507
9508    // ================================================================
9509    // normalize_casing
9510    // ================================================================
9511
9512    #[test]
9513    fn normalize_casing_documented_forms() {
9514        assert_eq!(normalize_casing("abcDef"), "abc_def");
9515        assert_eq!(normalize_casing("AbcDef"), "abc_def");
9516        assert_eq!(normalize_casing("abc-def"), "abc_def");
9517        assert_eq!(normalize_casing("abc_def"), "abc_def");
9518    }
9519
9520    #[test]
9521    fn normalize_casing_edge_inputs() {
9522        assert_eq!(normalize_casing(""), "");
9523        assert_eq!(normalize_casing("---"), "", "separators alone produce no words");
9524        assert_eq!(normalize_casing("___"), "");
9525        assert_eq!(normalize_casing("A"), "a");
9526        assert_eq!(
9527            normalize_casing("ABC"),
9528            "a_b_c",
9529            "every uppercase char starts a new word"
9530        );
9531        assert_eq!(normalize_casing("h1"), "h1");
9532        assert_eq!(normalize_casing("   "), "   ", "whitespace is not a separator");
9533    }
9534
9535    #[test]
9536    fn normalize_casing_unicode_and_long_no_panic() {
9537        // 'İ' (U+0130) lowercases to TWO chars — the fn must not slice bytes.
9538        assert_eq!(normalize_casing("\u{130}"), "i\u{307}");
9539        assert_eq!(normalize_casing("\u{1F600}"), "\u{1F600}");
9540        assert_eq!(normalize_casing(&"a".repeat(50_000)).len(), 50_000);
9541        // 50k uppercase chars => 50k single-char words joined by '_'.
9542        assert_eq!(normalize_casing(&"A".repeat(50_000)).len(), 50_000 * 2 - 1);
9543    }
9544
9545    // ================================================================
9546    // get_item / get_item_internal
9547    // ================================================================
9548
9549    #[test]
9550    fn get_item_empty_hierarchy_returns_the_root() {
9551        let mut root = node("div", &[("id", "root")], vec![]);
9552        let got = get_item(&[], &mut root).expect("empty hierarchy => root");
9553        assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("root"));
9554    }
9555
9556    #[test]
9557    fn get_item_walks_nested_elements() {
9558        let mut root = node(
9559            "a",
9560            &[],
9561            vec![elem(node("b", &[], vec![elem(node("c", &[("id", "deep")], vec![]))]))],
9562        );
9563        let got = get_item(&[0, 0], &mut root).expect("a > b > c");
9564        assert_eq!(got.node_type.as_str(), "c");
9565        assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("deep"));
9566    }
9567
9568    #[test]
9569    fn get_item_out_of_bounds_and_text_nodes_return_none() {
9570        let mut root = node("a", &[], vec![txt("hello"), elem(XmlNode::create("b"))]);
9571        assert!(get_item(&[5], &mut root).is_none(), "out of bounds => None");
9572        assert!(
9573            get_item(&[usize::MAX], &mut root).is_none(),
9574            "usize::MAX index must not panic"
9575        );
9576        assert!(
9577            get_item(&[0], &mut root).is_none(),
9578            "index 0 is a TEXT node — not traversable"
9579        );
9580        assert!(get_item(&[1], &mut root).is_some());
9581        assert!(
9582            get_item(&[1, 0], &mut root).is_none(),
9583            "descending past a leaf => None"
9584        );
9585    }
9586
9587    #[test]
9588    fn get_item_deep_hierarchy_terminates() {
9589        // 400 levels: deep, but bounded by the (short) hierarchy vec, not by markup.
9590        let mut root = wrap_divs(400, node("div", &[("id", "bottom")], vec![]));
9591        let path = vec![0usize; 400];
9592        let got = get_item(&path, &mut root).expect("reaches the bottom");
9593        assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("bottom"));
9594    }
9595
9596    // ================================================================
9597    // decode_numeric_entity  (parser)
9598    // ================================================================
9599
9600    #[test]
9601    fn decode_numeric_entity_valid_minimal() {
9602        assert_eq!(decode_numeric_entity("#65"), Some('A'));
9603        assert_eq!(decode_numeric_entity("#x41"), Some('A'));
9604        assert_eq!(decode_numeric_entity("#X41"), Some('A'), "uppercase X accepted");
9605        assert_eq!(decode_numeric_entity("#x1F600"), Some('\u{1F600}'));
9606    }
9607
9608    #[test]
9609    fn decode_numeric_entity_empty_whitespace_garbage() {
9610        assert_eq!(decode_numeric_entity(""), None);
9611        assert_eq!(decode_numeric_entity("   "), None);
9612        assert_eq!(decode_numeric_entity("\t\n"), None);
9613        assert_eq!(decode_numeric_entity("amp"), None, "named entity is not numeric");
9614        assert_eq!(decode_numeric_entity("#"), None);
9615        assert_eq!(decode_numeric_entity("#x"), None);
9616        assert_eq!(decode_numeric_entity("#zz"), None);
9617        assert_eq!(decode_numeric_entity("# 65"), None);
9618        assert_eq!(decode_numeric_entity("#65junk"), None);
9619        assert_eq!(decode_numeric_entity("\u{1F600}"), None);
9620    }
9621
9622    #[test]
9623    fn decode_numeric_entity_boundary_code_points() {
9624        assert_eq!(decode_numeric_entity("#0"), Some('\u{0}'), "NUL is a valid char");
9625        assert_eq!(decode_numeric_entity("#x10FFFF"), Some('\u{10FFFF}'), "max scalar");
9626        assert_eq!(
9627            decode_numeric_entity("#x110000"),
9628            None,
9629            "one past the max scalar value"
9630        );
9631        assert_eq!(
9632            decode_numeric_entity("#xD800"),
9633            None,
9634            "a lone surrogate is not a char"
9635        );
9636        assert_eq!(
9637            decode_numeric_entity("#4294967295"),
9638            None,
9639            "u32::MAX is not a scalar value"
9640        );
9641        assert_eq!(
9642            decode_numeric_entity("#4294967296"),
9643            None,
9644            "one past u32::MAX must not wrap — it must fail to parse"
9645        );
9646        assert_eq!(decode_numeric_entity("#-1"), None, "negative is rejected by u32");
9647        assert_eq!(
9648            decode_numeric_entity("#xFFFFFFFFFFFF"),
9649            None,
9650            "hex overflow is rejected, not truncated"
9651        );
9652    }
9653
9654    #[test]
9655    fn decode_numeric_entity_extremely_long_terminates() {
9656        let s = format!("#{}", "9".repeat(LONG));
9657        assert_eq!(s.len(), LONG + 1);
9658        assert_eq!(decode_numeric_entity(&s), None, "overflows u32 => None");
9659    }
9660
9661    // ================================================================
9662    // decode_entities / prepare_string
9663    // ================================================================
9664
9665    #[test]
9666    fn decode_entities_leaves_unrecognized_sequences_verbatim() {
9667        assert_eq!(decode_entities(""), "");
9668        assert_eq!(decode_entities("&"), "&", "a bare '&' at EOF must not panic");
9669        assert_eq!(decode_entities("&;"), "&;", "empty entity body is not decoded");
9670        assert_eq!(decode_entities("&bogus;"), "&bogus;");
9671        assert_eq!(decode_entities("&nbsp;"), "&nbsp;", "&nbsp; is deliberately kept");
9672        // A ';' further away than MAX_ENTITY_BODY is not treated as an entity end.
9673        assert_eq!(
9674            decode_entities("&averyveryverylongbody;"),
9675            "&averyveryverylongbody;"
9676        );
9677    }
9678
9679    #[test]
9680    fn decode_entities_single_pass_prevents_double_decoding() {
9681        assert_eq!(decode_entities("&amp;lt;"), "&lt;", "&amp; must not re-open an entity");
9682        assert_eq!(decode_entities("&lt;&gt;&amp;&quot;&apos;"), "<>&\"'");
9683    }
9684
9685    #[test]
9686    fn decode_entities_unicode_and_long_input_terminate() {
9687        assert_eq!(decode_entities("\u{1F600}\u{0301}\u{130}"), "\u{1F600}\u{0301}\u{130}");
9688        // NOTE: each '&' triggers a `find(';')` over the whole remaining suffix, so
9689        // this is quadratic in the number of '&'. Keep the input modest.
9690        let amps = "&".repeat(20_000);
9691        assert_eq!(decode_entities(&amps).len(), 20_000);
9692    }
9693
9694    #[test]
9695    fn prepare_string_empty_and_whitespace() {
9696        assert_eq!(prepare_string(""), "");
9697        assert_eq!(prepare_string("   "), "");
9698        assert_eq!(prepare_string("\t\n\r  \n"), "");
9699    }
9700
9701    #[test]
9702    fn prepare_string_nbsp_becomes_a_space_that_survives_trim() {
9703        assert_eq!(prepare_string("&nbsp;"), " ");
9704        assert_eq!(prepare_string("a&nbsp;b"), "a b");
9705    }
9706
9707    #[test]
9708    fn prepare_string_collapses_a_blank_line_into_a_single_return() {
9709        assert_eq!(prepare_string("a\n\nb"), "a\nb");
9710        assert_eq!(prepare_string("a\n\n\n\nb"), "a\nb", "runs of blanks collapse");
9711    }
9712
9713    #[test]
9714    fn prepare_string_unicode_and_long_input_no_panic() {
9715        assert_eq!(prepare_string("\u{1F600}"), "\u{1F600}");
9716        assert_eq!(prepare_string("  \u{130}\u{0301}  "), "\u{130}\u{0301}");
9717        assert_eq!(prepare_string(&"x".repeat(LONG)).len(), LONG);
9718    }
9719
9720    /// BUG (reported): a soft-wrapped multi-line text node loses the word break
9721    /// on the FINAL line, because `prepare_string` skips the joining space when
9722    /// `line_idx == line_len - 1`. HTML must collapse the newline into a space.
9723    #[test]
9724    fn prepare_string_joins_wrapped_lines_with_a_space() {
9725        assert_eq!(
9726            prepare_string("Hello\nworld"),
9727            "Hello world",
9728            "a single newline between words must collapse to a space, not vanish"
9729        );
9730        assert_eq!(prepare_string("a\nb\nc"), "a b c");
9731    }
9732
9733    // ================================================================
9734    // parse_bool  (parser)
9735    // ================================================================
9736
9737    #[test]
9738    fn parse_bool_valid_minimal_and_everything_else_is_none() {
9739        assert_eq!(parse_bool("true"), Some(true));
9740        assert_eq!(parse_bool("false"), Some(false));
9741
9742        for s in [
9743            "", "   ", "\t\n", " true", "true ", "TRUE", "True", "FALSE", "1", "0", "-0", "yes",
9744            "no", "NaN", "inf", "9223372036854775807", "\u{1F600}", "true;garbage",
9745        ] {
9746            assert_eq!(parse_bool(s), None, "{s:?} must not parse as a bool");
9747        }
9748        assert_eq!(parse_bool(&"a".repeat(LONG)), None);
9749    }
9750
9751    // ================================================================
9752    // split_dynamic_string / format_args_dynamic
9753    // ================================================================
9754
9755    fn args(kv: &[(&str, &str)]) -> ComponentArgumentVec {
9756        kv.iter()
9757            .map(|(n, t)| ComponentArgument {
9758                name: AzString::from(*n),
9759                arg_type: AzString::from(*t),
9760            })
9761            .collect::<Vec<_>>()
9762            .into()
9763    }
9764
9765    #[test]
9766    fn split_dynamic_string_empty_and_plain() {
9767        assert!(split_dynamic_string("").is_empty());
9768        assert_eq!(
9769            split_dynamic_string("abc"),
9770            vec![DynamicItem::Str("abc".to_string())]
9771        );
9772    }
9773
9774    #[test]
9775    fn split_dynamic_string_format_spec_is_split_on_the_first_colon() {
9776        assert_eq!(
9777            split_dynamic_string("{a:?}"),
9778            vec![DynamicItem::Var {
9779                name: "a".to_string(),
9780                format_spec: Some("?".to_string()),
9781            }]
9782        );
9783        assert_eq!(
9784            split_dynamic_string("{a:x:y}"),
9785            vec![DynamicItem::Var {
9786                name: "a".to_string(),
9787                format_spec: Some("x:y".to_string()),
9788            }],
9789            "only the FIRST colon separates name from spec"
9790        );
9791    }
9792
9793    #[test]
9794    fn split_dynamic_string_unterminated_var_stays_literal() {
9795        // No closing brace => the scan runs to EOF and the text stays a literal.
9796        assert_eq!(
9797            split_dynamic_string("{unterminated"),
9798            vec![DynamicItem::Str("{unterminated".to_string())]
9799        );
9800        // A whitespace inside the braces aborts the variable scan.
9801        assert_eq!(
9802            split_dynamic_string("{a b}"),
9803            vec![DynamicItem::Str("{a b}".to_string())]
9804        );
9805    }
9806
9807    #[test]
9808    fn split_dynamic_string_unicode_and_long_input_terminate() {
9809        assert_eq!(
9810            split_dynamic_string("\u{1F600}{v}\u{130}"),
9811            vec![
9812                DynamicItem::Str("\u{1F600}".to_string()),
9813                DynamicItem::Var {
9814                    name: "v".to_string(),
9815                    format_spec: None
9816                },
9817                DynamicItem::Str("\u{130}".to_string()),
9818            ]
9819        );
9820        // The failed-variable scan advances the cursor by the amount it scanned,
9821        // so this stays linear rather than quadratic.
9822        let bomb = "{a".repeat(50_000);
9823        let out = split_dynamic_string(&bomb);
9824        assert!(out.len() <= 2, "a never-closed variable collapses, got {}", out.len());
9825
9826        let braces = "{".repeat(100_000);
9827        assert!(split_dynamic_string(&braces).len() <= 1);
9828    }
9829
9830    #[test]
9831    fn format_args_dynamic_documented_example() {
9832        let vars = args(&[("a", "value1"), ("b", "value2")]);
9833        assert_eq!(
9834            format_args_dynamic("hello {a}, {b}{{ {c} }}", &vars),
9835            "hello value1, value2{ {c} }"
9836        );
9837    }
9838
9839    #[test]
9840    fn format_args_dynamic_unknown_var_is_preserved_verbatim() {
9841        let empty = no_args();
9842        assert_eq!(format_args_dynamic("{c}", &empty), "{c}");
9843        assert_eq!(
9844            format_args_dynamic("{c:?}", &empty),
9845            "{c:?}",
9846            "the format spec is re-attached when the var is unresolved"
9847        );
9848        // Escaped braces round-trip to themselves.
9849        assert_eq!(format_args_dynamic("{{}}", &empty), "{{}}");
9850    }
9851
9852    #[test]
9853    fn format_args_dynamic_variable_names_are_normalized() {
9854        let vars = args(&[("my_var", "V")]);
9855        assert_eq!(format_args_dynamic("{myVar}", &vars), "V", "camelCase is normalized");
9856        assert_eq!(format_args_dynamic("{ my-var }", &vars), "{ my-var }",
9857            "whitespace inside the braces aborts the variable scan entirely");
9858        assert_eq!(format_args_dynamic("{my-var}", &vars), "V");
9859    }
9860
9861    #[test]
9862    fn format_args_dynamic_edge_values_no_panic() {
9863        let empty = no_args();
9864        assert_eq!(format_args_dynamic("", &empty), "");
9865        assert_eq!(format_args_dynamic("   ", &empty), "   ");
9866        assert_eq!(format_args_dynamic("\u{1F600}", &empty), "\u{1F600}");
9867        assert_eq!(format_args_dynamic(&"x".repeat(50_000), &empty).len(), 50_000);
9868    }
9869
9870    #[test]
9871    fn combine_and_replace_dynamic_items_on_empty_input() {
9872        assert_eq!(combine_and_replace_dynamic_items(&[], &no_args()), "");
9873    }
9874
9875    // ================================================================
9876    // compile_and_format_dynamic_items / format_args_for_rust_code
9877    // ================================================================
9878
9879    #[test]
9880    fn format_args_for_rust_code_empty_and_single_items() {
9881        assert_eq!(format_args_for_rust_code(""), "AzString::from_const_str(\"\")");
9882        assert_eq!(format_args_for_rust_code("hi"), "AzString::from_const_str(\"hi\")");
9883        assert_eq!(format_args_for_rust_code("{a}"), "a", "a lone var becomes a bare ident");
9884        assert_eq!(
9885            format_args_for_rust_code("{a:?}"),
9886            "format!(\"{:?}\", a).into()"
9887        );
9888    }
9889
9890    #[test]
9891    fn format_args_for_rust_code_multi_item_builds_a_format_call() {
9892        assert_eq!(
9893            format_args_for_rust_code("x={a} y={b}"),
9894            "format!(\"x={a} y={b}\", a, b).into()"
9895        );
9896    }
9897
9898    #[test]
9899    fn format_args_for_rust_code_escapes_quotes_in_literals() {
9900        let out = format_args_for_rust_code("say \"hi\" {a}");
9901        assert!(
9902            out.contains("say \\\"hi\\\""),
9903            "double quotes must be escaped for the emitted literal, got {out}"
9904        );
9905    }
9906
9907    #[test]
9908    fn compile_and_format_dynamic_items_edge_values() {
9909        assert_eq!(
9910            compile_and_format_dynamic_items(&[]),
9911            "AzString::from_const_str(\"\")"
9912        );
9913        assert_eq!(
9914            compile_and_format_dynamic_items(&[DynamicItem::Str(String::new())]),
9915            "AzString::from_const_str(\"\")"
9916        );
9917        assert_eq!(
9918            compile_and_format_dynamic_items(&[DynamicItem::Var {
9919                name: "  spaced  ".to_string(),
9920                format_spec: None,
9921            }]),
9922            "spaced",
9923            "the var name is trimmed + normalized"
9924        );
9925    }
9926
9927    // ================================================================
9928    // cap_first / camel_to_snake / esc_lit / c_creator_suffix
9929    // ================================================================
9930
9931    #[test]
9932    fn cap_first_edge_inputs() {
9933        assert_eq!(cap_first(""), "", "empty input must not panic");
9934        assert_eq!(cap_first("h1"), "H1");
9935        assert_eq!(cap_first("button"), "Button");
9936        assert_eq!(cap_first("A"), "A", "already-uppercase is idempotent");
9937        assert_eq!(cap_first("\u{1F600}x"), "\u{1F600}x", "emoji has no uppercase form");
9938        // 'ß' uppercases to TWO chars — the fn must not assume 1:1.
9939        assert_eq!(cap_first("\u{df}x"), "SSx");
9940        assert_eq!(cap_first(&"a".repeat(10_000)).len(), 10_000);
9941    }
9942
9943    #[test]
9944    fn camel_to_snake_documented_forms() {
9945        assert_eq!(camel_to_snake("ButtonNoA11y"), "button_no_a11y");
9946        assert_eq!(camel_to_snake("PWithText"), "p_with_text");
9947        assert_eq!(camel_to_snake("ANoA11y"), "a_no_a11y");
9948        assert_eq!(camel_to_snake("H1WithText"), "h1_with_text");
9949        assert_eq!(camel_to_snake("Div"), "div");
9950    }
9951
9952    #[test]
9953    fn camel_to_snake_edge_inputs() {
9954        assert_eq!(camel_to_snake(""), "");
9955        assert_eq!(camel_to_snake("A"), "a");
9956        assert_eq!(camel_to_snake("AB"), "ab", "an all-caps run is not split");
9957        assert_eq!(camel_to_snake("ABc"), "a_bc", "a caps run splits before the last cap");
9958        assert_eq!(camel_to_snake("\u{1F600}"), "\u{1F600}");
9959        assert_eq!(camel_to_snake(&"a".repeat(10_000)).len(), 10_000);
9960    }
9961
9962    #[test]
9963    fn esc_lit_escapes_backslash_before_quote() {
9964        assert_eq!(esc_lit(""), "");
9965        assert_eq!(esc_lit("plain"), "plain");
9966        assert_eq!(esc_lit("a\"b"), "a\\\"b");
9967        assert_eq!(esc_lit("a\\b"), "a\\\\b");
9968        // The backslash pass must run FIRST so an escaped quote is not double-escaped.
9969        assert_eq!(esc_lit("\\\""), "\\\\\\\"");
9970        assert_eq!(esc_lit("\u{1F600}"), "\u{1F600}");
9971    }
9972
9973    #[test]
9974    fn c_creator_suffix_edge_inputs() {
9975        assert_eq!(c_creator_suffix(""), "Div", "empty debug name falls back to Div");
9976        assert_eq!(c_creator_suffix("Div"), "Div");
9977        assert_eq!(c_creator_suffix("H1"), "H1");
9978        assert_eq!(c_creator_suffix("BlockQuote"), "Blockquote");
9979        assert_eq!(c_creator_suffix("FigCaption"), "Figcaption");
9980        assert_eq!(c_creator_suffix("\u{1F600}"), "\u{1F600}");
9981    }
9982
9983    // ================================================================
9984    // safe_container_tag
9985    // ================================================================
9986
9987    #[test]
9988    fn safe_container_tag_falls_back_to_div_for_arg_taking_widgets() {
9989        assert_eq!(safe_container_tag(""), "Div");
9990        assert_eq!(safe_container_tag("Div"), "Div");
9991        assert_eq!(safe_container_tag("Span"), "Span");
9992        assert_eq!(safe_container_tag("H1"), "H1");
9993        // Interactive / arg-taking elements deliberately degrade to a container.
9994        assert_eq!(safe_container_tag("Button"), "Div");
9995        assert_eq!(safe_container_tag("Input"), "Div");
9996        assert_eq!(safe_container_tag("A"), "Div");
9997        assert_eq!(safe_container_tag("\u{1F600}"), "Div");
9998        assert_eq!(safe_container_tag(&"z".repeat(10_000)), "Div");
9999    }
10000
10001    /// BUG (reported): `SAFE_CONTAINER_TAGS` is documented as holding the
10002    /// `NodeType`/`NodeTypeTag` **debug names**, and `safe_container_tag` compares
10003    /// against `format!("{:?}", tag_to_node_type(tag))`. But six entries are spelled
10004    /// with a different inner capitalization than the actual variant, so the
10005    /// comparison never matches and `<blockquote>`/`<figcaption>`/`<thead>`/`<tbody>`
10006    /// /`<tfoot>`/`<colgroup>` silently compile down to a plain `div`.
10007    #[test]
10008    fn safe_container_tag_matches_the_real_nodetype_debug_names() {
10009        for tag in [
10010            "blockquote",
10011            "figcaption",
10012            "thead",
10013            "tbody",
10014            "tfoot",
10015            "colgroup",
10016        ] {
10017            let dbg = format!("{:?}", tag_to_node_type(tag));
10018            assert_ne!(
10019                safe_container_tag(&dbg),
10020                "Div",
10021                "<{tag}> (NodeType debug name {dbg:?}) is a pure container and must keep \
10022                 its own creator instead of degrading to a div"
10023            );
10024        }
10025    }
10026
10027    // ================================================================
10028    // fmt_f32_lit  (numeric)
10029    // ================================================================
10030
10031    #[test]
10032    fn fmt_f32_lit_zero_and_negative() {
10033        assert_eq!(fmt_f32_lit(0.0), "0.0", "an integral value gains a decimal point");
10034        assert_eq!(fmt_f32_lit(-0.0), "-0.0");
10035        assert_eq!(fmt_f32_lit(-1.0), "-1.0");
10036        assert_eq!(fmt_f32_lit(1.5), "1.5");
10037        assert_eq!(fmt_f32_lit(-1.5), "-1.5");
10038    }
10039
10040    #[test]
10041    fn fmt_f32_lit_min_max_stay_parseable_float_literals() {
10042        for f in [f32::MAX, f32::MIN, f32::MIN_POSITIVE, f32::EPSILON] {
10043            let s = fmt_f32_lit(f);
10044            assert_eq!(
10045                s.parse::<f32>(),
10046                Ok(f),
10047                "{f:e} must round-trip through its emitted literal ({s})"
10048            );
10049        }
10050    }
10051
10052    #[test]
10053    fn fmt_f32_lit_nan_inf_produce_a_defined_result_and_do_not_panic() {
10054        // NOTE: these are NOT valid Rust/C float literals — a page with
10055        // `<progress value="NaN">` emits `create_progress_no_a11y(NaN, 1.0)`.
10056        // Pinned so a fix (e.g. clamping to 0.0) is a visible change.
10057        assert_eq!(fmt_f32_lit(f32::NAN), "NaN");
10058        assert_eq!(fmt_f32_lit(f32::INFINITY), "inf");
10059        assert_eq!(fmt_f32_lit(f32::NEG_INFINITY), "-inf");
10060    }
10061
10062    // ================================================================
10063    // node_direct_text / node_aria_label / node_attr_or / node_attr_f32
10064    // first_caption_text
10065    // ================================================================
10066
10067    #[test]
10068    fn node_direct_text_trims_and_skips_elements() {
10069        assert_eq!(node_direct_text(&XmlNode::default()), "");
10070        assert_eq!(node_direct_text(&node("p", &[], vec![txt("  Go  ")])), "Go");
10071        assert_eq!(
10072            node_direct_text(&node(
10073                "p",
10074                &[],
10075                vec![txt("a"), elem(node("b", &[], vec![txt("IGNORED")])), txt("b")]
10076            )),
10077            "a b",
10078            "direct text children are joined with a single space"
10079        );
10080        assert_eq!(
10081            node_direct_text(&node("p", &[], vec![txt("   "), txt("\t\n")])),
10082            "",
10083            "whitespace-only children are dropped"
10084        );
10085    }
10086
10087    #[test]
10088    fn node_aria_label_ignores_empty_and_whitespace() {
10089        assert_eq!(node_aria_label(&XmlNode::default()), None);
10090        assert_eq!(node_aria_label(&node("b", &[("aria-label", "")], vec![])), None);
10091        assert_eq!(node_aria_label(&node("b", &[("aria-label", "   ")], vec![])), None);
10092        assert_eq!(
10093            node_aria_label(&node("b", &[("aria-label", "  Save  ")], vec![])),
10094            Some("Save".to_string())
10095        );
10096    }
10097
10098    #[test]
10099    fn node_attr_or_returns_the_default_when_absent() {
10100        let n = node("a", &[("href", "/x"), ("empty", "")], vec![]);
10101        assert_eq!(node_attr_or(&n, "href", "FALLBACK"), "/x");
10102        assert_eq!(node_attr_or(&n, "missing", "FALLBACK"), "FALLBACK");
10103        assert_eq!(
10104            node_attr_or(&n, "empty", "FALLBACK"),
10105            "",
10106            "a present-but-empty attribute wins over the default"
10107        );
10108        assert_eq!(node_attr_or(&XmlNode::default(), "x", ""), "");
10109    }
10110
10111    #[test]
10112    fn node_attr_f32_zero_negative_and_defaults() {
10113        let n = node(
10114            "meter",
10115            &[("zero", "0"), ("negzero", "-0"), ("neg", "-2.5"), ("pad", "  1.5  ")],
10116            vec![],
10117        );
10118        assert_eq!(node_attr_f32(&n, "zero", 9.0), 0.0);
10119        assert!(node_attr_f32(&n, "negzero", 9.0).is_sign_negative());
10120        assert_eq!(node_attr_f32(&n, "neg", 9.0), -2.5);
10121        assert_eq!(node_attr_f32(&n, "pad", 9.0), 1.5, "the value is trimmed first");
10122        assert_eq!(node_attr_f32(&n, "missing", 9.0), 9.0);
10123    }
10124
10125    #[test]
10126    fn node_attr_f32_unparsable_falls_back_and_min_max_saturate() {
10127        let n = node(
10128            "meter",
10129            &[
10130                ("junk", "abc"),
10131                ("empty", ""),
10132                ("huge", "1e400"),
10133                ("tiny", "-1e400"),
10134                ("big", "340282350000000000000000000000000000000"),
10135            ],
10136            vec![],
10137        );
10138        assert_eq!(node_attr_f32(&n, "junk", 7.0), 7.0);
10139        assert_eq!(node_attr_f32(&n, "empty", 7.0), 7.0);
10140        assert!(
10141            node_attr_f32(&n, "huge", 7.0).is_infinite(),
10142            "an out-of-range literal saturates to inf, it does not panic"
10143        );
10144        assert_eq!(node_attr_f32(&n, "tiny", 7.0), f32::NEG_INFINITY);
10145        assert_eq!(node_attr_f32(&n, "big", 7.0), f32::MAX);
10146    }
10147
10148    #[test]
10149    fn node_attr_f32_accepts_nan_and_inf_spellings() {
10150        // Rust's f32 FromStr accepts "NaN"/"inf", so hostile markup can inject a
10151        // non-finite value straight into codegen (see fmt_f32_lit above).
10152        let n = node("progress", &[("value", "NaN"), ("max", "inf")], vec![]);
10153        assert!(node_attr_f32(&n, "value", 0.0).is_nan());
10154        assert_eq!(node_attr_f32(&n, "max", 1.0), f32::INFINITY);
10155    }
10156
10157    #[test]
10158    fn node_attr_f32_nan_default_is_returned_verbatim() {
10159        assert!(node_attr_f32(&XmlNode::default(), "x", f32::NAN).is_nan());
10160        assert_eq!(node_attr_f32(&XmlNode::default(), "x", f32::INFINITY), f32::INFINITY);
10161    }
10162
10163    #[test]
10164    fn first_caption_text_edges() {
10165        assert_eq!(first_caption_text(&XmlNode::default()), None);
10166        assert_eq!(
10167            first_caption_text(&node("table", &[], vec![elem(node("caption", &[], vec![]))])),
10168            None,
10169            "an empty caption yields None"
10170        );
10171        assert_eq!(
10172            first_caption_text(&node(
10173                "table",
10174                &[],
10175                vec![elem(node("CAPTION", &[], vec![txt("  Hi  ")]))]
10176            )),
10177            Some("Hi".to_string()),
10178            "the tag match is ASCII-case-insensitive and the text is trimmed"
10179        );
10180    }
10181
10182    // ================================================================
10183    // analyze_node_ctor / CtorArg / NodeCtor
10184    // ================================================================
10185
10186    #[test]
10187    fn analyze_node_ctor_plain_for_unknown_and_empty_tags() {
10188        assert!(matches!(analyze_node_ctor("div", &XmlNode::default()), NodeCtor::Plain));
10189        assert!(matches!(analyze_node_ctor("", &XmlNode::default()), NodeCtor::Plain));
10190        assert!(matches!(
10191            analyze_node_ctor("\u{1F600}", &XmlNode::default()),
10192            NodeCtor::Plain
10193        ));
10194        let plain = analyze_node_ctor("div", &XmlNode::default());
10195        assert_eq!(plain.render_rust(), None);
10196        assert_eq!(plain.render_c(), None);
10197        assert_eq!(plain.render_fluent(&CompileTarget::Cpp), None);
10198        assert!(!plain.consumes_text());
10199        assert!(!plain.skip_caption());
10200    }
10201
10202    #[test]
10203    fn analyze_node_ctor_with_text_tier_requires_actual_text() {
10204        // Empty <p> stays a plain container (has_only_text_children() is vacuously
10205        // true for a childless node, so `has_text` is the real gate).
10206        assert!(matches!(analyze_node_ctor("p", &XmlNode::default()), NodeCtor::Plain));
10207        // <p> with an element child is not "pure text" either.
10208        let mixed = node("p", &[], vec![txt("a"), elem(XmlNode::create("span"))]);
10209        assert!(matches!(analyze_node_ctor("p", &mixed), NodeCtor::Plain));
10210
10211        let pure = node("p", &[], vec![txt("  Hello  ")]);
10212        let ctor = analyze_node_ctor("p", &pure);
10213        assert!(ctor.consumes_text(), "the text is folded into the constructor");
10214        assert_eq!(
10215            ctor.render_rust().as_deref(),
10216            Some("Dom::create_p_with_text(AzString::from(\"Hello\"))")
10217        );
10218        assert_eq!(
10219            ctor.render_c().as_deref(),
10220            Some("AzDom_createPWithText(AZ_STR(\"Hello\"))")
10221        );
10222        assert_eq!(
10223            ctor.render_fluent(&CompileTarget::Python).as_deref(),
10224            Some("azul.Dom.create_p_with_text(\"Hello\")")
10225        );
10226    }
10227
10228    #[test]
10229    fn analyze_node_ctor_button_with_and_without_aria() {
10230        let plain_btn = node("button", &[], vec![txt("Go")]);
10231        assert_eq!(
10232            analyze_node_ctor("button", &plain_btn).render_rust().as_deref(),
10233            Some("Dom::create_button_no_a11y(AzString::from(\"Go\"))")
10234        );
10235
10236        let aria_btn = node("button", &[("aria-label", "Save")], vec![txt("Go")]);
10237        assert_eq!(
10238            analyze_node_ctor("button", &aria_btn).render_rust().as_deref(),
10239            Some(
10240                "Dom::create_button(AzString::from(\"Go\"), \
10241                 SmallAriaInfo::label(AzString::from(\"Save\")))"
10242            )
10243        );
10244    }
10245
10246    #[test]
10247    fn analyze_node_ctor_escapes_quotes_and_backslashes_in_text() {
10248        let btn = node("button", &[], vec![txt("say \"hi\"\\now")]);
10249        let rust = analyze_node_ctor("button", &btn).render_rust().expect("semantic");
10250        assert!(
10251            rust.contains("say \\\"hi\\\"\\\\now"),
10252            "quotes and backslashes must be escaped for the literal, got {rust}"
10253        );
10254    }
10255
10256    #[test]
10257    fn analyze_node_ctor_anchor_uses_option_string_when_it_has_no_text() {
10258        let bare = node("a", &[], vec![]);
10259        assert_eq!(
10260            analyze_node_ctor("a", &bare).render_rust().as_deref(),
10261            Some("Dom::create_a_no_a11y(AzString::from(\"\"), OptionString::None)"),
10262            "a missing href defaults to an empty string, missing text to OptionString::None"
10263        );
10264
10265        let full = node("a", &[("href", "/x")], vec![txt("Home")]);
10266        assert_eq!(
10267            analyze_node_ctor("a", &full).render_rust().as_deref(),
10268            Some(
10269                "Dom::create_a_no_a11y(AzString::from(\"/x\"), \
10270                 OptionString::Some(AzString::from(\"Home\")))"
10271            )
10272        );
10273        assert_eq!(
10274            analyze_node_ctor("a", &full).render_c().as_deref(),
10275            Some("AzDom_createANoA11y(AZ_STR(\"/x\"), AzOptionString_some(AZ_STR(\"Home\")))")
10276        );
10277    }
10278
10279    #[test]
10280    fn analyze_node_ctor_table_aria_form_skips_the_literal_caption() {
10281        let t = node(
10282            "table",
10283            &[("aria-label", "Prices")],
10284            vec![elem(node("caption", &[], vec![txt("Q1")]))],
10285        );
10286        let ctor = analyze_node_ctor("table", &t);
10287        assert!(ctor.skip_caption(), "the aria form injects its own caption child");
10288        assert_eq!(
10289            ctor.render_rust().as_deref(),
10290            Some(
10291                "Dom::create_table(AzString::from(\"Q1\"), \
10292                 SmallAriaInfo::label(AzString::from(\"Prices\")))"
10293            )
10294        );
10295
10296        let plain = analyze_node_ctor("table", &node("table", &[], vec![]));
10297        assert!(!plain.skip_caption());
10298        assert_eq!(plain.render_rust().as_deref(), Some("Dom::create_table_no_a11y()"));
10299    }
10300
10301    #[test]
10302    fn analyze_node_ctor_scalar_widgets_use_defaults_and_emit_float_literals() {
10303        let p = node("progress", &[], vec![]);
10304        assert_eq!(
10305            analyze_node_ctor("progress", &p).render_rust().as_deref(),
10306            Some("Dom::create_progress_no_a11y(0.0, 1.0)"),
10307            "missing value/max fall back to 0.0 / 1.0 as float literals"
10308        );
10309
10310        let m = node("meter", &[("value", "5"), ("min", "-1"), ("max", "10")], vec![]);
10311        assert_eq!(
10312            analyze_node_ctor("meter", &m).render_c().as_deref(),
10313            Some("AzDom_createMeterNoA11y(5.0f, -1.0f, 10.0f)")
10314        );
10315    }
10316
10317    /// Non-finite attribute values flow straight into the emitted literal. Pinned
10318    /// so that a fix (clamping / rejecting them) shows up as a change.
10319    #[test]
10320    fn analyze_node_ctor_non_finite_attributes_emit_non_finite_literals() {
10321        let p = node("progress", &[("value", "NaN"), ("max", "inf")], vec![]);
10322        let rust = analyze_node_ctor("progress", &p).render_rust().expect("semantic");
10323        assert_eq!(rust, "Dom::create_progress_no_a11y(NaN, inf)");
10324    }
10325
10326    #[test]
10327    fn ctor_arg_render_targets_are_distinct() {
10328        let s = CtorArg::Str("a\"b".to_string());
10329        assert_eq!(s.render_rust(), "AzString::from(\"a\\\"b\")");
10330        assert_eq!(s.render_c(), "AZ_STR(\"a\\\"b\")");
10331        assert_eq!(s.render_cpp(), "String(\"a\\\"b\")");
10332        assert_eq!(s.render_python(), "\"a\\\"b\"");
10333
10334        assert_eq!(CtorArg::OptNone.render_rust(), "OptionString::None");
10335        assert_eq!(CtorArg::OptNone.render_c(), "AzOptionString_none()");
10336        assert_eq!(CtorArg::OptNone.render_cpp(), "OptionString::none()");
10337        assert_eq!(CtorArg::OptNone.render_python(), "azul.OptionString.none()");
10338
10339        assert_eq!(CtorArg::Float(0.0).render_rust(), "0.0");
10340        assert_eq!(CtorArg::Float(0.0).render_c(), "0.0f");
10341        assert_eq!(CtorArg::Float(f32::NAN).render_c(), "NaNf");
10342    }
10343
10344    #[test]
10345    fn node_ctor_render_fluent_returns_none_for_non_fluent_targets() {
10346        let ctor = analyze_node_ctor("p", &node("p", &[], vec![txt("x")]));
10347        assert!(ctor.render_fluent(&CompileTarget::Rust).is_none());
10348        assert!(ctor.render_fluent(&CompileTarget::C).is_none());
10349        assert!(ctor.render_fluent(&CompileTarget::Cpp).is_some());
10350        assert!(ctor.render_fluent(&CompileTarget::Python).is_some());
10351    }
10352
10353    // ================================================================
10354    // format_component_args / compile_component / compile_components
10355    // ================================================================
10356
10357    #[test]
10358    fn format_component_args_empty_and_ordering() {
10359        assert_eq!(format_component_args(&no_args()), "");
10360        // Args are sorted DESCENDING by their rendered "name: type" string.
10361        assert_eq!(
10362            format_component_args(&args(&[("a", "u32"), ("b", "String")])),
10363            "b: String, a: u32"
10364        );
10365    }
10366
10367    #[test]
10368    fn format_component_args_edge_values_no_panic() {
10369        let a = args(&[("", ""), ("\u{1F600}", "\u{130}")]);
10370        let out = format_component_args(&a);
10371        assert!(out.contains(": "), "still emits `name: type` pairs, got {out:?}");
10372    }
10373
10374    #[test]
10375    fn compile_component_emits_a_render_fn() {
10376        let ca = ComponentArguments {
10377            args: args(&[("count", "u32")]),
10378            accepts_text: false,
10379        };
10380        let out = compile_component("MyWidget", &ca, "Dom::create_div()");
10381        assert!(out.contains("pub fn render(count: u32) -> Dom {"), "got:\n{out}");
10382        assert!(out.contains("#[inline]"), "a one-line body is inlined");
10383    }
10384
10385    #[test]
10386    fn compile_component_accepts_text_prepends_the_text_param() {
10387        let ca = ComponentArguments {
10388            args: args(&[("count", "u32")]),
10389            accepts_text: true,
10390        };
10391        let out = compile_component("my-widget", &ca, "Dom::create_div()");
10392        assert!(
10393            out.contains("pub fn render(text: AzString, count: u32) -> Dom {"),
10394            "got:\n{out}"
10395        );
10396
10397        let ca_no_args = ComponentArguments {
10398            args: no_args(),
10399            accepts_text: true,
10400        };
10401        let out = compile_component("w", &ca_no_args, "Dom::create_div()");
10402        assert!(
10403            out.contains("pub fn render(text: AzString) -> Dom {"),
10404            "no trailing comma when there are no extra args, got:\n{out}"
10405        );
10406    }
10407
10408    #[test]
10409    fn compile_component_empty_name_and_body_no_panic() {
10410        let ca = ComponentArguments::default();
10411        let out = compile_component("", &ca, "");
10412        assert!(out.contains("pub fn render() -> Dom {"), "got:\n{out}");
10413    }
10414
10415    #[test]
10416    fn compile_components_of_an_empty_list_is_empty() {
10417        assert_eq!(compile_components(Vec::new()), "");
10418    }
10419
10420    // ================================================================
10421    // parse_svg_float / parse_svg_points  (parser / numeric)
10422    // ================================================================
10423
10424    #[test]
10425    fn parse_svg_float_none_empty_whitespace_garbage() {
10426        assert_eq!(parse_svg_float(None), None);
10427        let empty = AzString::from("");
10428        assert_eq!(parse_svg_float(Some(&empty)), None);
10429        let ws = AzString::from("   \t\n");
10430        assert_eq!(parse_svg_float(Some(&ws)), None);
10431        let junk = AzString::from("10px");
10432        assert_eq!(parse_svg_float(Some(&junk)), None, "units are not stripped");
10433        let uni = AzString::from("\u{1F600}");
10434        assert_eq!(parse_svg_float(Some(&uni)), None);
10435    }
10436
10437    #[test]
10438    fn parse_svg_float_valid_and_boundary_numbers() {
10439        let padded = AzString::from("  1.5  ");
10440        assert_eq!(parse_svg_float(Some(&padded)), Some(1.5), "value is trimmed");
10441        let zero = AzString::from("0");
10442        assert_eq!(parse_svg_float(Some(&zero)), Some(0.0));
10443        let negzero = AzString::from("-0");
10444        assert!(parse_svg_float(Some(&negzero)).unwrap().is_sign_negative());
10445        let huge = AzString::from("1e400");
10446        assert!(
10447            parse_svg_float(Some(&huge)).unwrap().is_infinite(),
10448            "overflow saturates to inf rather than erroring"
10449        );
10450        let nan = AzString::from("NaN");
10451        assert!(parse_svg_float(Some(&nan)).unwrap().is_nan());
10452        let inf = AzString::from("-inf");
10453        assert_eq!(parse_svg_float(Some(&inf)), Some(f32::NEG_INFINITY));
10454    }
10455
10456    #[test]
10457    fn parse_svg_points_rejects_degenerate_input() {
10458        assert!(parse_svg_points("", false).is_none());
10459        assert!(parse_svg_points("   ", false).is_none());
10460        assert!(parse_svg_points("garbage", false).is_none());
10461        assert!(parse_svg_points("1 2", false).is_none(), "a single point is not a line");
10462        assert!(
10463            parse_svg_points("1 2 3", false).is_none(),
10464            "an odd coordinate count is rejected"
10465        );
10466        assert!(parse_svg_points("\u{1F600}", false).is_none());
10467    }
10468
10469    #[test]
10470    fn parse_svg_points_valid_minimal_and_close() {
10471        let open = parse_svg_points("0,0 10,0", false).expect("two points => one line");
10472        assert_eq!(open.rings.as_ref().len(), 1);
10473        assert_eq!(open.rings.as_ref()[0].items.as_ref().len(), 1);
10474
10475        // `close` adds a segment back to the first point when it differs.
10476        let closed = parse_svg_points("0,0 10,0 10,10", true).expect("triangle");
10477        assert_eq!(
10478            closed.rings.as_ref()[0].items.as_ref().len(),
10479            3,
10480            "2 segments + 1 closing segment"
10481        );
10482
10483        // Already-closed rings do not get a duplicate closing segment.
10484        let already = parse_svg_points("0,0 10,0 0,0", true).expect("closed ring");
10485        assert_eq!(already.rings.as_ref()[0].items.as_ref().len(), 2);
10486    }
10487
10488    #[test]
10489    fn parse_svg_points_skips_unparsable_tokens_and_handles_boundaries() {
10490        // Unparsable coordinates are silently dropped, which can shift the pairing.
10491        let p = parse_svg_points("0,0 junk 10,0", false).expect("junk token dropped");
10492        assert_eq!(p.rings.as_ref()[0].items.as_ref().len(), 1);
10493
10494        let nan = parse_svg_points("NaN,0 1,1", false).expect("NaN is a parseable f32");
10495        assert_eq!(nan.rings.as_ref()[0].items.as_ref().len(), 1);
10496    }
10497
10498    #[test]
10499    fn parse_svg_points_extremely_long_terminates() {
10500        let pts = "1,2 ".repeat(20_000);
10501        let p = parse_svg_points(&pts, false).expect("20k points");
10502        assert_eq!(p.rings.as_ref()[0].items.as_ref().len(), 19_999);
10503    }
10504
10505    // ================================================================
10506    // CompactDomBuilder  (constructor / numeric)
10507    // ================================================================
10508
10509    #[test]
10510    fn compact_dom_builder_new_and_with_capacity_start_empty() {
10511        for b in [
10512            CompactDomBuilder::new(),
10513            CompactDomBuilder::with_capacity(0),
10514            CompactDomBuilder::with_capacity(1),
10515            CompactDomBuilder::with_capacity(4096),
10516        ] {
10517            let fd = b.finish();
10518            assert_eq!(fd.node_hierarchy.as_ref().len(), 0);
10519            assert_eq!(fd.node_data.as_ref().len(), 0);
10520            assert_eq!(fd.css.as_ref().len(), 0);
10521        }
10522        assert_eq!(
10523            CompactDomBuilder::default().finish().node_data.as_ref().len(),
10524            0
10525        );
10526    }
10527
10528    #[test]
10529    fn compact_dom_builder_close_node_on_an_empty_stack_is_a_no_op() {
10530        let mut b = CompactDomBuilder::new();
10531        b.close_node();
10532        b.close_node();
10533        assert_eq!(b.finish().node_hierarchy.as_ref().len(), 0, "no panic, no nodes");
10534    }
10535
10536    #[test]
10537    fn compact_dom_builder_keeps_hierarchy_and_data_parallel() {
10538        let mut b = CompactDomBuilder::new();
10539        b.open_node(NodeData::create_node(NodeType::Html));
10540        b.add_leaf(NodeData::create_text("a"));
10541        b.add_leaf(NodeData::create_text("b"));
10542        b.close_node();
10543        let fd = b.finish();
10544        assert_eq!(fd.node_data.as_ref().len(), 3);
10545        assert_eq!(
10546            fd.node_hierarchy.as_ref().len(),
10547            fd.node_data.as_ref().len(),
10548            "the two arenas must stay the same length"
10549        );
10550    }
10551
10552    #[test]
10553    fn compact_dom_builder_unclosed_node_still_finishes() {
10554        let mut b = CompactDomBuilder::new();
10555        b.open_node(NodeData::create_node(NodeType::Div));
10556        // Deliberately NOT closed.
10557        let fd = b.finish();
10558        assert_eq!(fd.node_hierarchy.as_ref().len(), 1);
10559        assert_eq!(
10560            fd.node_hierarchy.as_ref()[0].last_child, 0,
10561            "last_child stays unset when close_node() is never called"
10562        );
10563    }
10564
10565    #[test]
10566    fn compact_dom_builder_add_css_accepts_zero_and_usize_max_node_ids() {
10567        let mut b = CompactDomBuilder::new();
10568        b.add_css(0, Css::empty());
10569        b.add_css(usize::MAX, Css::empty());
10570        let fd = b.finish();
10571        assert_eq!(fd.css.as_ref().len(), 2);
10572        assert_eq!(fd.css.as_ref()[0].node_id, 0);
10573        assert_eq!(
10574            fd.css.as_ref()[1].node_id,
10575            usize::MAX,
10576            "an out-of-range node id is stored verbatim (no bounds check, no panic)"
10577        );
10578    }
10579
10580    // ================================================================
10581    // xml_node_to_dom_fast / xml_node_to_fast_dom  (numeric: depth)
10582    // ================================================================
10583
10584    #[test]
10585    fn xml_node_to_dom_fast_depth_zero_builds_children() {
10586        let map = ComponentMap::default();
10587        let n = node("div", &[], vec![txt("hi"), elem(XmlNode::create("span"))]);
10588        let dom = xml_node_to_dom_fast(&n, &map, false, 0).expect("ok");
10589        assert_eq!(dom.children.as_ref().len(), 2);
10590    }
10591
10592    #[test]
10593    fn xml_node_to_dom_fast_at_and_past_the_depth_cap_truncates_instead_of_panicking() {
10594        let map = ComponentMap::default();
10595        let n = node("div", &[], vec![txt("hi")]);
10596
10597        let at_cap = xml_node_to_dom_fast(&n, &map, false, MAX_XML_NESTING_DEPTH).expect("ok");
10598        assert!(
10599            at_cap.children.as_ref().is_empty(),
10600            "at the cap the node is emitted without children"
10601        );
10602
10603        let saturated = xml_node_to_dom_fast(&n, &map, false, usize::MAX)
10604            .expect("usize::MAX depth must not overflow when computing depth + 1");
10605        assert!(saturated.children.as_ref().is_empty());
10606
10607        let below = xml_node_to_dom_fast(&n, &map, false, MAX_XML_NESTING_DEPTH - 1).expect("ok");
10608        assert_eq!(below.children.as_ref().len(), 1, "one below the cap still recurses");
10609    }
10610
10611    #[test]
10612    fn xml_node_to_fast_dom_at_the_depth_cap_still_emits_the_node() {
10613        let map = ComponentMap::default();
10614        let n = node("div", &[], vec![txt("hi")]);
10615
10616        let mut b = CompactDomBuilder::new();
10617        xml_node_to_fast_dom(&n, &map, false, &mut b, usize::MAX).expect("no overflow");
10618        let fd = b.finish();
10619        assert_eq!(
10620            fd.node_data.as_ref().len(),
10621            1,
10622            "the node itself is still opened+closed, only its children are dropped"
10623        );
10624
10625        let mut b2 = CompactDomBuilder::new();
10626        xml_node_to_fast_dom(&n, &map, false, &mut b2, 0).expect("ok");
10627        assert_eq!(b2.finish().node_data.as_ref().len(), 2, "node + text child");
10628    }
10629
10630    #[test]
10631    fn apply_xml_node_attributes_extreme_tabindex_does_not_panic() {
10632        let map = ComponentMap::default();
10633        for v in [
10634            "0",
10635            "-1",
10636            "2147483647",
10637            "9223372036854775807",
10638            "-9223372036854775808",
10639            "99999999999999999999999999999999",
10640            "abc",
10641            "",
10642            "\u{1F600}",
10643        ] {
10644            let n = node("div", &[("tabindex", v), ("focusable", "true")], vec![]);
10645            assert!(
10646                xml_node_to_dom_fast(&n, &map, false, 0).is_ok(),
10647                "tabindex={v:?} must not panic"
10648            );
10649        }
10650    }
10651
10652    #[test]
10653    fn apply_xml_node_attributes_img_width_height_garbage_falls_back_to_zero() {
10654        let map = ComponentMap::default();
10655        let n = node(
10656            "img",
10657            &[("src", "a.png"), ("width", "-5"), ("height", "not-a-number")],
10658            vec![],
10659        );
10660        let dom = xml_node_to_dom_fast(&n, &map, false, 0).expect("ok");
10661        match dom.root.get_node_type() {
10662            NodeType::Image(_) => {}
10663            other => panic!("expected an Image node, got {other:?}"),
10664        }
10665    }
10666
10667    // ================================================================
10668    // set_stringified_attributes  (numeric: tabs / tabindex)
10669    // ================================================================
10670
10671    #[test]
10672    fn set_stringified_attributes_zero_tabs_and_empty_attrs() {
10673        let mut s = String::new();
10674        set_stringified_attributes(&mut s, &attrs(&[]), &no_args(), 0);
10675        assert_eq!(s, "", "nothing to emit for an attribute-less node");
10676    }
10677
10678    #[test]
10679    fn set_stringified_attributes_splits_ids_and_classes_on_whitespace() {
10680        let mut s = String::new();
10681        set_stringified_attributes(
10682            &mut s,
10683            &attrs(&[("id", "a  b"), ("class", "c\td")]),
10684            &no_args(),
10685            0,
10686        );
10687        assert!(s.contains(".with_id(\"a\")"), "got {s:?}");
10688        assert!(s.contains(".with_id(\"b\")"));
10689        assert!(s.contains(".with_class(\"c\")"));
10690        assert!(s.contains(".with_class(\"d\")"));
10691    }
10692
10693    #[test]
10694    fn set_stringified_attributes_tabindex_boundaries() {
10695        let cases: &[(&str, &str)] = &[
10696            ("0", "TabIndex::Auto"),
10697            ("5", "TabIndex::OverrideInParent(5)"),
10698            ("-1", "TabIndex::NoKeyboardFocus"),
10699        ];
10700        for (val, expected) in cases {
10701            let mut s = String::new();
10702            set_stringified_attributes(&mut s, &attrs(&[("tabindex", val)]), &no_args(), 0);
10703            assert!(s.contains(expected), "tabindex={val:?} => {s:?}");
10704        }
10705
10706        // Unparsable / overflowing values emit nothing rather than panicking.
10707        for val in ["abc", "", "99999999999999999999999999999999", "1.5"] {
10708            let mut s = String::new();
10709            set_stringified_attributes(&mut s, &attrs(&[("tabindex", val)]), &no_args(), 0);
10710            assert!(
10711                !s.contains("TabIndex"),
10712                "tabindex={val:?} must be ignored, got {s:?}"
10713            );
10714        }
10715    }
10716
10717    #[test]
10718    fn set_stringified_attributes_focusable_only_accepts_exact_true_false() {
10719        let mut s = String::new();
10720        set_stringified_attributes(&mut s, &attrs(&[("focusable", "true")]), &no_args(), 0);
10721        assert!(s.contains("TabIndex::Auto"));
10722
10723        let mut s = String::new();
10724        set_stringified_attributes(&mut s, &attrs(&[("focusable", "false")]), &no_args(), 0);
10725        assert!(s.contains("TabIndex::NoKeyboardFocus"));
10726
10727        let mut s = String::new();
10728        set_stringified_attributes(&mut s, &attrs(&[("focusable", "TRUE")]), &no_args(), 0);
10729        assert!(s.is_empty(), "casing other than `true`/`false` is ignored, got {s:?}");
10730    }
10731
10732    #[test]
10733    fn set_stringified_attributes_large_tab_depth_does_not_overflow() {
10734        // `tabs` becomes `"    ".repeat(tabs)`; a large-but-sane nesting depth must
10735        // stay linear and allocate without panicking.
10736        let mut s = String::new();
10737        set_stringified_attributes(&mut s, &attrs(&[("id", "x")]), &no_args(), 1_000);
10738        assert!(s.contains(".with_id(\"x\")"));
10739        assert!(s.len() > 4_000, "the 1000-level indent is actually emitted");
10740    }
10741
10742    // ================================================================
10743    // group_matches / CssMatcher  (numeric: indices)
10744    // ================================================================
10745
10746    fn refs(v: &[CssPathSelector]) -> Vec<&CssPathSelector> {
10747        v.iter().collect()
10748    }
10749
10750    #[test]
10751    fn group_matches_global_matches_at_any_index() {
10752        let a = vec![CssPathSelector::Global];
10753        assert!(group_matches(&refs(&a), &[], 0, 0));
10754        assert!(
10755            group_matches(&refs(&a), &[], usize::MAX, usize::MAX),
10756            "usize::MAX indices must not overflow"
10757        );
10758    }
10759
10760    #[test]
10761    fn group_matches_type_class_id() {
10762        let div = vec![CssPathSelector::Type(NodeTypeTag::Div)];
10763        let p = vec![CssPathSelector::Type(NodeTypeTag::P)];
10764        assert!(group_matches(&refs(&div), &refs(&div), 0, 1));
10765        assert!(!group_matches(&refs(&div), &refs(&p), 0, 1));
10766        assert!(!group_matches(&refs(&div), &[], 0, 1), "an empty haystack never matches");
10767
10768        let cls = vec![CssPathSelector::Class(AzString::from("x"))];
10769        assert!(group_matches(&refs(&cls), &refs(&cls), 0, 1));
10770        let id = vec![CssPathSelector::Id(AzString::from("x"))];
10771        assert!(!group_matches(&refs(&id), &refs(&cls), 0, 1), "an id is not a class");
10772    }
10773
10774    #[test]
10775    fn group_matches_first_and_last_pseudo_at_boundaries() {
10776        let first = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::First)];
10777        assert!(group_matches(&refs(&first), &[], 0, 10));
10778        assert!(!group_matches(&refs(&first), &[], 1, 10));
10779
10780        let last = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::Last)];
10781        assert!(group_matches(&refs(&last), &[], 9, 10));
10782        assert!(!group_matches(&refs(&last), &[], 8, 10));
10783        assert!(
10784            group_matches(&refs(&last), &[], 0, 0),
10785            "parent_children == 0 saturates to 0, so index 0 counts as last"
10786        );
10787    }
10788
10789    #[test]
10790    fn group_matches_nth_child_even_odd_and_number() {
10791        let even = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10792            CssNthChildSelector::Even,
10793        ))];
10794        assert!(group_matches(&refs(&even), &[], 0, 0));
10795        assert!(!group_matches(&refs(&even), &[], 1, 0));
10796        assert!(
10797            !group_matches(&refs(&even), &[], usize::MAX, 0),
10798            "usize::MAX is odd"
10799        );
10800
10801        let odd = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10802            CssNthChildSelector::Odd,
10803        ))];
10804        assert!(group_matches(&refs(&odd), &[], 1, 0));
10805        assert!(!group_matches(&refs(&odd), &[], 2, 0));
10806
10807        let n = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10808            CssNthChildSelector::Number(u32::MAX),
10809        ))];
10810        assert!(group_matches(&refs(&n), &[], u32::MAX as usize, 0));
10811        assert!(!group_matches(&refs(&n), &[], 0, 0));
10812    }
10813
10814    #[test]
10815    fn group_matches_nth_child_pattern_zero_repeat_does_not_divide_by_zero() {
10816        let zero = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10817            CssNthChildSelector::Pattern(CssNthChildPattern {
10818                pattern_repeat: 0,
10819                offset: 0,
10820            }),
10821        ))];
10822        // `is_multiple_of(0)` is `self == 0` — no division by zero.
10823        assert!(group_matches(&refs(&zero), &[], 0, 0));
10824        assert!(!group_matches(&refs(&zero), &[], 5, 0));
10825
10826        let offset_past = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10827            CssNthChildSelector::Pattern(CssNthChildPattern {
10828                pattern_repeat: 2,
10829                offset: u32::MAX,
10830            }),
10831        ))];
10832        assert!(
10833            group_matches(&refs(&offset_past), &[], 0, 0),
10834            "index - offset saturates to 0 rather than underflowing"
10835        );
10836    }
10837
10838    #[test]
10839    fn group_matches_structural_combinators_never_match() {
10840        for sel in [
10841            CssPathSelector::Children,
10842            CssPathSelector::DirectChildren,
10843            CssPathSelector::AdjacentSibling,
10844            CssPathSelector::GeneralSibling,
10845        ] {
10846            let a = vec![sel.clone()];
10847            assert!(
10848                !group_matches(&refs(&a), &refs(&a), 0, 1),
10849                "{sel:?} is a combinator, not a matchable group member"
10850            );
10851        }
10852    }
10853
10854    #[test]
10855    fn css_matcher_empty_path_never_matches() {
10856        let m = CssMatcher {
10857            path: Vec::new(),
10858            indices_in_parent: vec![0],
10859            children_length: vec![0],
10860        };
10861        let path = CssPath {
10862            selectors: vec![CssPathSelector::Type(NodeTypeTag::Body)].into(),
10863        };
10864        assert!(!m.matches(&path), "an empty matcher path can never match");
10865
10866        let m2 = CssMatcher {
10867            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10868            indices_in_parent: vec![0],
10869            children_length: vec![0],
10870        };
10871        let empty_path = CssPath {
10872            selectors: Vec::new().into(),
10873        };
10874        assert!(!m2.matches(&empty_path), "an empty CSS path can never match");
10875    }
10876
10877    #[test]
10878    fn css_matcher_get_hash_is_deterministic_and_path_sensitive() {
10879        let a = CssMatcher {
10880            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10881            indices_in_parent: vec![0],
10882            children_length: vec![0],
10883        };
10884        let b = CssMatcher {
10885            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10886            indices_in_parent: vec![9],
10887            children_length: vec![9],
10888        };
10889        let c = CssMatcher {
10890            path: vec![CssPathSelector::Type(NodeTypeTag::Div)],
10891            indices_in_parent: vec![0],
10892            children_length: vec![0],
10893        };
10894        assert_eq!(a.get_hash(), a.get_hash(), "stable across calls");
10895        assert_eq!(
10896            a.get_hash(),
10897            b.get_hash(),
10898            "the hash covers only `path`, not the sibling indices"
10899        );
10900        assert_ne!(a.get_hash(), c.get_hash());
10901
10902        let empty = CssMatcher {
10903            path: Vec::new(),
10904            indices_in_parent: Vec::new(),
10905            children_length: Vec::new(),
10906        };
10907        let _ = empty.get_hash(); // must not panic
10908    }
10909
10910    #[test]
10911    fn css_matcher_mismatched_bookkeeping_vec_lengths_bail_out() {
10912        // `indices_in_parent` / `children_length` must be as long as the group list.
10913        let m = CssMatcher {
10914            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10915            indices_in_parent: Vec::new(),
10916            children_length: Vec::new(),
10917        };
10918        let path = CssPath {
10919            selectors: vec![CssPathSelector::Type(NodeTypeTag::Body)].into(),
10920        };
10921        assert!(
10922            !m.matches(&path),
10923            "a desynced matcher must return false, not index out of bounds"
10924        );
10925    }
10926
10927    #[test]
10928    fn get_css_blocks_and_inline_string_on_empty_css() {
10929        let m = CssMatcher {
10930            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10931            indices_in_parent: vec![0],
10932            children_length: vec![0],
10933        };
10934        assert!(get_css_blocks(&Css::empty(), &m).is_empty());
10935        assert_eq!(css_blocks_to_inline_string(&[]), "");
10936    }
10937
10938    // ================================================================
10939    // str_to_dom / str_to_dom_unstyled / parse_page_style_and_body / body_matcher
10940    // ================================================================
10941
10942    #[test]
10943    fn str_to_dom_rejects_documents_without_html_or_body() {
10944        let map = ComponentMap::with_builtin();
10945        assert_eq!(
10946            str_to_dom(&[], &map, None).unwrap_err(),
10947            DomXmlParseError::NoHtmlNode
10948        );
10949        let html_only = vec![elem(XmlNode::create("html"))];
10950        assert_eq!(
10951            str_to_dom(&html_only, &map, None).unwrap_err(),
10952            DomXmlParseError::NoBodyInHtml
10953        );
10954        assert!(str_to_dom_unstyled(&[], &map).is_err());
10955    }
10956
10957    #[test]
10958    fn str_to_dom_valid_minimal() {
10959        let map = ComponentMap::with_builtin();
10960        let d = doc("body { color: red; }", vec![elem(node("div", &[("id", "x")], vec![]))]);
10961        assert!(str_to_dom(&d, &map, None).is_ok());
10962        assert!(str_to_dom_unstyled(&d, &map).is_ok());
10963    }
10964
10965    #[test]
10966    fn str_to_dom_max_width_edge_values_do_not_panic() {
10967        let map = ComponentMap::with_builtin();
10968        let d = doc("", vec![elem(XmlNode::create("div"))]);
10969        for w in [
10970            Some(0.0f32),
10971            Some(-0.0),
10972            Some(-1.0),
10973            Some(f32::MAX),
10974            Some(f32::MIN),
10975            Some(f32::NAN),
10976            Some(f32::INFINITY),
10977            Some(f32::NEG_INFINITY),
10978            None,
10979        ] {
10980            assert!(
10981                str_to_dom(&d, &map, w).is_ok(),
10982                "max_width={w:?} is formatted straight into a CSS string and must not panic"
10983            );
10984        }
10985    }
10986
10987    #[test]
10988    fn str_to_dom_deeply_nested_body_is_depth_capped_not_stack_overflowing() {
10989        let map = ComponentMap::with_builtin();
10990        let deep = wrap_divs(2_000, node("div", &[("id", "bottom")], vec![]));
10991        let d = doc("", vec![elem(deep)]);
10992        assert!(
10993            str_to_dom(&d, &map, None).is_ok(),
10994            "children past MAX_XML_NESTING_DEPTH are dropped, not crashed on"
10995        );
10996    }
10997
10998    #[test]
10999    fn parse_page_style_and_body_and_body_matcher() {
11000        let d = doc("body { color: red; }", vec![elem(XmlNode::create("div"))]);
11001        let (css, body) = parse_page_style_and_body(&d).expect("well-formed page");
11002        assert_eq!(body.node_type.as_str(), "body");
11003        assert!(!css.rules.as_ref().is_empty(), "the <style> block is parsed");
11004
11005        let m = body_matcher(body);
11006        assert!(m.path.is_empty(), "the matcher starts with an empty path");
11007        assert_eq!(m.indices_in_parent, vec![0]);
11008        assert_eq!(m.children_length, vec![body.children.as_ref().len()]);
11009    }
11010
11011    #[test]
11012    fn parse_page_style_and_body_with_no_style_block() {
11013        let head = node("head", &[], vec![]);
11014        let body = node("body", &[], vec![]);
11015        let d = vec![elem(node("html", &[], vec![elem(head), elem(body)]))];
11016        let (css, body) = parse_page_style_and_body(&d).expect("ok");
11017        assert!(css.rules.as_ref().is_empty());
11018        assert_eq!(body.children.as_ref().len(), 0);
11019    }
11020
11021    // ================================================================
11022    // str_to_rust_code / str_to_c_code / str_to_cpp_code / str_to_python_code
11023    // ================================================================
11024
11025    #[test]
11026    fn str_to_rust_code_empty_input_is_an_error_not_a_panic() {
11027        let map = ComponentMap::with_builtin();
11028        assert!(matches!(
11029            str_to_rust_code(&[], "", &map),
11030            Err(CompileError::Xml(DomXmlParseError::NoHtmlNode))
11031        ));
11032        assert!(str_to_c_code(&[], &map).is_err());
11033        assert!(str_to_cpp_code(&[], &map).is_err());
11034        assert!(str_to_python_code(&[], &map).is_err());
11035    }
11036
11037    #[test]
11038    fn str_to_rust_code_whitespace_and_text_only_roots_are_errors() {
11039        let map = ComponentMap::with_builtin();
11040        for roots in [vec![txt("   ")], vec![txt("\t\n")], vec![txt("garbage")]] {
11041            assert!(
11042                str_to_rust_code(&roots, "", &map).is_err(),
11043                "a document with no <html> element must be rejected"
11044            );
11045        }
11046    }
11047
11048    #[test]
11049    fn str_to_rust_code_valid_minimal() {
11050        let map = ComponentMap::with_builtin();
11051        let d = doc("", vec![elem(node("p", &[], vec![txt("Hi")]))]);
11052        let src = str_to_rust_code(&d, "// imports", &map).expect("compiles");
11053        assert!(src.contains("Dom::create_body()"), "got:\n{src}");
11054        assert!(src.contains("Dom::create_p_with_text(AzString::from(\"Hi\"))"));
11055        assert!(src.contains("// imports"), "the imports blob is spliced in");
11056        assert!(src.contains("fn main()"));
11057    }
11058
11059    #[test]
11060    fn str_to_c_cpp_python_code_valid_minimal() {
11061        let map = ComponentMap::with_builtin();
11062        let d = doc("", vec![elem(node("p", &[], vec![txt("Hi")]))]);
11063
11064        let c = str_to_c_code(&d, &map).expect("compiles");
11065        assert!(c.contains("#include \"azul.h\""), "got:\n{c}");
11066        assert!(c.contains("AzDom n0 = AzDom_createBody();"));
11067        assert!(c.contains("AzDom_createPWithText(AZ_STR(\"Hi\"))"));
11068
11069        let cpp = str_to_cpp_code(&d, &map).expect("compiles");
11070        assert!(cpp.contains("#include \"azul20.hpp\""), "got:\n{cpp}");
11071        assert!(cpp.contains("Dom::create_p_with_text(String(\"Hi\"))"));
11072
11073        let py = str_to_python_code(&d, &map).expect("compiles");
11074        assert!(py.contains("import azul"), "got:\n{py}");
11075        assert!(py.contains("azul.Dom.create_p_with_text(\"Hi\")"));
11076    }
11077
11078    #[test]
11079    fn compile_targets_escape_quotes_in_text_content() {
11080        let map = ComponentMap::with_builtin();
11081        let d = doc("", vec![elem(node("div", &[], vec![txt("say \"hi\"")]))]);
11082
11083        let rust = str_to_rust_code(&d, "", &map).expect("compiles");
11084        assert!(rust.contains("say \\\"hi\\\""), "got:\n{rust}");
11085        let c = str_to_c_code(&d, &map).expect("compiles");
11086        assert!(c.contains("say \\\"hi\\\""), "got:\n{c}");
11087    }
11088
11089    #[test]
11090    fn compile_body_node_to_rust_code_on_an_empty_body() {
11091        let map = ComponentMap::with_builtin();
11092        let body = node("body", &[], vec![]);
11093        let mut extra = VecContents::default();
11094        let mut blocks = BTreeMap::new();
11095        let out = compile_body_node_to_rust_code(
11096            &body,
11097            &map,
11098            &mut extra,
11099            &mut blocks,
11100            &Css::empty(),
11101            body_matcher(&body),
11102        )
11103        .expect("ok");
11104        assert_eq!(out, "Dom::create_body()", "no children => no .with_children()");
11105    }
11106
11107    #[test]
11108    fn compile_body_node_to_rust_code_skips_whitespace_only_text_children() {
11109        let map = ComponentMap::with_builtin();
11110        let body = node("body", &[], vec![txt("   \n\t ")]);
11111        let mut extra = VecContents::default();
11112        let mut blocks = BTreeMap::new();
11113        let out = compile_body_node_to_rust_code(
11114            &body,
11115            &map,
11116            &mut extra,
11117            &mut blocks,
11118            &Css::empty(),
11119            body_matcher(&body),
11120        )
11121        .expect("ok");
11122        assert!(
11123            !out.contains("create_text"),
11124            "a whitespace-only text child emits nothing, got:\n{out}"
11125        );
11126    }
11127
11128    // ================================================================
11129    // builtin_render_fn / builtin_compile_fn  (numeric: indent)
11130    // ================================================================
11131
11132    #[test]
11133    fn builtin_render_fn_for_a_text_and_a_textless_element() {
11134        let map = ComponentMap::with_builtin();
11135        let div = map.get_unqualified("div").expect("builtin div");
11136        assert!(matches!(
11137            builtin_render_fn(div, &div.data_model, &map),
11138            ResultStyledDomRenderDomError::Ok(_)
11139        ));
11140
11141        let p = map.get_unqualified("p").expect("builtin p");
11142        assert!(matches!(
11143            builtin_render_fn(p, &p.data_model, &map),
11144            ResultStyledDomRenderDomError::Ok(_)
11145        ));
11146    }
11147
11148    #[test]
11149    fn builtin_compile_fn_ignores_indent_so_usize_max_is_safe() {
11150        let map = ComponentMap::with_builtin();
11151        let div = map.get_unqualified("div").expect("builtin div");
11152        for indent in [0usize, 1, 1024, usize::MAX] {
11153            match builtin_compile_fn(div, &CompileTarget::Rust, &div.data_model, indent) {
11154                ResultStringCompileError::Ok(s) => assert_eq!(
11155                    s.as_str(),
11156                    "Dom::create_node(NodeType::Div)",
11157                    "indent is unused by builtin_compile_fn (indent={indent})"
11158                ),
11159                ResultStringCompileError::Err(e) => panic!("unexpected error: {e:?}"),
11160            }
11161        }
11162    }
11163
11164    #[test]
11165    fn builtin_compile_fn_emits_text_and_escapes_it() {
11166        let map = ComponentMap::with_builtin();
11167        let p = map.get_unqualified("p").expect("builtin p");
11168        let data = p
11169            .data_model
11170            .clone()
11171            .with_default("text", ComponentDefaultValue::String(AzString::from("a\"b\\c")));
11172
11173        match builtin_compile_fn(p, &CompileTarget::Rust, &data, 0) {
11174            ResultStringCompileError::Ok(s) => {
11175                assert!(s.as_str().contains("a\\\"b\\\\c"), "got {}", s.as_str());
11176            }
11177            ResultStringCompileError::Err(e) => panic!("unexpected error: {e:?}"),
11178        }
11179    }
11180
11181    #[test]
11182    fn builtin_compile_fn_covers_every_target() {
11183        let map = ComponentMap::with_builtin();
11184        let div = map.get_unqualified("div").expect("builtin div");
11185        for target in [
11186            CompileTarget::Rust,
11187            CompileTarget::C,
11188            CompileTarget::Cpp,
11189            CompileTarget::Python,
11190        ] {
11191            match builtin_compile_fn(div, &target, &div.data_model, 0) {
11192                ResultStringCompileError::Ok(s) => {
11193                    assert!(!s.as_str().is_empty(), "{target:?} emitted nothing");
11194                }
11195                ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11196            }
11197        }
11198    }
11199
11200    // ================================================================
11201    // user_defined_render_fn / user_defined_compile_fn
11202    // ================================================================
11203
11204    fn every_default_kind() -> Vec<ComponentDataField> {
11205        use ComponentDefaultValue as D;
11206        vec![
11207            data_field("s", ComponentFieldType::String, Some(D::String(AzString::from("txt"))), ""),
11208            data_field("b", ComponentFieldType::Bool, Some(D::Bool(true)), ""),
11209            data_field("i32", ComponentFieldType::I32, Some(D::I32(i32::MIN)), ""),
11210            data_field("i64", ComponentFieldType::I64, Some(D::I64(i64::MIN)), ""),
11211            data_field("u32", ComponentFieldType::U32, Some(D::U32(u32::MAX)), ""),
11212            data_field("u64", ComponentFieldType::U64, Some(D::U64(u64::MAX)), ""),
11213            data_field("us", ComponentFieldType::Usize, Some(D::Usize(usize::MAX)), ""),
11214            data_field("f32", ComponentFieldType::F32, Some(D::F32(f32::NAN)), ""),
11215            data_field("f64", ComponentFieldType::F64, Some(D::F64(f64::INFINITY)), ""),
11216            data_field(
11217                "c",
11218                ComponentFieldType::ColorU,
11219                Some(D::ColorU(ColorU { r: 0, g: 0, b: 0, a: 0 })),
11220                "",
11221            ),
11222            data_field("cb", ComponentFieldType::StyledDom, Some(D::CallbackFnPointer(AzString::from("on_click"))), ""),
11223            data_field("j", ComponentFieldType::String, Some(D::Json(AzString::from("{}"))), ""),
11224            data_field("none", ComponentFieldType::String, Some(D::None), ""),
11225            data_field("missing", ComponentFieldType::String, None, ""),
11226        ]
11227    }
11228
11229    #[test]
11230    fn user_defined_render_fn_handles_every_default_value_kind() {
11231        let map = ComponentMap::with_builtin();
11232        let def = user_def("", every_default_kind());
11233        assert!(matches!(
11234            user_defined_render_fn(&def, &def.data_model, &map),
11235            ResultStyledDomRenderDomError::Ok(_)
11236        ));
11237    }
11238
11239    #[test]
11240    fn user_defined_render_fn_on_an_empty_model_and_with_css() {
11241        let map = ComponentMap::with_builtin();
11242        let empty = user_def("", Vec::new());
11243        assert!(matches!(
11244            user_defined_render_fn(&empty, &empty.data_model, &map),
11245            ResultStyledDomRenderDomError::Ok(_)
11246        ));
11247
11248        let styled = user_def(".widget { color: red; }", Vec::new());
11249        assert!(matches!(
11250            user_defined_render_fn(&styled, &styled.data_model, &map),
11251            ResultStyledDomRenderDomError::Ok(_)
11252        ));
11253    }
11254
11255    #[test]
11256    fn user_defined_render_fn_unknown_sub_component_renders_a_placeholder() {
11257        let map = ComponentMap::create(); // empty: no library can resolve the instance
11258        let def = user_def(
11259            "",
11260            vec![data_field(
11261                "child",
11262                ComponentFieldType::StyledDom,
11263                Some(ComponentDefaultValue::ComponentInstance(ComponentInstanceDefault {
11264                    library: AzString::from("nope"),
11265                    component: AzString::from("missing"),
11266                    field_overrides: Vec::new().into(),
11267                })),
11268                "",
11269            )],
11270        );
11271        assert!(
11272            matches!(
11273                user_defined_render_fn(&def, &def.data_model, &map),
11274                ResultStyledDomRenderDomError::Ok(_)
11275            ),
11276            "an unresolvable sub-component must render a placeholder, not error out"
11277        );
11278    }
11279
11280    #[test]
11281    fn user_defined_compile_fn_indent_zero_and_every_target() {
11282        let def = user_def("", every_default_kind());
11283        for target in [
11284            CompileTarget::Rust,
11285            CompileTarget::C,
11286            CompileTarget::Cpp,
11287            CompileTarget::Python,
11288        ] {
11289            match user_defined_compile_fn(&def, &target, &def.data_model, 0) {
11290                ResultStringCompileError::Ok(s) => {
11291                    assert!(!s.as_str().is_empty(), "{target:?} emitted nothing");
11292                }
11293                ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11294            }
11295        }
11296    }
11297
11298    #[test]
11299    fn user_defined_compile_fn_indent_scales_the_leading_whitespace() {
11300        // NOTE: `indent` is used as `" ".repeat(indent * 4)`, so it is NOT safe at
11301        // usize::MAX (the multiply overflows). Exercise the realistic range.
11302        let def = user_def("", Vec::new());
11303        let mut prev = 0usize;
11304        for indent in [0usize, 1, 2, 8] {
11305            match user_defined_compile_fn(&def, &CompileTarget::Rust, &def.data_model, indent) {
11306                ResultStringCompileError::Ok(s) => {
11307                    let len = s.as_str().len();
11308                    assert!(len > prev, "indent={indent} must widen the output");
11309                    prev = len;
11310                }
11311                ResultStringCompileError::Err(e) => panic!("indent={indent}: {e:?}"),
11312            }
11313        }
11314    }
11315
11316    #[test]
11317    fn user_defined_compile_fn_escapes_string_defaults() {
11318        let def = user_def(
11319            "",
11320            vec![data_field(
11321                "s",
11322                ComponentFieldType::String,
11323                Some(ComponentDefaultValue::String(AzString::from("a\"b\\c"))),
11324                "",
11325            )],
11326        );
11327        match user_defined_compile_fn(&def, &CompileTarget::Rust, &def.data_model, 0) {
11328            ResultStringCompileError::Ok(s) => {
11329                assert!(s.as_str().contains("a\\\"b\\\\c"), "got:\n{}", s.as_str());
11330            }
11331            ResultStringCompileError::Err(e) => panic!("{e:?}"),
11332        }
11333    }
11334
11335    #[test]
11336    fn push_scalar_field_appends_one_div_per_call() {
11337        let mut children: Vec<Dom> = Vec::new();
11338        push_scalar_field(&mut children, "n", &i64::MIN);
11339        push_scalar_field(&mut children, "", &f32::NAN);
11340        push_scalar_field(&mut children, "\u{1F600}", &usize::MAX);
11341        assert_eq!(children.len(), 3);
11342    }
11343
11344    // ================================================================
11345    // Structural builtins: if / for / map
11346    // ================================================================
11347
11348    #[test]
11349    fn builtin_if_for_map_component_defs_are_well_formed() {
11350        for (def, model, field) in [
11351            (builtin_if_component(), "IfData", "condition"),
11352            (builtin_for_component(), "ForData", "count"),
11353            (builtin_map_component(), "MapData", "data_json"),
11354        ] {
11355            assert_eq!(def.id.collection.as_str(), "builtin");
11356            assert_eq!(def.data_model.name.as_str(), model);
11357            assert!(
11358                def.data_model.get_field(field).is_some(),
11359                "{model} must expose `{field}`"
11360            );
11361        }
11362    }
11363
11364    #[test]
11365    fn builtin_if_render_fn_defaults_to_the_else_branch() {
11366        let map = ComponentMap::create();
11367        let def = builtin_if_component();
11368        // Missing / wrongly-typed condition => false, no panic.
11369        let empty = dm("IfData", Vec::new());
11370        assert!(matches!(
11371            builtin_if_render_fn(&def, &empty, &map),
11372            ResultStyledDomRenderDomError::Ok(_)
11373        ));
11374
11375        let truthy = def
11376            .data_model
11377            .clone()
11378            .with_default("condition", ComponentDefaultValue::Bool(true));
11379        assert!(matches!(
11380            builtin_if_render_fn(&def, &truthy, &map),
11381            ResultStyledDomRenderDomError::Ok(_)
11382        ));
11383    }
11384
11385    #[test]
11386    fn builtin_for_render_fn_handles_zero_and_a_wrongly_typed_count() {
11387        let map = ComponentMap::create();
11388        let def = builtin_for_component();
11389
11390        let zero = def
11391            .data_model
11392            .clone()
11393            .with_default("count", ComponentDefaultValue::U32(0));
11394        assert!(matches!(
11395            builtin_for_render_fn(&def, &zero, &map),
11396            ResultStyledDomRenderDomError::Ok(_)
11397        ));
11398
11399        // A non-U32 default falls back to the documented default of 3.
11400        let wrong_type = def
11401            .data_model
11402            .clone()
11403            .with_default("count", ComponentDefaultValue::String(AzString::from("9")));
11404        assert!(matches!(
11405            builtin_for_render_fn(&def, &wrong_type, &map),
11406            ResultStyledDomRenderDomError::Ok(_)
11407        ));
11408    }
11409
11410    #[test]
11411    fn builtin_map_render_fn_defaults_to_an_empty_json_array() {
11412        let map = ComponentMap::create();
11413        let def = builtin_map_component();
11414        assert!(matches!(
11415            builtin_map_render_fn(&def, &dm("MapData", Vec::new()), &map),
11416            ResultStyledDomRenderDomError::Ok(_)
11417        ));
11418        let garbage = def
11419            .data_model
11420            .clone()
11421            .with_default("data_json", ComponentDefaultValue::String(AzString::from("{{{")));
11422        assert!(
11423            matches!(
11424                builtin_map_render_fn(&def, &garbage, &map),
11425                ResultStyledDomRenderDomError::Ok(_)
11426            ),
11427            "malformed JSON must not panic — it is only echoed into a label"
11428        );
11429    }
11430
11431    #[test]
11432    fn structural_builtin_compile_fns_ignore_indent_entirely() {
11433        let cases: [(ComponentDef, ComponentCompileFn); 3] = [
11434            (builtin_if_component(), builtin_if_compile_fn),
11435            (builtin_for_component(), builtin_for_compile_fn),
11436            (builtin_map_component(), builtin_map_compile_fn),
11437        ];
11438        for (def, f) in cases {
11439            for target in [
11440                CompileTarget::Rust,
11441                CompileTarget::C,
11442                CompileTarget::Cpp,
11443                CompileTarget::Python,
11444            ] {
11445                for indent in [0usize, usize::MAX] {
11446                    match f(&def, &target, &def.data_model, indent) {
11447                        ResultStringCompileError::Ok(s) => {
11448                            assert!(!s.as_str().is_empty(), "{target:?}/{indent} emitted nothing");
11449                        }
11450                        ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11451                    }
11452                }
11453            }
11454        }
11455    }
11456
11457    // ================================================================
11458    // data_field / builtin_data_model / builtin_component_def
11459    // ================================================================
11460
11461    #[test]
11462    fn data_field_required_is_the_inverse_of_having_a_default() {
11463        let with = data_field(
11464            "x",
11465            ComponentFieldType::String,
11466            Some(ComponentDefaultValue::String(AzString::from("v"))),
11467            "d",
11468        );
11469        assert!(!with.required);
11470        assert_eq!(with.description.as_str(), "d");
11471
11472        let without = data_field("x", ComponentFieldType::String, None, "");
11473        assert!(without.required);
11474        assert!(matches!(
11475            without.default_value,
11476            OptionComponentDefaultValue::None
11477        ));
11478    }
11479
11480    #[test]
11481    fn builtin_data_model_unknown_tag_is_empty() {
11482        assert!(builtin_data_model("").is_empty());
11483        assert!(builtin_data_model("div").is_empty());
11484        assert!(builtin_data_model("\u{1F600}").is_empty());
11485        assert!(builtin_data_model(&"z".repeat(10_000)).is_empty());
11486    }
11487
11488    #[test]
11489    fn builtin_data_model_known_tags_expose_their_attributes() {
11490        let a = builtin_data_model("a");
11491        assert!(
11492            a.iter().any(|f| f.name.as_str() == "href"),
11493            "<a> must expose href"
11494        );
11495        // `src` on <img> is required (it has no default value).
11496        let img = builtin_data_model("img");
11497        let src = img
11498            .iter()
11499            .find(|f| f.name.as_str() == "src")
11500            .expect("img has src");
11501        assert!(src.required, "<img src> must be a required field");
11502        // `img` and `image` share the same model.
11503        assert_eq!(builtin_data_model("image").len(), img.len());
11504    }
11505
11506    #[test]
11507    fn builtin_component_def_default_text_controls_the_text_field() {
11508        let with_text = builtin_component_def("p", "Paragraph", Some("Hi"), "");
11509        assert_eq!(
11510            with_text.data_model.get_default_string("text").map(AzString::as_str),
11511            Some("Hi")
11512        );
11513        assert_eq!(with_text.data_model.name.as_str(), "ParagraphData");
11514        assert_eq!(with_text.id.qualified_name(), "builtin:p");
11515
11516        let no_text = builtin_component_def("div", "Div", None, "");
11517        assert!(
11518            no_text.data_model.get_field("text").is_none(),
11519            "a `None` default_text means the element has no text field at all"
11520        );
11521
11522        // An empty-string default still creates the field.
11523        let empty_text = builtin_component_def("span", "Span", Some(""), "");
11524        assert!(empty_text.data_model.get_field("text").is_some());
11525        assert_eq!(
11526            empty_text.data_model.get_default_string("text").map(AzString::as_str),
11527            Some("")
11528        );
11529    }
11530
11531    // ================================================================
11532    // xml_attrs_to_data_model
11533    // ================================================================
11534
11535    #[test]
11536    fn xml_attrs_to_data_model_overrides_defaults_from_attributes() {
11537        let base = builtin_component_def("a", "Link", Some("Link text"), "").data_model;
11538        let model = xml_attrs_to_data_model(&base, &attrs(&[("href", "/x")]), None);
11539        assert_eq!(
11540            model.get_default_string("href").map(AzString::as_str),
11541            Some("/x")
11542        );
11543        assert_eq!(
11544            model.get_default_string("text").map(AzString::as_str),
11545            Some("Link text"),
11546            "un-supplied fields keep their defaults"
11547        );
11548        assert_eq!(
11549            model.fields.as_ref().len(),
11550            base.fields.as_ref().len(),
11551            "no field is added or dropped"
11552        );
11553    }
11554
11555    #[test]
11556    fn xml_attrs_to_data_model_text_content_is_prepared_and_empty_text_is_ignored() {
11557        let base = builtin_component_def("a", "Link", Some("Link text"), "").data_model;
11558
11559        let with_text = xml_attrs_to_data_model(&base, &attrs(&[]), Some("  Hello &amp; bye  "));
11560        assert_eq!(
11561            with_text.get_default_string("text").map(AzString::as_str),
11562            Some("Hello & bye"),
11563            "text content is trimmed and entity-decoded"
11564        );
11565
11566        let blank = xml_attrs_to_data_model(&base, &attrs(&[]), Some("   \n\t "));
11567        assert_eq!(
11568            blank.get_default_string("text").map(AzString::as_str),
11569            Some("Link text"),
11570            "whitespace-only text content leaves the default intact"
11571        );
11572    }
11573
11574    #[test]
11575    fn xml_attrs_to_data_model_ignores_unknown_attributes() {
11576        let base = builtin_component_def("a", "Link", Some(""), "").data_model;
11577        let before = base.fields.as_ref().len();
11578        let model = xml_attrs_to_data_model(
11579            &base,
11580            &attrs(&[("data-nonsense", "1"), ("", ""), ("\u{1F600}", "x")]),
11581            None,
11582        );
11583        assert_eq!(
11584            model.fields.as_ref().len(),
11585            before,
11586            "unknown attributes must not create fields"
11587        );
11588    }
11589
11590    // ================================================================
11591    // DomXml
11592    // ================================================================
11593
11594    #[test]
11595    fn dom_xml_into_styled_dom_matches_the_from_impl() {
11596        let via_method: StyledDom = DomXml::default().into_styled_dom();
11597        let via_from: StyledDom = DomXml::default().into();
11598        assert_eq!(
11599            via_method, via_from,
11600            "into_styled_dom() must be exactly the From<DomXml> impl"
11601        );
11602    }
11603
11604    // ================================================================
11605    // Display impls  (serializer)
11606    // ================================================================
11607
11608    fn pos() -> XmlTextPos {
11609        XmlTextPos {
11610            row: u32::MAX,
11611            col: 0,
11612        }
11613    }
11614
11615    #[test]
11616    fn xml_text_pos_display_is_non_empty_for_edge_values() {
11617        assert_eq!(
11618            format!("{}", XmlTextPos { row: 0, col: 0 }),
11619            "line 0:0",
11620            "a zero position is still rendered"
11621        );
11622        assert_eq!(
11623            format!(
11624                "{}",
11625                XmlTextPos {
11626                    row: u32::MAX,
11627                    col: u32::MAX
11628                }
11629            ),
11630            "line 4294967295:4294967295"
11631        );
11632    }
11633
11634    #[test]
11635    fn xml_stream_error_display_covers_every_variant() {
11636        let variants = vec![
11637            XmlStreamError::UnexpectedEndOfStream,
11638            XmlStreamError::InvalidName,
11639            XmlStreamError::NonXmlChar(NonXmlCharError {
11640                ch: u32::MAX,
11641                pos: pos(),
11642            }),
11643            XmlStreamError::InvalidChar(InvalidCharError {
11644                expected: u8::MAX,
11645                got: 0,
11646                pos: pos(),
11647            }),
11648            XmlStreamError::InvalidCharMultiple(InvalidCharMultipleError {
11649                expected: 0,
11650                got: Vec::<u8>::new().into(),
11651                pos: pos(),
11652            }),
11653            XmlStreamError::InvalidQuote(InvalidQuoteError { got: 0, pos: pos() }),
11654            XmlStreamError::InvalidSpace(InvalidSpaceError { got: 0, pos: pos() }),
11655            XmlStreamError::InvalidString(InvalidStringError {
11656                got: AzString::from(""),
11657                pos: pos(),
11658            }),
11659            XmlStreamError::InvalidReference,
11660            XmlStreamError::InvalidExternalID,
11661            XmlStreamError::InvalidCommentData,
11662            XmlStreamError::InvalidCommentEnd,
11663            XmlStreamError::InvalidCharacterData,
11664        ];
11665        for v in &variants {
11666            let s = format!("{v}");
11667            assert!(!s.is_empty(), "{v:?} must render a non-empty message");
11668        }
11669        // `char::from_u32(u32::MAX)` is None — the formatter must not unwrap it.
11670        assert!(format!("{}", variants[2]).contains("None"));
11671    }
11672
11673    #[test]
11674    fn xml_parse_error_display_covers_every_variant() {
11675        let te = XmlTextError {
11676            stream_error: XmlStreamError::InvalidName,
11677            pos: pos(),
11678        };
11679        let variants = vec![
11680            XmlParseError::InvalidDeclaration(te.clone()),
11681            XmlParseError::InvalidComment(te.clone()),
11682            XmlParseError::InvalidPI(te.clone()),
11683            XmlParseError::InvalidDoctype(te.clone()),
11684            XmlParseError::InvalidEntity(te.clone()),
11685            XmlParseError::InvalidElement(te.clone()),
11686            XmlParseError::InvalidAttribute(te.clone()),
11687            XmlParseError::InvalidCdata(te.clone()),
11688            XmlParseError::InvalidCharData(te),
11689            XmlParseError::UnknownToken(pos()),
11690        ];
11691        for v in &variants {
11692            assert!(!format!("{v}").is_empty(), "{v:?} must render");
11693        }
11694    }
11695
11696    #[test]
11697    fn xml_error_display_covers_the_non_css_variants() {
11698        let variants = vec![
11699            XmlError::NoParserAvailable,
11700            XmlError::InvalidXmlPrefixUri(pos()),
11701            XmlError::UnexpectedXmlUri(pos()),
11702            XmlError::UnexpectedXmlnsUri(pos()),
11703            XmlError::InvalidElementNamePrefix(pos()),
11704            XmlError::DuplicatedNamespace(DuplicatedNamespaceError {
11705                ns: AzString::from(""),
11706                pos: pos(),
11707            }),
11708            XmlError::UnknownNamespace(UnknownNamespaceError {
11709                ns: AzString::from("\u{1F600}"),
11710                pos: pos(),
11711            }),
11712            XmlError::UnexpectedCloseTag(UnexpectedCloseTagError {
11713                expected: AzString::from("a"),
11714                actual: AzString::from("b"),
11715                pos: pos(),
11716            }),
11717            XmlError::UnexpectedEntityCloseTag(pos()),
11718            XmlError::UnknownEntityReference(UnknownEntityReferenceError {
11719                entity: AzString::from("x"),
11720                pos: pos(),
11721            }),
11722            XmlError::MalformedEntityReference(pos()),
11723            XmlError::EntityReferenceLoop(pos()),
11724            XmlError::InvalidAttributeValue(pos()),
11725            XmlError::DuplicatedAttribute(DuplicatedAttributeError {
11726                attribute: AzString::from("id"),
11727                pos: pos(),
11728            }),
11729            XmlError::NoRootNode,
11730            XmlError::SizeLimit,
11731            XmlError::DtdDetected,
11732            XmlError::MalformedHierarchy(MalformedHierarchyError {
11733                expected: AzString::from("app"),
11734                got: AzString::from("p"),
11735            }),
11736            XmlError::ParserError(XmlParseError::UnknownToken(pos())),
11737            XmlError::UnclosedRootNode,
11738            XmlError::UnexpectedDeclaration(pos()),
11739            XmlError::NodesLimitReached,
11740            XmlError::AttributesLimitReached,
11741            XmlError::NamespacesLimitReached,
11742            XmlError::InvalidName(pos()),
11743            XmlError::NonXmlChar(pos()),
11744            XmlError::InvalidChar(pos()),
11745            XmlError::InvalidChar2(pos()),
11746            XmlError::InvalidString(pos()),
11747            XmlError::InvalidExternalID(pos()),
11748            XmlError::InvalidComment(pos()),
11749            XmlError::InvalidCharacterData(pos()),
11750            XmlError::UnknownToken(pos()),
11751            XmlError::UnexpectedEndOfStream,
11752        ];
11753        for v in &variants {
11754            assert!(!format!("{v}").is_empty(), "{v:?} must render");
11755        }
11756    }
11757
11758    #[test]
11759    fn component_and_render_and_compile_error_display() {
11760        let unknown = ComponentError::UnknownComponent(AzString::from("\u{1F600}"));
11761        assert!(format!("{unknown}").contains("Unknown component"));
11762
11763        let useless = ComponentError::UselessFunctionArgument(UselessFunctionArgumentError {
11764            component_name: AzString::from("c"),
11765            argument_name: AzString::from("a"),
11766            valid_args: Vec::<AzString>::new().into(),
11767        });
11768        assert!(!format!("{useless}").is_empty());
11769
11770        let render: RenderDomError = unknown.clone().into();
11771        assert!(!format!("{render}").is_empty());
11772
11773        let compile: CompileError = render.clone().into();
11774        assert!(!format!("{compile}").is_empty());
11775
11776        let dom_xml: DomXmlParseError = render.into();
11777        assert!(!format!("{dom_xml}").is_empty());
11778        let compile2: CompileError = dom_xml.into();
11779        assert!(!format!("{compile2}").is_empty());
11780    }
11781
11782    #[test]
11783    fn dom_xml_parse_error_display_covers_the_non_css_variants() {
11784        let variants = vec![
11785            DomXmlParseError::NoHtmlNode,
11786            DomXmlParseError::MultipleHtmlRootNodes,
11787            DomXmlParseError::NoBodyInHtml,
11788            DomXmlParseError::MultipleBodyNodes,
11789            DomXmlParseError::Xml(XmlError::NoRootNode),
11790            DomXmlParseError::MalformedHierarchy(MalformedHierarchyError {
11791                expected: AzString::from("app"),
11792                got: AzString::from("p"),
11793            }),
11794            DomXmlParseError::RenderDom(RenderDomError::Component(
11795                ComponentError::UnknownComponent(AzString::from("x")),
11796            )),
11797            DomXmlParseError::Component(ComponentParseError::NotAComponent),
11798        ];
11799        for v in &variants {
11800            assert!(!format!("{v}").is_empty(), "{v:?} must render");
11801        }
11802    }
11803
11804    #[test]
11805    fn component_parse_error_display_covers_the_non_css_variants() {
11806        let variants = vec![
11807            ComponentParseError::NotAComponent,
11808            ComponentParseError::UnnamedComponent,
11809            ComponentParseError::MissingName(usize::MAX),
11810            ComponentParseError::MissingType(MissingTypeError {
11811                arg_pos: 0,
11812                arg_name: AzString::from(""),
11813            }),
11814            ComponentParseError::WhiteSpaceInComponentName(WhiteSpaceInComponentNameError {
11815                arg_pos: usize::MAX,
11816                arg_name: AzString::from("a b"),
11817            }),
11818            ComponentParseError::WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError {
11819                arg_pos: 0,
11820                arg_name: AzString::from("a"),
11821                arg_type: AzString::from("b c"),
11822            }),
11823        ];
11824        for v in &variants {
11825            assert!(!format!("{v}").is_empty(), "{v:?} must render");
11826        }
11827    }
11828
11829    // ================================================================
11830    // serde-json gated: ComponentDataModel::to_json / from_json
11831    // ================================================================
11832
11833    #[cfg(feature = "serde-json")]
11834    #[test]
11835    fn data_model_to_json_round_trips() {
11836        let m = model_with_text();
11837        let json = m.to_json().expect("serializes");
11838        let back = ComponentDataModel::from_json(&json).expect("deserializes");
11839        assert_eq!(back.name.as_str(), m.name.as_str());
11840        assert_eq!(back.fields.as_ref().len(), m.fields.as_ref().len());
11841        assert_eq!(back.get_default_string("text").map(AzString::as_str), Some("hi"));
11842    }
11843
11844    #[cfg(feature = "serde-json")]
11845    #[test]
11846    fn data_model_from_json_rejects_garbage_without_panicking() {
11847        for s in [
11848            "",
11849            "   ",
11850            "\t\n",
11851            "not json",
11852            "{",
11853            "[]",
11854            "null",
11855            "0",
11856            "-0",
11857            "9223372036854775807",
11858            "NaN",
11859            "\u{1F600}",
11860        ] {
11861            assert!(
11862                ComponentDataModel::from_json(s).is_err(),
11863                "{s:?} is not a data model"
11864            );
11865        }
11866    }
11867
11868    #[cfg(feature = "serde-json")]
11869    #[test]
11870    fn data_model_from_json_deeply_nested_input_does_not_stack_overflow() {
11871        let bomb = format!("{}{}", "[".repeat(10_000), "]".repeat(10_000));
11872        assert!(
11873            ComponentDataModel::from_json(&bomb).is_err(),
11874            "serde_json must reject the nesting bomb, not crash"
11875        );
11876    }
11877
11878    #[cfg(feature = "serde-json")]
11879    #[test]
11880    fn data_model_to_json_on_an_empty_model() {
11881        let m = dm("Empty", Vec::new());
11882        let json = m.to_json().expect("serializes");
11883        assert!(json.contains("\"fields\""), "got {json}");
11884        assert!(ComponentDataModel::from_json(&json).is_ok());
11885    }
11886}