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 after = search_from + rel + 4;
651            let after_url = &css[after..];
652            if let Some(url) = Self::extract_url_value(after_url) {
653                let mime = Self::guess_mime_from_url(&url, "");
654                let kind = Self::guess_kind_from_url(&url);
655                resources.push(ExternalResource {
656                    url: AzString::from(url),
657                    kind,
658                    mime_type: mime.into(),
659                    source_element: AzString::from("style"),
660                    source_attribute: AzString::from("url()"),
661                });
662            }
663            search_from = after;
664        }
665
666        // Handle @import "url" or @import url(...) (case-insensitive)
667        let mut search_from = 0;
668        while let Some(rel) = lower[search_from..].find("@import") {
669            let after = search_from + rel + 7;
670            let after_import = &css[after..];
671            let trimmed = after_import.trim_start();
672
673            // Match `url(` case-insensitively without allocating. `get(..4)`
674            // returns `None` if byte 4 is not a char boundary, so the slice below
675            // can never panic on multi-byte input.
676            let import_url = if trimmed.get(..4).is_some_and(|p| p.eq_ignore_ascii_case("url(")) {
677                Self::extract_url_value(&trimmed[4..])
678            } else {
679                Self::extract_quoted_string(trimmed)
680            };
681
682            if let Some(url) = import_url {
683                resources.push(ExternalResource {
684                    url: AzString::from(url),
685                    kind: ExternalResourceKind::Stylesheet,
686                    mime_type: Some(MimeTypeHint::new("text/css")).into(),
687                    source_element: AzString::from("style"),
688                    source_attribute: AzString::from("@import"),
689                });
690            }
691
692            search_from = after;
693        }
694    }
695
696    /// Extract value from url(...) - handles quoted and unquoted URLs
697    fn extract_url_value(s: &str) -> Option<String> {
698        let trimmed = s.trim_start();
699        if trimmed.starts_with('"') {
700            Self::extract_quoted_string(trimmed)
701        } else if let Some(rest) = trimmed.strip_prefix('\'') {
702            let end = rest.find('\'')?;
703            Some(rest[..end].to_string())
704        } else {
705            let end = trimmed.find(')')?;
706            Some(trimmed[..end].trim().to_string())
707        }
708    }
709
710    /// Extract a quoted string value
711    fn extract_quoted_string(s: &str) -> Option<String> {
712        if let Some(rest) = s.strip_prefix('"') {
713            let end = rest.find('"')?;
714            Some(rest[..end].to_string())
715        } else if let Some(rest) = s.strip_prefix('\'') {
716            let end = rest.find('\'')?;
717            Some(rest[..end].to_string())
718        } else {
719            None
720        }
721    }
722
723    /// Parse srcset attribute into individual URLs
724    fn parse_srcset(srcset: &str) -> Vec<String> {
725        srcset
726            .split(',')
727            .filter_map(|entry| {
728                let trimmed = entry.trim();
729                // srcset format: "url 1x" or "url 100w"
730                trimmed.split_whitespace().next().map(alloc::string::ToString::to_string)
731            })
732            .filter(|url| !url.is_empty())
733            .collect()
734    }
735
736    /// Check if a URL looks like a downloadable resource (not a page)
737    fn looks_like_resource(url: &str) -> bool {
738        let lower = url.to_lowercase();
739        // Check for common resource extensions
740        let resource_exts = [
741            ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp", ".ttf", ".otf",
742            ".woff", ".woff2", ".eot", ".css", ".js", ".mp4", ".webm", ".ogg", ".mp3", ".wav",
743            ".pdf", ".zip", ".tar", ".gz",
744        ];
745        resource_exts.iter().any(|ext| lower.ends_with(ext))
746    }
747
748    /// Guess the resource kind from URL based on file extension.
749    // `url` is lowercased into `path` below, so these literal `.ext` checks are
750    // already case-insensitive — the lint can't see the runtime lowercasing.
751    #[allow(clippy::case_sensitive_file_extension_comparisons)]
752    fn guess_kind_from_url(url: &str) -> ExternalResourceKind {
753        let lower = url.to_lowercase();
754        // Strip query string before checking extension
755        let path = lower.split('?').next().unwrap_or(&lower);
756        if path.ends_with(".png")
757            || path.ends_with(".jpg")
758            || path.ends_with(".jpeg")
759            || path.ends_with(".gif")
760            || path.ends_with(".webp")
761            || path.ends_with(".svg")
762            || path.ends_with(".bmp")
763            || path.ends_with(".avif")
764        {
765            ExternalResourceKind::Image
766        } else if path.ends_with(".ttf")
767            || path.ends_with(".otf")
768            || path.ends_with(".woff")
769            || path.ends_with(".woff2")
770            || path.ends_with(".eot")
771        {
772            ExternalResourceKind::Font
773        } else if path.ends_with(".css") {
774            ExternalResourceKind::Stylesheet
775        } else if path.ends_with(".js") || path.ends_with(".mjs") {
776            ExternalResourceKind::Script
777        } else if path.ends_with(".mp4") || path.ends_with(".webm") || path.ends_with(".ogg") {
778            ExternalResourceKind::Video
779        } else if path.ends_with(".mp3") || path.ends_with(".wav") || path.ends_with(".flac") {
780            ExternalResourceKind::Audio
781        } else if path.ends_with(".ico") {
782            ExternalResourceKind::Icon
783        } else {
784            ExternalResourceKind::Unknown
785        }
786    }
787
788    /// Guess MIME type from URL based on extension
789    fn guess_mime_from_url(url: &str, category: &str) -> Option<MimeTypeHint> {
790        let lower = url.to_lowercase();
791        // Find extension
792        let ext = lower.rsplit('.').next()?;
793        // Remove query string if present
794        let ext = ext.split('?').next()?;
795
796        // Check if it's a valid extension
797        let valid_exts = [
798            "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "avif", "ttf", "otf", "woff",
799            "woff2", "eot", "css", "js", "mjs", "mp4", "webm", "ogg", "mp3", "wav", "flac",
800        ];
801
802        if valid_exts.contains(&ext) {
803            Some(MimeTypeHint::from_extension(ext))
804        } else if !category.is_empty() {
805            // Use category hint for default
806            match category {
807                "image" => Some(MimeTypeHint::new("image/*")),
808                "font" => Some(MimeTypeHint::new("font/*")),
809                "stylesheet" => Some(MimeTypeHint::new("text/css")),
810                "script" => Some(MimeTypeHint::new("application/javascript")),
811                "video" => Some(MimeTypeHint::new("video/*")),
812                "audio" => Some(MimeTypeHint::new("audio/*")),
813                _ => None,
814            }
815        } else {
816            None
817        }
818    }
819}
820
821#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
822#[repr(C)]
823pub struct NonXmlCharError {
824    pub ch: u32, /* u32 = char, but ABI stable */
825    pub pos: XmlTextPos,
826}
827
828#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
829#[repr(C)]
830pub struct InvalidCharError {
831    pub expected: u8,
832    pub got: u8,
833    pub pos: XmlTextPos,
834}
835
836#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
837#[repr(C)]
838pub struct InvalidCharMultipleError {
839    pub expected: u8,
840    pub got: U8Vec,
841    pub pos: XmlTextPos,
842}
843
844#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
845#[repr(C)]
846pub struct InvalidQuoteError {
847    pub got: u8,
848    pub pos: XmlTextPos,
849}
850
851#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
852#[repr(C)]
853pub struct InvalidSpaceError {
854    pub got: u8,
855    pub pos: XmlTextPos,
856}
857
858#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
859#[repr(C)]
860pub struct InvalidStringError {
861    pub got: AzString,
862    pub pos: XmlTextPos,
863}
864
865#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
866#[repr(C, u8)]
867pub enum XmlStreamError {
868    UnexpectedEndOfStream,
869    InvalidName,
870    NonXmlChar(NonXmlCharError),
871    InvalidChar(InvalidCharError),
872    InvalidCharMultiple(InvalidCharMultipleError),
873    InvalidQuote(InvalidQuoteError),
874    InvalidSpace(InvalidSpaceError),
875    InvalidString(InvalidStringError),
876    InvalidReference,
877    InvalidExternalID,
878    InvalidCommentData,
879    InvalidCommentEnd,
880    InvalidCharacterData,
881}
882
883impl fmt::Display for XmlStreamError {
884    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
885        use self::XmlStreamError::{UnexpectedEndOfStream, InvalidName, NonXmlChar, InvalidChar, InvalidCharMultiple, InvalidQuote, InvalidSpace, InvalidString, InvalidReference, InvalidExternalID, InvalidCommentData, InvalidCommentEnd, InvalidCharacterData};
886        match self {
887            UnexpectedEndOfStream => write!(f, "Unexpected end of stream"),
888            InvalidName => write!(f, "Invalid name"),
889            NonXmlChar(nx) => write!(
890                f,
891                "Non-XML character: {:?} at {}",
892                core::char::from_u32(nx.ch),
893                nx.pos
894            ),
895            InvalidChar(ic) => write!(
896                f,
897                "Invalid character: expected: {}, got: {} at {}",
898                ic.expected as char, ic.got as char, ic.pos
899            ),
900            InvalidCharMultiple(imc) => write!(
901                f,
902                "Multiple invalid characters: expected: {}, got: {:?} at {}",
903                imc.expected,
904                imc.got.as_ref(),
905                imc.pos
906            ),
907            InvalidQuote(iq) => write!(f, "Invalid quote: got {} at {}", iq.got as char, iq.pos),
908            InvalidSpace(is) => write!(f, "Invalid space: got {} at {}", is.got as char, is.pos),
909            InvalidString(ise) => write!(
910                f,
911                "Invalid string: got \"{}\" at {}",
912                ise.got.as_str(),
913                ise.pos
914            ),
915            InvalidReference => write!(f, "Invalid reference"),
916            InvalidExternalID => write!(f, "Invalid external ID"),
917            InvalidCommentData => write!(f, "Invalid comment data"),
918            InvalidCommentEnd => write!(f, "Invalid comment end"),
919            InvalidCharacterData => write!(f, "Invalid character data"),
920        }
921    }
922}
923
924#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Ord, Hash, Eq)]
925#[repr(C)]
926pub struct XmlTextPos {
927    pub row: u32,
928    pub col: u32,
929}
930
931impl fmt::Display for XmlTextPos {
932    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
933        write!(f, "line {}:{}", self.row, self.col)
934    }
935}
936
937#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
938#[repr(C)]
939pub struct XmlTextError {
940    pub stream_error: XmlStreamError,
941    pub pos: XmlTextPos,
942}
943
944#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
945#[repr(C, u8)]
946pub enum XmlParseError {
947    InvalidDeclaration(XmlTextError),
948    InvalidComment(XmlTextError),
949    InvalidPI(XmlTextError),
950    InvalidDoctype(XmlTextError),
951    InvalidEntity(XmlTextError),
952    InvalidElement(XmlTextError),
953    InvalidAttribute(XmlTextError),
954    InvalidCdata(XmlTextError),
955    InvalidCharData(XmlTextError),
956    UnknownToken(XmlTextPos),
957}
958
959impl fmt::Display for XmlParseError {
960    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
961        use self::XmlParseError::{InvalidDeclaration, InvalidComment, InvalidPI, InvalidDoctype, InvalidEntity, InvalidElement, InvalidAttribute, InvalidCdata, InvalidCharData, UnknownToken};
962        match self {
963            InvalidDeclaration(e) => {
964                write!(f, "Invalid declaration: {} at {}", e.stream_error, e.pos)
965            }
966            InvalidComment(e) => write!(f, "Invalid comment: {} at {}", e.stream_error, e.pos),
967            InvalidPI(e) => write!(
968                f,
969                "Invalid processing instruction: {} at {}",
970                e.stream_error, e.pos
971            ),
972            InvalidDoctype(e) => write!(f, "Invalid doctype: {} at {}", e.stream_error, e.pos),
973            InvalidEntity(e) => write!(f, "Invalid entity: {} at {}", e.stream_error, e.pos),
974            InvalidElement(e) => write!(f, "Invalid element: {} at {}", e.stream_error, e.pos),
975            InvalidAttribute(e) => write!(f, "Invalid attribute: {} at {}", e.stream_error, e.pos),
976            InvalidCdata(e) => write!(f, "Invalid CDATA: {} at {}", e.stream_error, e.pos),
977            InvalidCharData(e) => write!(f, "Invalid char data: {} at {}", e.stream_error, e.pos),
978            UnknownToken(e) => write!(f, "Unknown token at {e}"),
979        }
980    }
981}
982
983impl_result!(
984    Xml,
985    XmlError,
986    ResultXmlXmlError,
987    copy = false,
988    [Debug, PartialEq, Eq, PartialOrd, Clone]
989);
990
991#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
992#[repr(C)]
993pub struct DuplicatedNamespaceError {
994    pub ns: AzString,
995    pub pos: XmlTextPos,
996}
997
998#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
999#[repr(C)]
1000pub struct UnknownNamespaceError {
1001    pub ns: AzString,
1002    pub pos: XmlTextPos,
1003}
1004
1005#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1006#[repr(C)]
1007pub struct UnexpectedCloseTagError {
1008    pub expected: AzString,
1009    pub actual: AzString,
1010    pub pos: XmlTextPos,
1011}
1012
1013#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1014#[repr(C)]
1015pub struct UnknownEntityReferenceError {
1016    pub entity: AzString,
1017    pub pos: XmlTextPos,
1018}
1019
1020#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1021#[repr(C)]
1022pub struct DuplicatedAttributeError {
1023    pub attribute: AzString,
1024    pub pos: XmlTextPos,
1025}
1026
1027/// Error for mismatched open/close tags in XML hierarchy
1028#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1029#[repr(C)]
1030pub struct MalformedHierarchyError {
1031    /// The tag that was expected (from the opening tag)
1032    pub expected: AzString,
1033    /// The tag that was actually found (the closing tag)
1034    pub got: AzString,
1035}
1036
1037#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1038#[repr(C, u8)]
1039pub enum XmlError {
1040    NoParserAvailable,
1041    InvalidXmlPrefixUri(XmlTextPos),
1042    UnexpectedXmlUri(XmlTextPos),
1043    UnexpectedXmlnsUri(XmlTextPos),
1044    InvalidElementNamePrefix(XmlTextPos),
1045    DuplicatedNamespace(DuplicatedNamespaceError),
1046    UnknownNamespace(UnknownNamespaceError),
1047    UnexpectedCloseTag(UnexpectedCloseTagError),
1048    UnexpectedEntityCloseTag(XmlTextPos),
1049    UnknownEntityReference(UnknownEntityReferenceError),
1050    MalformedEntityReference(XmlTextPos),
1051    EntityReferenceLoop(XmlTextPos),
1052    InvalidAttributeValue(XmlTextPos),
1053    DuplicatedAttribute(DuplicatedAttributeError),
1054    NoRootNode,
1055    SizeLimit,
1056    DtdDetected,
1057    /// Invalid hierarchy close tags, i.e `<app></p></app>`
1058    MalformedHierarchy(MalformedHierarchyError),
1059    ParserError(XmlParseError),
1060    UnclosedRootNode,
1061    UnexpectedDeclaration(XmlTextPos),
1062    NodesLimitReached,
1063    AttributesLimitReached,
1064    NamespacesLimitReached,
1065    InvalidName(XmlTextPos),
1066    NonXmlChar(XmlTextPos),
1067    InvalidChar(XmlTextPos),
1068    InvalidChar2(XmlTextPos),
1069    InvalidString(XmlTextPos),
1070    InvalidExternalID(XmlTextPos),
1071    InvalidComment(XmlTextPos),
1072    InvalidCharacterData(XmlTextPos),
1073    UnknownToken(XmlTextPos),
1074    UnexpectedEndOfStream,
1075}
1076
1077impl fmt::Display for XmlError {
1078    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1079        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};
1080        match self {
1081            NoParserAvailable => write!(
1082                f,
1083                "Library was compiled without XML parser (XML parser not available)"
1084            ),
1085            InvalidXmlPrefixUri(pos) => {
1086                write!(f, "Invalid XML Prefix URI at line {}:{}", pos.row, pos.col)
1087            }
1088            UnexpectedXmlUri(pos) => {
1089                write!(f, "Unexpected XML URI at line {}:{}", pos.row, pos.col)
1090            }
1091            UnexpectedXmlnsUri(pos) => write!(
1092                f,
1093                "Unexpected XML namespace URI at line {}:{}",
1094                pos.row, pos.col
1095            ),
1096            InvalidElementNamePrefix(pos) => write!(
1097                f,
1098                "Invalid element name prefix at line {}:{}",
1099                pos.row, pos.col
1100            ),
1101            DuplicatedNamespace(ns) => write!(
1102                f,
1103                "Duplicated namespace: \"{}\" at {}",
1104                ns.ns.as_str(),
1105                ns.pos
1106            ),
1107            UnknownNamespace(uns) => write!(
1108                f,
1109                "Unknown namespace: \"{}\" at {}",
1110                uns.ns.as_str(),
1111                uns.pos
1112            ),
1113            UnexpectedCloseTag(ct) => write!(
1114                f,
1115                "Unexpected close tag: expected \"{}\", got \"{}\" at {}",
1116                ct.expected.as_str(),
1117                ct.actual.as_str(),
1118                ct.pos
1119            ),
1120            UnexpectedEntityCloseTag(pos) => write!(
1121                f,
1122                "Unexpected entity close tag at line {}:{}",
1123                pos.row, pos.col
1124            ),
1125            UnknownEntityReference(uer) => write!(
1126                f,
1127                "Unexpected entity reference: \"{}\" at {}",
1128                uer.entity, uer.pos
1129            ),
1130            MalformedEntityReference(pos) => write!(
1131                f,
1132                "Malformed entity reference at line {}:{}",
1133                pos.row, pos.col
1134            ),
1135            EntityReferenceLoop(pos) => write!(
1136                f,
1137                "Entity reference loop (recursive entity reference) at line {}:{}",
1138                pos.row, pos.col
1139            ),
1140            InvalidAttributeValue(pos) => {
1141                write!(f, "Invalid attribute value at line {}:{}", pos.row, pos.col)
1142            }
1143            DuplicatedAttribute(ae) => write!(
1144                f,
1145                "Duplicated attribute \"{}\" at line {}:{}",
1146                ae.attribute.as_str(),
1147                ae.pos.row,
1148                ae.pos.col
1149            ),
1150            NoRootNode => write!(f, "No root node found"),
1151            SizeLimit => write!(f, "XML file too large (size limit reached)"),
1152            DtdDetected => write!(f, "Document type descriptor detected"),
1153            MalformedHierarchy(e) => write!(
1154                f,
1155                "Malformed hierarchy: expected <{}/> closing tag, got <{}/>",
1156                e.expected.as_str(),
1157                e.got.as_str()
1158            ),
1159            ParserError(p) => write!(f, "{p}"),
1160            UnclosedRootNode => write!(f, "unclosed root node"),
1161            UnexpectedDeclaration(tp) => write!(f, "unexpected declaration at {tp}"),
1162            NodesLimitReached => write!(f, "nodes limit reached"),
1163            AttributesLimitReached => write!(f, "attributes limit reached"),
1164            NamespacesLimitReached => write!(f, "namespaces limit reached"),
1165            InvalidName(tp) => write!(f, "invalid name at {tp}"),
1166            NonXmlChar(tp) => write!(f, "non xml char at {tp}"),
1167            InvalidChar(tp) => write!(f, "invalid char at {tp}"),
1168            InvalidChar2(tp) => write!(f, "invalid char2 at {tp}"),
1169            InvalidString(tp) => write!(f, "invalid string at {tp}"),
1170            InvalidExternalID(tp) => write!(f, "invalid externalid at {tp}"),
1171            InvalidComment(tp) => write!(f, "invalid comment at {tp}"),
1172            InvalidCharacterData(tp) => write!(f, "invalid character data at {tp}"),
1173            UnknownToken(tp) => write!(f, "unknown token at {tp}"),
1174            UnexpectedEndOfStream => write!(f, "unexpected end of stream"),
1175        }
1176    }
1177}
1178
1179// ============================================================================
1180// New repr(C) component system
1181// ============================================================================
1182
1183/// Identifies a component within a library collection.
1184/// e.g. collection="builtin", name="div" for the `<div>` element,
1185/// or collection="shadcn", name="avatar" for a custom component.
1186#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1187#[repr(C)]
1188pub struct ComponentId {
1189    /// Library / collection name: "builtin", "shadcn", "myproject"
1190    pub collection: AzString,
1191    /// Component name within the collection: "div", "avatar", "card"
1192    pub name: AzString,
1193}
1194
1195impl ComponentId {
1196    #[must_use] pub fn builtin(name: &str) -> Self {
1197        Self {
1198            collection: AzString::from_const_str("builtin"),
1199            name: AzString::from(name),
1200        }
1201    }
1202
1203    #[must_use] pub fn new(collection: &str, name: &str) -> Self {
1204        Self {
1205            collection: AzString::from(collection),
1206            name: AzString::from(name),
1207        }
1208    }
1209
1210    /// Returns "collection:name" format string
1211    #[must_use] pub fn qualified_name(&self) -> String {
1212        format!("{}:{}", self.collection.as_str(), self.name.as_str())
1213    }
1214}
1215
1216// ============================================================================
1217// Component type system — rich type descriptors for component fields
1218// ============================================================================
1219
1220/// A single argument in a callback signature.
1221#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1222#[repr(C)]
1223pub struct ComponentCallbackArg {
1224    /// Argument name, e.g. "`button_id`"
1225    pub name: AzString,
1226    /// Argument type
1227    pub arg_type: ComponentFieldType,
1228}
1229
1230impl_vec!(
1231    ComponentCallbackArg,
1232    ComponentCallbackArgVec,
1233    ComponentCallbackArgVecDestructor,
1234    ComponentCallbackArgVecDestructorType,
1235    ComponentCallbackArgVecSlice,
1236    OptionComponentCallbackArg
1237);
1238impl_option!(
1239    ComponentCallbackArg,
1240    OptionComponentCallbackArg,
1241    copy = false,
1242    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1243);
1244impl_vec_debug!(ComponentCallbackArg, ComponentCallbackArgVec);
1245impl_vec_partialeq!(ComponentCallbackArg, ComponentCallbackArgVec);
1246impl_vec_eq!(ComponentCallbackArg, ComponentCallbackArgVec);
1247impl_vec_partialord!(ComponentCallbackArg, ComponentCallbackArgVec);
1248impl_vec_ord!(ComponentCallbackArg, ComponentCallbackArgVec);
1249impl_vec_hash!(ComponentCallbackArg, ComponentCallbackArgVec);
1250impl_vec_clone!(
1251    ComponentCallbackArg,
1252    ComponentCallbackArgVec,
1253    ComponentCallbackArgVecDestructor
1254);
1255
1256/// Callback signature: return type + argument list.
1257#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1258#[repr(C)]
1259pub struct ComponentCallbackSignature {
1260    /// Return type name, e.g. "Update"
1261    pub return_type: AzString,
1262    /// Callback arguments (excluding the implicit `&mut RefAny` and `&mut CallbackInfo`)
1263    pub args: ComponentCallbackArgVec,
1264}
1265
1266/// Heap-allocated box for recursive `ComponentFieldType` (e.g. `Option<String>`).
1267/// Uses raw pointer indirection to break the infinite size.
1268#[repr(C)]
1269pub struct ComponentFieldTypeBox {
1270    pub ptr: *mut ComponentFieldType,
1271}
1272
1273impl ComponentFieldTypeBox {
1274    #[must_use] pub fn new(t: ComponentFieldType) -> Self {
1275        Self {
1276            ptr: Box::into_raw(Box::new(t)),
1277        }
1278    }
1279
1280    #[must_use] pub fn as_ref(&self) -> &ComponentFieldType {
1281        unsafe { &*self.ptr }
1282    }
1283}
1284
1285impl Clone for ComponentFieldTypeBox {
1286    fn clone(&self) -> Self {
1287        Self::new(unsafe { (*self.ptr).clone() })
1288    }
1289}
1290
1291impl Drop for ComponentFieldTypeBox {
1292    fn drop(&mut self) {
1293        // Null the pointer as we free it, so a *second* drop is a no-op instead
1294        // of a double free. This type is a by-value payload of the
1295        // `ComponentFieldType` enum, whose codegen FFI mirror gets
1296        // `impl Drop { _delete }` (= drop_in_place of the real type) AND Rust
1297        // field drop-glue — dropping each by-value field twice. Without this
1298        // take-and-null the second drop would `Box::from_raw` a dangling pointer.
1299        let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
1300        if !ptr.is_null() {
1301            unsafe {
1302                drop(Box::from_raw(ptr));
1303            }
1304        }
1305    }
1306}
1307
1308impl fmt::Debug for ComponentFieldTypeBox {
1309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1310        if self.ptr.is_null() {
1311            write!(f, "ComponentFieldTypeBox(null)")
1312        } else {
1313            write!(f, "ComponentFieldTypeBox({:?})", unsafe { &*self.ptr })
1314        }
1315    }
1316}
1317
1318impl PartialEq for ComponentFieldTypeBox {
1319    fn eq(&self, other: &Self) -> bool {
1320        if self.ptr.is_null() && other.ptr.is_null() {
1321            return true;
1322        }
1323        if self.ptr.is_null() || other.ptr.is_null() {
1324            return false;
1325        }
1326        unsafe { *self.ptr == *other.ptr }
1327    }
1328}
1329
1330impl Eq for ComponentFieldTypeBox {}
1331
1332impl PartialOrd for ComponentFieldTypeBox {
1333    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1334        Some(self.cmp(other))
1335    }
1336}
1337
1338impl Ord for ComponentFieldTypeBox {
1339    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1340        match (self.ptr.is_null(), other.ptr.is_null()) {
1341            (true, true) => core::cmp::Ordering::Equal,
1342            (true, false) => core::cmp::Ordering::Less,
1343            (false, true) => core::cmp::Ordering::Greater,
1344            (false, false) => unsafe { (*self.ptr).cmp(&*other.ptr) },
1345        }
1346    }
1347}
1348
1349impl Hash for ComponentFieldTypeBox {
1350    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1351        if !self.ptr.is_null() {
1352            unsafe {
1353                (*self.ptr).hash(state);
1354            }
1355        }
1356    }
1357}
1358
1359/// Heap-allocated box for recursive `ComponentFieldValue` (e.g. `Some(value)`).
1360/// Uses raw pointer indirection to break the infinite size.
1361#[repr(C)]
1362pub struct ComponentFieldValueBox {
1363    pub ptr: *mut ComponentFieldValue,
1364}
1365
1366impl ComponentFieldValueBox {
1367    #[must_use] pub fn new(v: ComponentFieldValue) -> Self {
1368        Self {
1369            ptr: Box::into_raw(Box::new(v)),
1370        }
1371    }
1372
1373    #[must_use] pub fn as_ref(&self) -> &ComponentFieldValue {
1374        unsafe { &*self.ptr }
1375    }
1376}
1377
1378impl Clone for ComponentFieldValueBox {
1379    fn clone(&self) -> Self {
1380        Self::new(unsafe { (*self.ptr).clone() })
1381    }
1382}
1383
1384impl Drop for ComponentFieldValueBox {
1385    fn drop(&mut self) {
1386        // Take-and-null so a second drop (codegen FFI double-drop of a by-value
1387        // field, see `ComponentFieldTypeBox`) is a no-op, not a double free.
1388        let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
1389        if !ptr.is_null() {
1390            unsafe {
1391                drop(Box::from_raw(ptr));
1392            }
1393        }
1394    }
1395}
1396
1397impl fmt::Debug for ComponentFieldValueBox {
1398    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1399        if self.ptr.is_null() {
1400            write!(f, "ComponentFieldValueBox(null)")
1401        } else {
1402            write!(f, "ComponentFieldValueBox({:?})", unsafe { &*self.ptr })
1403        }
1404    }
1405}
1406
1407impl PartialEq for ComponentFieldValueBox {
1408    fn eq(&self, other: &Self) -> bool {
1409        if self.ptr.is_null() && other.ptr.is_null() {
1410            return true;
1411        }
1412        if self.ptr.is_null() || other.ptr.is_null() {
1413            return false;
1414        }
1415        unsafe { *self.ptr == *other.ptr }
1416    }
1417}
1418
1419/// Rich type descriptor for a component field.
1420/// Replaces the old `AzString` type names ("String", "bool", etc.) with
1421/// a structured enum that the debugger can use for type-aware editing.
1422#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1423#[repr(C, u8)]
1424pub enum ComponentFieldType {
1425    String,
1426    Bool,
1427    I32,
1428    I64,
1429    U32,
1430    U64,
1431    Usize,
1432    F32,
1433    F64,
1434    ColorU,
1435    CssProperty,
1436    ImageRef,
1437    FontRef,
1438    /// `StyledDom` slot — field name = slot name
1439    StyledDom,
1440    /// Callback with typed signature
1441    Callback(ComponentCallbackSignature),
1442    /// `RefAny` data binding with type hint
1443    RefAny(AzString),
1444    /// Optional value (recursive via Box)
1445    OptionType(ComponentFieldTypeBox),
1446    /// Vec of values (recursive via Box)
1447    VecType(ComponentFieldTypeBox),
1448    /// Reference to a struct defined in the same library
1449    StructRef(AzString),
1450    /// Reference to an enum defined in the same library
1451    EnumRef(AzString),
1452}
1453
1454impl ComponentFieldType {
1455    /// Parse a field type string like "String", "Option<Bool>", "Vec<I32>",
1456    /// "Callback(fn(LayoutCallbackInfo) -> Dom)", "StructRef(MyStruct)" etc.
1457    /// Returns `None` if the string cannot be parsed.
1458    #[must_use] pub fn parse(s: &str) -> Option<Self> {
1459        Self::parse_depth(s, 0)
1460    }
1461
1462    /// Depth-bounded implementation of [`parse`](Self::parse).
1463    ///
1464    /// AUDIT 2026-07-08: `Option<..>` / `Vec<..>` wrappers recurse once per level,
1465    /// so an attacker string like `"Option<".repeat(100_000)` (with matching `>`)
1466    /// overflowed the stack. Recursion is capped at [`MAX_TYPE_PARSE_DEPTH`];
1467    /// beyond it, parsing fails (`None`) instead of crashing.
1468    fn parse_depth(s: &str, depth: usize) -> Option<Self> {
1469        if depth > MAX_TYPE_PARSE_DEPTH {
1470            return None;
1471        }
1472        let s = s.trim();
1473        match s {
1474            "String" | "string" => return Some(Self::String),
1475            "Bool" | "bool" => return Some(Self::Bool),
1476            "I32" | "i32" => return Some(Self::I32),
1477            "I64" | "i64" => return Some(Self::I64),
1478            "U32" | "u32" => return Some(Self::U32),
1479            "U64" | "u64" => return Some(Self::U64),
1480            "Usize" | "usize" => return Some(Self::Usize),
1481            "F32" | "f32" => return Some(Self::F32),
1482            "F64" | "f64" => return Some(Self::F64),
1483            "ColorU" => return Some(Self::ColorU),
1484            "CssProperty" => return Some(Self::CssProperty),
1485            "ImageRef" => return Some(Self::ImageRef),
1486            "FontRef" => return Some(Self::FontRef),
1487            "StyledDom" => return Some(Self::StyledDom),
1488            "RefAny" => return Some(Self::RefAny(AzString::from(""))),
1489            _ => {}
1490        }
1491
1492        // Option<T>
1493        if let Some(inner) = s.strip_prefix("Option<").and_then(|r| r.strip_suffix('>')) {
1494            let inner_type = Self::parse_depth(inner, depth + 1)?;
1495            return Some(Self::OptionType(ComponentFieldTypeBox::new(
1496                inner_type,
1497            )));
1498        }
1499
1500        // Vec<T>
1501        if let Some(inner) = s.strip_prefix("Vec<").and_then(|r| r.strip_suffix('>')) {
1502            let inner_type = Self::parse_depth(inner, depth + 1)?;
1503            return Some(Self::VecType(ComponentFieldTypeBox::new(
1504                inner_type,
1505            )));
1506        }
1507
1508        // Callback(signature)
1509        if let Some(sig) = s
1510            .strip_prefix("Callback(")
1511            .and_then(|r| r.strip_suffix(')'))
1512        {
1513            return Some(Self::Callback(ComponentCallbackSignature {
1514                return_type: AzString::from(sig),
1515                args: Vec::new().into(),
1516            }));
1517        }
1518
1519        // RefAny(TypeHint)
1520        if let Some(hint) = s.strip_prefix("RefAny(").and_then(|r| r.strip_suffix(')')) {
1521            return Some(Self::RefAny(AzString::from(hint)));
1522        }
1523
1524        // EnumRef(Name) — explicit
1525        if let Some(name) = s.strip_prefix("EnumRef(").and_then(|r| r.strip_suffix(')')) {
1526            return Some(Self::EnumRef(AzString::from(name)));
1527        }
1528
1529        // StructRef(Name) — explicit
1530        if let Some(name) = s
1531            .strip_prefix("StructRef(")
1532            .and_then(|r| r.strip_suffix(')'))
1533        {
1534            return Some(Self::StructRef(AzString::from(name)));
1535        }
1536
1537        // If starts with uppercase, treat as StructRef
1538        if s.chars().next().is_some_and(char::is_uppercase) {
1539            return Some(Self::StructRef(AzString::from(s)));
1540        }
1541
1542        None
1543    }
1544
1545    /// Format this field type to its canonical string representation.
1546    /// This is the inverse of `parse`.
1547    #[must_use] pub fn format(&self) -> String {
1548        match self {
1549            Self::String => "String".to_string(),
1550            Self::Bool => "Bool".to_string(),
1551            Self::I32 => "I32".to_string(),
1552            Self::I64 => "I64".to_string(),
1553            Self::U32 => "U32".to_string(),
1554            Self::U64 => "U64".to_string(),
1555            Self::Usize => "Usize".to_string(),
1556            Self::F32 => "F32".to_string(),
1557            Self::F64 => "F64".to_string(),
1558            Self::ColorU => "ColorU".to_string(),
1559            Self::CssProperty => "CssProperty".to_string(),
1560            Self::ImageRef => "ImageRef".to_string(),
1561            Self::FontRef => "FontRef".to_string(),
1562            Self::StyledDom => "StyledDom".to_string(),
1563            Self::Callback(sig) => format!("Callback({})", sig.return_type.as_str()),
1564            Self::RefAny(hint) => {
1565                if hint.as_str().is_empty() {
1566                    "RefAny".to_string()
1567                } else {
1568                    format!("RefAny({})", hint.as_str())
1569                }
1570            }
1571            Self::OptionType(inner) => format!("Option<{}>", inner.as_ref().format()),
1572            Self::VecType(inner) => format!("Vec<{}>", inner.as_ref().format()),
1573            Self::StructRef(name) | Self::EnumRef(name) => name.as_str().to_string(),
1574        }
1575    }
1576}
1577
1578impl fmt::Display for ComponentFieldType {
1579    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1580        f.write_str(&self.format())
1581    }
1582}
1583
1584/// A single variant in a component enum model.
1585#[derive(Debug, Clone, PartialEq)]
1586#[repr(C)]
1587pub struct ComponentEnumVariant {
1588    /// Variant name, e.g. "Admin", "Editor", "Viewer"
1589    pub name: AzString,
1590    /// Human-readable description for this variant
1591    pub description: AzString,
1592    /// Optional associated fields for this variant
1593    pub fields: ComponentDataFieldVec,
1594}
1595
1596impl_vec!(
1597    ComponentEnumVariant,
1598    ComponentEnumVariantVec,
1599    ComponentEnumVariantVecDestructor,
1600    ComponentEnumVariantVecDestructorType,
1601    ComponentEnumVariantVecSlice,
1602    OptionComponentEnumVariant
1603);
1604impl_option!(
1605    ComponentEnumVariant,
1606    OptionComponentEnumVariant,
1607    copy = false,
1608    [Debug, Clone, PartialEq]
1609);
1610impl_vec_debug!(ComponentEnumVariant, ComponentEnumVariantVec);
1611impl_vec_partialeq!(ComponentEnumVariant, ComponentEnumVariantVec);
1612impl_vec_clone!(
1613    ComponentEnumVariant,
1614    ComponentEnumVariantVec,
1615    ComponentEnumVariantVecDestructor
1616);
1617
1618/// A named enum model for code generation.
1619/// Stored in `ComponentLibrary::enum_models`.
1620#[derive(Debug, Clone, PartialEq)]
1621#[repr(C)]
1622pub struct ComponentEnumModel {
1623    /// Enum name, e.g. "`UserRole`"
1624    pub name: AzString,
1625    /// Human-readable description
1626    pub description: AzString,
1627    /// Variants
1628    pub variants: ComponentEnumVariantVec,
1629}
1630
1631impl_vec!(
1632    ComponentEnumModel,
1633    ComponentEnumModelVec,
1634    ComponentEnumModelVecDestructor,
1635    ComponentEnumModelVecDestructorType,
1636    ComponentEnumModelVecSlice,
1637    OptionComponentEnumModel
1638);
1639impl_option!(
1640    ComponentEnumModel,
1641    OptionComponentEnumModel,
1642    copy = false,
1643    [Debug, Clone, PartialEq]
1644);
1645impl_vec_debug!(ComponentEnumModel, ComponentEnumModelVec);
1646impl_vec_partialeq!(ComponentEnumModel, ComponentEnumModelVec);
1647impl_vec_clone!(
1648    ComponentEnumModel,
1649    ComponentEnumModelVec,
1650    ComponentEnumModelVecDestructor
1651);
1652
1653/// Default value for a component field.
1654#[derive(Debug, Clone, PartialEq)]
1655#[repr(C, u8)]
1656pub enum ComponentDefaultValue {
1657    /// No default value (field is required)
1658    None,
1659    /// String literal default
1660    String(AzString),
1661    /// Boolean default
1662    Bool(bool),
1663    /// i32 default
1664    I32(i32),
1665    /// i64 default
1666    I64(i64),
1667    /// u32 default
1668    U32(u32),
1669    /// u64 default
1670    U64(u64),
1671    /// usize default
1672    Usize(usize),
1673    /// f32 default
1674    F32(f32),
1675    /// f64 default
1676    F64(f64),
1677    /// `ColorU` default
1678    ColorU(ColorU),
1679    /// Default is an instance of another component
1680    ComponentInstance(ComponentInstanceDefault),
1681    /// Default callback function pointer name
1682    CallbackFnPointer(AzString),
1683    /// JSON string representing a complex default value
1684    Json(AzString),
1685}
1686
1687impl_option!(
1688    ComponentDefaultValue,
1689    OptionComponentDefaultValue,
1690    copy = false,
1691    [Debug, Clone, PartialEq]
1692);
1693
1694/// Default component instance for a `StyledDom` slot.
1695#[derive(Debug, Clone, PartialEq)]
1696#[repr(C)]
1697pub struct ComponentInstanceDefault {
1698    /// Library name, e.g. "builtin"
1699    pub library: AzString,
1700    /// Component tag, e.g. "a"
1701    pub component: AzString,
1702    /// Field overrides for this instance
1703    pub field_overrides: ComponentFieldOverrideVec,
1704}
1705
1706/// An override for a single field in a component instance.
1707#[derive(Debug, Clone, PartialEq, Eq)]
1708#[repr(C)]
1709pub struct ComponentFieldOverride {
1710    /// Field name to override
1711    pub field_name: AzString,
1712    /// Value source for this override
1713    pub source: ComponentFieldValueSource,
1714}
1715
1716impl_vec!(
1717    ComponentFieldOverride,
1718    ComponentFieldOverrideVec,
1719    ComponentFieldOverrideVecDestructor,
1720    ComponentFieldOverrideVecDestructorType,
1721    ComponentFieldOverrideVecSlice,
1722    OptionComponentFieldOverride
1723);
1724impl_option!(
1725    ComponentFieldOverride,
1726    OptionComponentFieldOverride,
1727    copy = false,
1728    [Debug, Clone, PartialEq, Eq]
1729);
1730impl_vec_debug!(ComponentFieldOverride, ComponentFieldOverrideVec);
1731impl_vec_partialeq!(ComponentFieldOverride, ComponentFieldOverrideVec);
1732impl_vec_clone!(
1733    ComponentFieldOverride,
1734    ComponentFieldOverrideVec,
1735    ComponentFieldOverrideVecDestructor
1736);
1737
1738/// How a field value is sourced at the instance level.
1739#[derive(Debug, Clone, PartialEq, Eq)]
1740#[repr(C, u8)]
1741pub enum ComponentFieldValueSource {
1742    /// Use the component's default value
1743    Default,
1744    /// Hardcoded literal value (as string, parsed at runtime)
1745    Literal(AzString),
1746    /// Bound to an app state path (e.g. "`app_state.user.name`")
1747    Binding(AzString),
1748}
1749#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
1750/// Runtime value for a component field — the "instance" counterpart
1751/// to `ComponentFieldType` (which is the "class" / type descriptor).
1752#[derive(Debug, Clone, PartialEq)]
1753#[repr(C, u8)]
1754#[allow(clippy::large_enum_variant)] // #[repr(C,u8)] FFI enum: boxing a variant changes the C ABI/api.json
1755pub enum ComponentFieldValue {
1756    String(AzString),
1757    Bool(bool),
1758    I32(i32),
1759    I64(i64),
1760    U32(u32),
1761    U64(u64),
1762    Usize(usize),
1763    F32(f32),
1764    F64(f64),
1765    ColorU(ColorU),
1766    /// Option<T> with no value
1767    None,
1768    /// Option<T> with a value
1769    Some(ComponentFieldValueBox),
1770    /// Vec of values
1771    Vec(ComponentFieldValueVec),
1772    /// `StyledDom` slot content
1773    StyledDom(StyledDom),
1774    /// Struct fields, in order
1775    Struct(ComponentFieldNamedValueVec),
1776    /// Enum variant
1777    Enum {
1778        variant: AzString,
1779        fields: ComponentFieldNamedValueVec,
1780    },
1781    /// Callback function reference (function name as string)
1782    Callback(AzString),
1783    /// Opaque reference-counted data
1784    RefAny(crate::refany::RefAny),
1785}
1786
1787/// Named field value: (`field_name`, value) pair.
1788#[derive(Debug, Clone, PartialEq)]
1789#[repr(C)]
1790pub struct ComponentFieldNamedValue {
1791    pub name: AzString,
1792    pub value: ComponentFieldValue,
1793}
1794
1795impl_vec!(
1796    ComponentFieldNamedValue,
1797    ComponentFieldNamedValueVec,
1798    ComponentFieldNamedValueVecDestructor,
1799    ComponentFieldNamedValueVecDestructorType,
1800    ComponentFieldNamedValueVecSlice,
1801    OptionComponentFieldNamedValue
1802);
1803impl_option!(
1804    ComponentFieldNamedValue,
1805    OptionComponentFieldNamedValue,
1806    copy = false,
1807    [Debug, Clone, PartialEq]
1808);
1809impl_vec_debug!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1810impl_vec_partialeq!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1811impl_vec_clone!(
1812    ComponentFieldNamedValue,
1813    ComponentFieldNamedValueVec,
1814    ComponentFieldNamedValueVecDestructor
1815);
1816
1817impl ComponentFieldNamedValueVec {
1818    /// Look up a field by name, return a reference to its value.
1819    #[must_use] pub fn get_field(&self, name: &str) -> Option<&ComponentFieldValue> {
1820        self.as_ref().iter().find_map(|v| {
1821            if v.name.as_str() == name {
1822                Some(&v.value)
1823            } else {
1824                None
1825            }
1826        })
1827    }
1828
1829    /// Convenience: get a field as `&str` if it is `ComponentFieldValue::String`.
1830    #[must_use] pub fn get_string(&self, name: &str) -> Option<&AzString> {
1831        match self.get_field(name) {
1832            Some(ComponentFieldValue::String(s)) => Some(s),
1833            _ => None,
1834        }
1835    }
1836}
1837
1838impl_vec!(
1839    ComponentFieldValue,
1840    ComponentFieldValueVec,
1841    ComponentFieldValueVecDestructor,
1842    ComponentFieldValueVecDestructorType,
1843    ComponentFieldValueVecSlice,
1844    OptionComponentFieldValue
1845);
1846impl_option!(
1847    ComponentFieldValue,
1848    OptionComponentFieldValue,
1849    copy = false,
1850    [Debug, Clone, PartialEq]
1851);
1852impl_vec_debug!(ComponentFieldValue, ComponentFieldValueVec);
1853impl_vec_partialeq!(ComponentFieldValue, ComponentFieldValueVec);
1854impl_vec_clone!(
1855    ComponentFieldValue,
1856    ComponentFieldValueVec,
1857    ComponentFieldValueVecDestructor
1858);
1859
1860/// A field in the component's internal data model.
1861#[derive(Debug, Clone, PartialEq)]
1862#[repr(C)]
1863pub struct ComponentDataField {
1864    /// Field name, e.g. "counter", "text", "number"
1865    pub name: AzString,
1866    /// Rich type descriptor for this field
1867    pub field_type: ComponentFieldType,
1868    /// Typed default value, or None if the field is required
1869    pub default_value: OptionComponentDefaultValue,
1870    /// Whether this field is required (must be provided by the parent)
1871    pub required: bool,
1872    /// Human-readable description
1873    pub description: AzString,
1874}
1875
1876impl_vec!(
1877    ComponentDataField,
1878    ComponentDataFieldVec,
1879    ComponentDataFieldVecDestructor,
1880    ComponentDataFieldVecDestructorType,
1881    ComponentDataFieldVecSlice,
1882    OptionComponentDataField
1883);
1884impl_option!(
1885    ComponentDataField,
1886    OptionComponentDataField,
1887    copy = false,
1888    [Debug, Clone, PartialEq]
1889);
1890impl_vec_debug!(ComponentDataField, ComponentDataFieldVec);
1891impl_vec_partialeq!(ComponentDataField, ComponentDataFieldVec);
1892impl_vec_clone!(
1893    ComponentDataField,
1894    ComponentDataFieldVec,
1895    ComponentDataFieldVecDestructor
1896);
1897
1898/// A named data model (struct definition) for code generation.
1899///
1900/// Stored in `ComponentLibrary::data_models`. Components reference these
1901/// by name in `ComponentDataField::field_type`, enabling nested/structured
1902/// data models. For example, a `UserCard` component might have a field
1903/// `user: UserProfile` where `UserProfile` is a `ComponentDataModel`.
1904#[derive(Debug, Clone)]
1905#[repr(C)]
1906pub struct ComponentDataModel {
1907    /// Type name, e.g. "`UserProfile`", "`TodoItem`"
1908    pub name: AzString,
1909    /// Human-readable description
1910    pub description: AzString,
1911    /// Fields in this struct
1912    pub fields: ComponentDataFieldVec,
1913}
1914
1915impl ComponentDataModel {
1916    /// Look up a field by name.
1917    #[must_use] pub fn get_field(&self, name: &str) -> Option<&ComponentDataField> {
1918        self.fields
1919            .as_ref()
1920            .iter()
1921            .find(|f| f.name.as_str() == name)
1922    }
1923
1924    /// Look up a field's default value as a string, if it exists and is a String variant.
1925    #[must_use] pub fn get_default_string(&self, name: &str) -> Option<&AzString> {
1926        self.get_field(name).and_then(|f| match &f.default_value {
1927            OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s),
1928            _ => None,
1929        })
1930    }
1931
1932    /// Clone this data model, overriding the default value for a field by name.
1933    /// If the field is not found, the data model is returned unchanged.
1934    #[must_use] pub fn with_default(mut self, name: &str, value: ComponentDefaultValue) -> Self {
1935        let mut fields_vec = core::mem::replace(
1936            &mut self.fields,
1937            ComponentDataFieldVec::from_const_slice(&[]),
1938        )
1939        .into_library_owned_vec();
1940        for f in &mut fields_vec {
1941            if f.name.as_str() == name {
1942                f.default_value = OptionComponentDefaultValue::Some(value);
1943                break;
1944            }
1945        }
1946        self.fields = ComponentDataFieldVec::from_vec(fields_vec);
1947        self
1948    }
1949}
1950
1951impl_vec!(
1952    ComponentDataModel,
1953    ComponentDataModelVec,
1954    ComponentDataModelVecDestructor,
1955    ComponentDataModelVecDestructorType,
1956    ComponentDataModelVecSlice,
1957    OptionComponentDataModel
1958);
1959impl_option!(
1960    ComponentDataModel,
1961    OptionComponentDataModel,
1962    copy = false,
1963    [Debug, Clone]
1964);
1965impl_vec_debug!(ComponentDataModel, ComponentDataModelVec);
1966impl_vec_clone!(
1967    ComponentDataModel,
1968    ComponentDataModelVec,
1969    ComponentDataModelVecDestructor
1970);
1971impl_vec_mut!(ComponentDataModel, ComponentDataModelVec);
1972
1973// ============================================================================
1974// Serde support for ComponentDataModel (feature-gated)
1975// ============================================================================
1976
1977#[cfg(feature = "serde-json")]
1978mod serde_impl {
1979    use super::*;
1980    use serde::ser::SerializeStruct;
1981    use serde::{Deserialize, Deserializer, Serialize, Serializer};
1982
1983    // --- AzString helpers ---
1984
1985    fn ser_azstring<S: Serializer>(s: &AzString, serializer: S) -> Result<S::Ok, S::Error> {
1986        serializer.serialize_str(s.as_str())
1987    }
1988
1989    fn de_azstring<'de, D: Deserializer<'de>>(deserializer: D) -> Result<AzString, D::Error> {
1990        let s = alloc::string::String::deserialize(deserializer)?;
1991        Ok(AzString::from(s.as_str()))
1992    }
1993
1994    // --- ComponentFieldType ---
1995
1996    impl Serialize for ComponentFieldType {
1997        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1998            serializer.serialize_str(&field_type_to_string(self))
1999        }
2000    }
2001
2002    impl<'de> Deserialize<'de> for ComponentFieldType {
2003        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2004            let s = alloc::string::String::deserialize(deserializer)?;
2005            Ok(string_to_field_type(&s))
2006        }
2007    }
2008
2009    fn field_type_to_string(ft: &ComponentFieldType) -> alloc::string::String {
2010        match ft {
2011            ComponentFieldType::String => "String".into(),
2012            ComponentFieldType::Bool => "bool".into(),
2013            ComponentFieldType::I32 => "i32".into(),
2014            ComponentFieldType::I64 => "i64".into(),
2015            ComponentFieldType::U32 => "u32".into(),
2016            ComponentFieldType::U64 => "u64".into(),
2017            ComponentFieldType::Usize => "usize".into(),
2018            ComponentFieldType::F32 => "f32".into(),
2019            ComponentFieldType::F64 => "f64".into(),
2020            ComponentFieldType::ColorU => "ColorU".into(),
2021            ComponentFieldType::CssProperty => "CssProperty".into(),
2022            ComponentFieldType::ImageRef => "ImageRef".into(),
2023            ComponentFieldType::FontRef => "FontRef".into(),
2024            ComponentFieldType::StyledDom => "Dom".into(),
2025            ComponentFieldType::Callback(sig) => {
2026                alloc::format!("Callback({})", sig.return_type.as_str())
2027            }
2028            ComponentFieldType::RefAny(hint) => alloc::format!("RefAny({})", hint.as_str()),
2029            ComponentFieldType::OptionType(inner) => {
2030                alloc::format!("Option<{}>", field_type_to_string(inner.as_ref()))
2031            }
2032            ComponentFieldType::VecType(inner) => {
2033                alloc::format!("Vec<{}>", field_type_to_string(inner.as_ref()))
2034            }
2035            ComponentFieldType::StructRef(name) => alloc::format!("struct:{}", name.as_str()),
2036            ComponentFieldType::EnumRef(name) => alloc::format!("enum:{}", name.as_str()),
2037        }
2038    }
2039
2040    fn string_to_field_type(s: &str) -> ComponentFieldType {
2041        match s {
2042            "String" | "string" => ComponentFieldType::String,
2043            "bool" | "Bool" => ComponentFieldType::Bool,
2044            "i32" | "I32" => ComponentFieldType::I32,
2045            "i64" | "I64" => ComponentFieldType::I64,
2046            "u32" | "U32" => ComponentFieldType::U32,
2047            "u64" | "U64" => ComponentFieldType::U64,
2048            "usize" | "Usize" => ComponentFieldType::Usize,
2049            "f32" | "F32" => ComponentFieldType::F32,
2050            "f64" | "F64" => ComponentFieldType::F64,
2051            "ColorU" | "Color" | "color" => ComponentFieldType::ColorU,
2052            "CssProperty" => ComponentFieldType::CssProperty,
2053            "ImageRef" | "Image" => ComponentFieldType::ImageRef,
2054            "FontRef" | "Font" => ComponentFieldType::FontRef,
2055            "Dom" | "StyledDom" | "Children" => ComponentFieldType::StyledDom,
2056            other => {
2057                if let Some(inner) = other
2058                    .strip_prefix("Option<")
2059                    .and_then(|s| s.strip_suffix('>'))
2060                {
2061                    ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
2062                        string_to_field_type(inner),
2063                    ))
2064                } else if let Some(inner) =
2065                    other.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>'))
2066                {
2067                    ComponentFieldType::VecType(ComponentFieldTypeBox::new(string_to_field_type(
2068                        inner,
2069                    )))
2070                } else if let Some(name) = other.strip_prefix("struct:") {
2071                    ComponentFieldType::StructRef(AzString::from(name))
2072                } else if let Some(name) = other.strip_prefix("enum:") {
2073                    ComponentFieldType::EnumRef(AzString::from(name))
2074                } else if other.starts_with("Callback") {
2075                    let ret = other
2076                        .strip_prefix("Callback(")
2077                        .and_then(|s| s.strip_suffix(')'))
2078                        .unwrap_or("()");
2079                    ComponentFieldType::Callback(ComponentCallbackSignature {
2080                        return_type: AzString::from(ret),
2081                        args: ComponentCallbackArgVec::from_const_slice(&[]),
2082                    })
2083                } else if other.starts_with("RefAny") {
2084                    let hint = other
2085                        .strip_prefix("RefAny(")
2086                        .and_then(|s| s.strip_suffix(')'))
2087                        .unwrap_or("");
2088                    ComponentFieldType::RefAny(AzString::from(hint))
2089                } else {
2090                    ComponentFieldType::String // fallback
2091                }
2092            }
2093        }
2094    }
2095
2096    // --- ComponentDefaultValue ---
2097
2098    impl Serialize for ComponentDefaultValue {
2099        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2100            use serde::ser::SerializeMap;
2101            match self {
2102                ComponentDefaultValue::None => serializer.serialize_none(),
2103                ComponentDefaultValue::String(s) => serializer.serialize_str(s.as_str()),
2104                ComponentDefaultValue::Bool(b) => serializer.serialize_bool(*b),
2105                ComponentDefaultValue::I32(v) => serializer.serialize_i32(*v),
2106                ComponentDefaultValue::I64(v) => serializer.serialize_i64(*v),
2107                ComponentDefaultValue::U32(v) => serializer.serialize_u32(*v),
2108                ComponentDefaultValue::U64(v) => serializer.serialize_u64(*v),
2109                ComponentDefaultValue::Usize(v) => serializer.serialize_u64(*v as u64),
2110                ComponentDefaultValue::F32(v) => serializer.serialize_f32(*v),
2111                ComponentDefaultValue::F64(v) => serializer.serialize_f64(*v),
2112                ComponentDefaultValue::ColorU(c) => serializer.serialize_str(&alloc::format!(
2113                    "#{:02x}{:02x}{:02x}{:02x}",
2114                    c.r,
2115                    c.g,
2116                    c.b,
2117                    c.a
2118                )),
2119                ComponentDefaultValue::ComponentInstance(ci) => {
2120                    let mut map = serializer.serialize_map(Some(2))?;
2121                    map.serialize_entry("library", ci.library.as_str())?;
2122                    map.serialize_entry("component", ci.component.as_str())?;
2123                    map.end()
2124                }
2125                ComponentDefaultValue::CallbackFnPointer(name) => {
2126                    serializer.serialize_str(name.as_str())
2127                }
2128                ComponentDefaultValue::Json(json_str) => {
2129                    // Serialize raw JSON string as-is by parsing and re-emitting
2130                    match serde_json::from_str::<serde_json::Value>(json_str.as_str()) {
2131                        Ok(v) => v.serialize(serializer),
2132                        Err(_) => serializer.serialize_str(json_str.as_str()),
2133                    }
2134                }
2135            }
2136        }
2137    }
2138
2139    impl<'de> Deserialize<'de> for ComponentDefaultValue {
2140        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2141            let val = serde_json::Value::deserialize(deserializer)?;
2142            Ok(match val {
2143                serde_json::Value::Null => ComponentDefaultValue::None,
2144                serde_json::Value::Bool(b) => ComponentDefaultValue::Bool(b),
2145                serde_json::Value::Number(n) => {
2146                    if let Some(i) = n.as_i64() {
2147                        if let Ok(v) = i32::try_from(i) {
2148                            ComponentDefaultValue::I32(v)
2149                        } else {
2150                            ComponentDefaultValue::I64(i)
2151                        }
2152                    } else if let Some(f) = n.as_f64() {
2153                        ComponentDefaultValue::F64(f)
2154                    } else {
2155                        ComponentDefaultValue::None
2156                    }
2157                }
2158                serde_json::Value::String(s) => {
2159                    ComponentDefaultValue::String(AzString::from(s.as_str()))
2160                }
2161                _ => ComponentDefaultValue::None,
2162            })
2163        }
2164    }
2165
2166    // --- OptionComponentDefaultValue ---
2167
2168    impl Serialize for OptionComponentDefaultValue {
2169        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2170            match self {
2171                OptionComponentDefaultValue::Some(v) => v.serialize(serializer),
2172                OptionComponentDefaultValue::None => serializer.serialize_none(),
2173            }
2174        }
2175    }
2176
2177    impl<'de> Deserialize<'de> for OptionComponentDefaultValue {
2178        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2179            let val = Option::<ComponentDefaultValue>::deserialize(deserializer)?;
2180            Ok(match val {
2181                Some(v) => OptionComponentDefaultValue::Some(v),
2182                None => OptionComponentDefaultValue::None,
2183            })
2184        }
2185    }
2186
2187    // --- ComponentDataField ---
2188
2189    impl Serialize for ComponentDataField {
2190        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2191            let mut s = serializer.serialize_struct("ComponentDataField", 5)?;
2192            s.serialize_field("name", self.name.as_str())?;
2193            s.serialize_field("type", &self.field_type)?;
2194            s.serialize_field("default", &self.default_value)?;
2195            s.serialize_field("required", &self.required)?;
2196            s.serialize_field("description", self.description.as_str())?;
2197            s.end()
2198        }
2199    }
2200
2201    impl<'de> Deserialize<'de> for ComponentDataField {
2202        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2203            #[derive(Deserialize)]
2204            struct Helper {
2205                name: alloc::string::String,
2206                #[serde(rename = "type", default = "default_type")]
2207                field_type: ComponentFieldType,
2208                #[serde(default)]
2209                default: OptionComponentDefaultValue,
2210                #[serde(default)]
2211                required: bool,
2212                #[serde(default)]
2213                description: alloc::string::String,
2214            }
2215            fn default_type() -> ComponentFieldType {
2216                ComponentFieldType::String
2217            }
2218
2219            let h = Helper::deserialize(deserializer)?;
2220            Ok(ComponentDataField {
2221                name: AzString::from(h.name.as_str()),
2222                field_type: h.field_type,
2223                default_value: h.default,
2224                required: h.required,
2225                description: AzString::from(h.description.as_str()),
2226            })
2227        }
2228    }
2229
2230    // --- ComponentDataModel ---
2231
2232    impl Serialize for ComponentDataModel {
2233        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2234            let mut s = serializer.serialize_struct("ComponentDataModel", 3)?;
2235            s.serialize_field("name", self.name.as_str())?;
2236            s.serialize_field("description", self.description.as_str())?;
2237            let fields: alloc::vec::Vec<&ComponentDataField> =
2238                self.fields.as_ref().iter().collect();
2239            s.serialize_field("fields", &fields)?;
2240            s.end()
2241        }
2242    }
2243
2244    impl<'de> Deserialize<'de> for ComponentDataModel {
2245        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2246            #[derive(Deserialize)]
2247            struct Helper {
2248                #[serde(default)]
2249                name: alloc::string::String,
2250                #[serde(default)]
2251                description: alloc::string::String,
2252                #[serde(default)]
2253                fields: alloc::vec::Vec<ComponentDataField>,
2254            }
2255
2256            let h = Helper::deserialize(deserializer)?;
2257            Ok(ComponentDataModel {
2258                name: AzString::from(h.name.as_str()),
2259                description: AzString::from(h.description.as_str()),
2260                fields: ComponentDataFieldVec::from_vec(h.fields),
2261            })
2262        }
2263    }
2264}
2265
2266// Re-export serde impls so they're visible when the feature is enabled
2267#[cfg(feature = "serde-json")]
2268pub use serde_impl::*;
2269
2270#[cfg(feature = "serde-json")]
2271impl ComponentDataModel {
2272    /// Serialize this data model to a JSON string.
2273    pub fn to_json(&self) -> Result<alloc::string::String, alloc::string::String> {
2274        serde_json::to_string_pretty(self).map_err(|e| alloc::format!("{}", e))
2275    }
2276
2277    /// Deserialize a data model from a JSON string.
2278    pub fn from_json(json: &str) -> Result<Self, alloc::string::String> {
2279        serde_json::from_str(json).map_err(|e| alloc::format!("{}", e))
2280    }
2281}
2282
2283/// Source of a component definition — determines whether it can be exported
2284#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2285#[repr(C)]
2286#[derive(Default)]
2287pub enum ComponentSource {
2288    /// Built into the DLL (HTML elements). Never exported.
2289    Builtin,
2290    /// Compiled Rust widget (Button, `TextInput`, etc.). Never exported.
2291    Compiled,
2292    /// Defined via JSON/XML at runtime. Can be exported.
2293    #[default]
2294    UserDefined,
2295}
2296
2297
2298impl ComponentSource {
2299    #[must_use] pub fn create() -> Self {
2300        Self::default()
2301    }
2302}
2303
2304/// The target language for code compilation
2305// Threaded by reference through the codegen call graph; kept non-Copy so
2306// deriving Copy doesn't force trivially_copy_pass_by_ref churn across the many
2307// &CompileTarget codegen callers for a perf-neutral change.
2308#[allow(missing_copy_implementations)]
2309#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2310#[repr(C)]
2311pub enum CompileTarget {
2312    Rust,
2313    C,
2314    Cpp,
2315    Python,
2316}
2317
2318impl_result!(
2319    StyledDom,
2320    RenderDomError,
2321    ResultStyledDomRenderDomError,
2322    copy = false,
2323    [Debug, Clone, PartialEq]
2324);
2325
2326impl_result!(
2327    AzString,
2328    CompileError,
2329    ResultStringCompileError,
2330    copy = false,
2331    [Debug, Clone, PartialEq]
2332);
2333
2334/// Render function type: takes component definition + data model (with current values
2335/// in `default_value` fields) + component map for recursive sub-component instantiation,
2336/// returns `StyledDom`.
2337///
2338/// The `data` parameter is typically `def.data_model` cloned and with caller-provided
2339/// values substituted into the `default_value` fields.
2340pub type ComponentRenderFn =
2341    fn(&ComponentDef, &ComponentDataModel, &ComponentMap) -> ResultStyledDomRenderDomError;
2342
2343/// Compile function type: takes component definition + target language + data model, returns source code.
2344pub type ComponentCompileFn = fn(
2345    &ComponentDef,
2346    &CompileTarget,
2347    &ComponentDataModel,
2348    indent: usize,
2349) -> ResultStringCompileError;
2350
2351/// Raw function pointer type that returns a single `ComponentDef` when called.
2352/// Used as the `cb` field in `RegisterComponentFn`.
2353pub type RegisterComponentFnType = extern "C" fn() -> ComponentDef;
2354
2355/// Callback struct for registering individual components at startup.
2356///
2357/// In C: pass a bare `extern "C" fn() -> ComponentDef` function pointer —
2358/// it converts automatically via `From<RegisterComponentFnType>`.
2359///
2360/// In Python: construct this struct with `cb` set to a trampoline and
2361/// `ctx` set to `Some(RefAny(...))` wrapping the Python callable.
2362#[repr(C)]
2363pub struct RegisterComponentFn {
2364    pub cb: RegisterComponentFnType,
2365    /// For FFI: stores the foreign callable (e.g., `PyFunction`).
2366    /// Native Rust/C code sets this to None.
2367    pub ctx: crate::refany::OptionRefAny,
2368}
2369
2370impl_callback!(RegisterComponentFn, RegisterComponentFnType);
2371
2372/// Raw function pointer type that returns a complete `ComponentLibrary` when called.
2373/// Used as the `cb` field in `RegisterComponentLibraryFn`.
2374pub type RegisterComponentLibraryFnType = extern "C" fn() -> ComponentLibrary;
2375
2376/// Callback struct for registering entire component libraries at startup.
2377///
2378/// In C: pass a bare `extern "C" fn() -> ComponentLibrary` function pointer —
2379/// it converts automatically via `From<RegisterComponentLibraryFnType>`.
2380///
2381/// In Python: construct this struct with `cb` set to a trampoline and
2382/// `ctx` set to `Some(RefAny(...))` wrapping the Python callable.
2383#[repr(C)]
2384pub struct RegisterComponentLibraryFn {
2385    pub cb: RegisterComponentLibraryFnType,
2386    /// For FFI: stores the foreign callable (e.g., `PyFunction`).
2387    /// Native Rust/C code sets this to None.
2388    pub ctx: crate::refany::OptionRefAny,
2389}
2390
2391impl_callback!(RegisterComponentLibraryFn, RegisterComponentLibraryFnType);
2392
2393/// A component definition — the "class" / "template" of a component.
2394/// Can come from Rust builtins, compiled widgets, JSON, or user creation in debugger.
2395///
2396#[derive(Clone)]
2397#[repr(C)]
2398pub struct ComponentDef {
2399    /// Collection + name, e.g. builtin:div, shadcn:avatar
2400    pub id: ComponentId,
2401    /// Human-readable display name, e.g. "Link" for builtin:a, "Avatar" for shadcn:avatar
2402    pub display_name: AzString,
2403    /// Markdown documentation for the component
2404    pub description: AzString,
2405    /// The component's CSS
2406    pub css: AzString,
2407    /// Where this component was defined (determines exportability)
2408    pub source: ComponentSource,
2409    /// Unified data model: all value fields, callback slots, and child slots
2410    /// in a single named struct. Code gen uses `data_model.name` as the
2411    /// input struct type name (e.g. "`ButtonData`").
2412    /// The `default_value` on each field doubles as the "current value" for
2413    /// preview rendering — callers override defaults before calling `render_fn`.
2414    pub data_model: ComponentDataModel,
2415    /// Render to live DOM
2416    pub render_fn: ComponentRenderFn,
2417    /// Compile to source code in target language
2418    pub compile_fn: ComponentCompileFn,
2419    /// Source code for `render_fn` (user-defined components only)
2420    pub render_fn_source: OptionString,
2421    /// Source code for `compile_fn` (user-defined components only)
2422    pub compile_fn_source: OptionString,
2423}
2424
2425impl fmt::Debug for ComponentDef {
2426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2427        f.debug_struct("ComponentDef")
2428            .field("id", &self.id)
2429            .field("display_name", &self.display_name)
2430            .field("source", &self.source)
2431            .field("data_model", &self.data_model.name)
2432            .finish_non_exhaustive()
2433    }
2434}
2435
2436impl_vec!(
2437    ComponentDef,
2438    ComponentDefVec,
2439    ComponentDefVecDestructor,
2440    ComponentDefVecDestructorType,
2441    ComponentDefVecSlice,
2442    OptionComponentDef
2443);
2444impl_option!(ComponentDef, OptionComponentDef, copy = false, [Clone]);
2445impl_vec_debug!(ComponentDef, ComponentDefVec);
2446impl_vec_clone!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor);
2447impl_vec_mut!(ComponentDef, ComponentDefVec);
2448
2449/// A named collection of component definitions
2450#[derive(Debug, Clone)]
2451#[repr(C)]
2452pub struct ComponentLibrary {
2453    /// Library identifier, e.g. "builtin", "shadcn", "myproject"
2454    pub name: AzString,
2455    /// Version string
2456    pub version: AzString,
2457    /// Human-readable description
2458    pub description: AzString,
2459    /// The components in this library
2460    pub components: ComponentDefVec,
2461    /// Whether this library can be exported (false for builtin/compiled)
2462    pub exportable: bool,
2463    /// Whether this library can be modified by the user (add/remove/edit components).
2464    /// False for builtin and compiled libraries. True for user-created libraries.
2465    pub modifiable: bool,
2466    /// Named data model types defined by this library.
2467    /// Components reference these by name in their `field_type`.
2468    pub data_models: ComponentDataModelVec,
2469    /// Named enum types defined by this library.
2470    /// Components reference these via `ComponentFieldType::EnumRef(name)`.
2471    pub enum_models: ComponentEnumModelVec,
2472}
2473
2474impl_vec!(
2475    ComponentLibrary,
2476    ComponentLibraryVec,
2477    ComponentLibraryVecDestructor,
2478    ComponentLibraryVecDestructorType,
2479    ComponentLibraryVecSlice,
2480    OptionComponentLibrary
2481);
2482impl_option!(
2483    ComponentLibrary,
2484    OptionComponentLibrary,
2485    copy = false,
2486    [Debug, Clone]
2487);
2488impl_vec_debug!(ComponentLibrary, ComponentLibraryVec);
2489impl_vec_clone!(
2490    ComponentLibrary,
2491    ComponentLibraryVec,
2492    ComponentLibraryVecDestructor
2493);
2494impl_vec_mut!(ComponentLibrary, ComponentLibraryVec);
2495
2496/// The component map — holds libraries with namespaced components.
2497#[derive(Debug, Clone)]
2498#[repr(C)]
2499pub struct ComponentMap {
2500    /// Libraries indexed by name. "builtin" is always present.
2501    pub libraries: ComponentLibraryVec,
2502}
2503
2504impl ComponentMap {
2505    /// Qualified lookup: "shadcn:avatar" -> finds library "shadcn", component "avatar"
2506    #[must_use] pub fn get(&self, collection: &str, name: &str) -> Option<&ComponentDef> {
2507        self.libraries
2508            .iter()
2509            .find(|lib| lib.name.as_str() == collection)
2510            .and_then(|lib| lib.components.iter().find(|c| c.id.name.as_str() == name))
2511    }
2512
2513    /// Unqualified lookup: "div" -> searches ONLY the "builtin" library.
2514    #[must_use] pub fn get_unqualified(&self, name: &str) -> Option<&ComponentDef> {
2515        self.get("builtin", name)
2516    }
2517
2518    /// Parse a "collection:name" string into a lookup
2519    #[must_use] pub fn get_by_qualified_name(&self, qualified: &str) -> Option<&ComponentDef> {
2520        if let Some((collection, name)) = qualified.split_once(':') {
2521            self.get(collection, name)
2522        } else {
2523            self.get_unqualified(qualified)
2524        }
2525    }
2526
2527    /// Get all libraries that can be exported (user-defined only)
2528    #[must_use] pub fn get_exportable_libraries(&self) -> Vec<&ComponentLibrary> {
2529        self.libraries.iter().filter(|lib| lib.exportable).collect()
2530    }
2531
2532    /// Get all component definitions across all libraries
2533    #[must_use] pub fn all_components(&self) -> Vec<&ComponentDef> {
2534        self.libraries
2535            .iter()
2536            .flat_map(|lib| lib.components.iter())
2537            .collect()
2538    }
2539}
2540
2541// ============================================================================
2542// Builtin component bridge — wraps existing render/compile into ComponentDef
2543// ============================================================================
2544
2545/// Single source of truth mapping HTML/SVG tag names to node variants.
2546///
2547/// Each `"tag" => Variant` entry expands to **both** a `NodeType::Variant` arm in
2548/// [`tag_to_node_type`] and a `NodeTypeTag::Variant` arm in [`tag_to_node_type_tag`],
2549/// so the two lookups can never drift apart. Tags whose two enums diverge —
2550/// `img`, `image`, `icon` — are handled as explicit special cases inside each
2551/// generated function and are intentionally absent from this table.
2552macro_rules! html_tag_node_types {
2553    ($($tag:literal => $variant:ident),* $(,)?) => {
2554        /// Map a builtin tag name to its corresponding `NodeType`.
2555        /// Falls back to `NodeType::Div` for unknown tags.
2556        #[must_use] pub fn tag_to_node_type(tag: &str) -> NodeType {
2557            match tag {
2558                // `<img>` becomes a replaced `NodeType::Image`. The `src` attribute is not
2559                // available here, so a placeholder `NullImage` (0x0, empty tag) is created;
2560                // `xml_node_to_dom_fast` overrides it with a `NullImage` whose `tag` carries
2561                // the `src` bytes so a renderer (e.g. printpdf) can resolve the actual image.
2562                "img" => NodeType::Image(azul_css::css::BoxOrStatic::heap(
2563                    crate::resources::ImageRef::null_image(
2564                        0,
2565                        0,
2566                        crate::resources::RawImageFormat::RGBA8,
2567                        alloc::vec::Vec::new(),
2568                    ),
2569                )),
2570                $($tag => NodeType::$variant,)*
2571                _ => NodeType::Div,
2572            }
2573        }
2574
2575        /// Map a tag name to its CSS `NodeTypeTag` for CSS matching in the compile pipeline.
2576        /// Falls back to `NodeTypeTag::Div` for unknown tags.
2577        fn tag_to_node_type_tag(tag: &str) -> NodeTypeTag {
2578            match tag {
2579                // `img`/`image`/`icon` have no 1:1 `NodeType` equivalent (see
2580                // `tag_to_node_type`), so they map to dedicated `NodeTypeTag` variants.
2581                "img" | "image" => NodeTypeTag::Img,
2582                "icon" => NodeTypeTag::Icon,
2583                $($tag => NodeTypeTag::$variant,)*
2584                _ => NodeTypeTag::Div,
2585            }
2586        }
2587    };
2588}
2589
2590html_tag_node_types! {
2591    // Document structure
2592    "html" => Html,
2593    "head" => Head,
2594    "title" => Title,
2595    "body" => Body,
2596    // Block-level
2597    "div" => Div,
2598    "header" => Header,
2599    "footer" => Footer,
2600    "section" => Section,
2601    "article" => Article,
2602    "aside" => Aside,
2603    "nav" => Nav,
2604    "main" => Main,
2605    "figure" => Figure,
2606    "figcaption" => FigCaption,
2607    "address" => Address,
2608    "details" => Details,
2609    "summary" => Summary,
2610    "dialog" => Dialog,
2611    // Headings
2612    "h1" => H1,
2613    "h2" => H2,
2614    "h3" => H3,
2615    "h4" => H4,
2616    "h5" => H5,
2617    "h6" => H6,
2618    // Text content
2619    "p" => P,
2620    "span" => Span,
2621    "pre" => Pre,
2622    "code" => Code,
2623    "blockquote" => BlockQuote,
2624    "br" => Br,
2625    "hr" => Hr,
2626    // Lists
2627    "ul" => Ul,
2628    "ol" => Ol,
2629    "li" => Li,
2630    "dl" => Dl,
2631    "dt" => Dt,
2632    "dd" => Dd,
2633    "menu" => Menu,
2634    "menuitem" => MenuItem,
2635    "dir" => Dir,
2636    // Tables
2637    "table" => Table,
2638    "caption" => Caption,
2639    "thead" => THead,
2640    "tbody" => TBody,
2641    "tfoot" => TFoot,
2642    "tr" => Tr,
2643    "th" => Th,
2644    "td" => Td,
2645    "colgroup" => ColGroup,
2646    "col" => Col,
2647    // Forms
2648    "form" => Form,
2649    "fieldset" => FieldSet,
2650    "legend" => Legend,
2651    "label" => Label,
2652    "input" => Input,
2653    "button" => Button,
2654    "select" => Select,
2655    "optgroup" => OptGroup,
2656    "option" => SelectOption,
2657    "textarea" => TextArea,
2658    "output" => Output,
2659    "progress" => Progress,
2660    "meter" => Meter,
2661    "datalist" => DataList,
2662    // Inline
2663    "a" => A,
2664    "strong" => Strong,
2665    "em" => Em,
2666    "b" => B,
2667    "i" => I,
2668    "u" => U,
2669    "s" => S,
2670    "small" => Small,
2671    "mark" => Mark,
2672    "del" => Del,
2673    "ins" => Ins,
2674    "samp" => Samp,
2675    "kbd" => Kbd,
2676    "var" => Var,
2677    "cite" => Cite,
2678    "dfn" => Dfn,
2679    "abbr" => Abbr,
2680    "acronym" => Acronym,
2681    "q" => Q,
2682    "time" => Time,
2683    "sub" => Sub,
2684    "sup" => Sup,
2685    "big" => Big,
2686    "bdo" => Bdo,
2687    "bdi" => Bdi,
2688    "wbr" => Wbr,
2689    "ruby" => Ruby,
2690    "rt" => Rt,
2691    "rtc" => Rtc,
2692    "rp" => Rp,
2693    "data" => Data,
2694    // Embedded content (`img` is a special case in the generated fns)
2695    "canvas" => Canvas,
2696    "object" => Object,
2697    "param" => Param,
2698    "embed" => Embed,
2699    "audio" => Audio,
2700    "video" => Video,
2701    "source" => Source,
2702    "track" => Track,
2703    "map" => Map,
2704    "area" => Area,
2705    // SVG elements
2706    "svg" => Svg,
2707    "g" => SvgG,
2708    "defs" => SvgDefs,
2709    "symbol" => SvgSymbol,
2710    "use" => SvgUse,
2711    "switch" => SvgSwitch,
2712    "path" => SvgPath,
2713    "circle" => SvgCircle,
2714    "rect" => SvgRect,
2715    "ellipse" => SvgEllipse,
2716    "line" => SvgLine,
2717    "polygon" => SvgPolygon,
2718    "polyline" => SvgPolyline,
2719    "tspan" => SvgTspan,
2720    "textpath" => SvgTextPath,
2721    "lineargradient" => SvgLinearGradient,
2722    "radialgradient" => SvgRadialGradient,
2723    "stop" => SvgStop,
2724    "pattern" => SvgPattern,
2725    "clippath" => SvgClipPathElement,
2726    "mask" => SvgMask,
2727    "filter" => SvgFilter,
2728    "feblend" => SvgFeBlend,
2729    "fecolormatrix" => SvgFeColorMatrix,
2730    "fecomponenttransfer" => SvgFeComponentTransfer,
2731    "fecomposite" => SvgFeComposite,
2732    "feconvolvematrix" => SvgFeConvolveMatrix,
2733    "fediffuselighting" => SvgFeDiffuseLighting,
2734    "fedisplacementmap" => SvgFeDisplacementMap,
2735    "fedistantlight" => SvgFeDistantLight,
2736    "fedropshadow" => SvgFeDropShadow,
2737    "feflood" => SvgFeFlood,
2738    "fefuncr" => SvgFeFuncR,
2739    "fefuncg" => SvgFeFuncG,
2740    "fefuncb" => SvgFeFuncB,
2741    "fefunca" => SvgFeFuncA,
2742    "fegaussianblur" => SvgFeGaussianBlur,
2743    "feimage" => SvgFeImage,
2744    "femerge" => SvgFeMerge,
2745    "femergenode" => SvgFeMergeNode,
2746    "femorphology" => SvgFeMorphology,
2747    "feoffset" => SvgFeOffset,
2748    "fepointlight" => SvgFePointLight,
2749    "fespecularlighting" => SvgFeSpecularLighting,
2750    "fespotlight" => SvgFeSpotLight,
2751    "fetile" => SvgFeTile,
2752    "feturbulence" => SvgFeTurbulence,
2753    "foreignobject" => SvgForeignObject,
2754    "desc" => SvgDesc,
2755    "view" => SvgView,
2756    "animate" => SvgAnimate,
2757    "animatemotion" => SvgAnimateMotion,
2758    "animatetransform" => SvgAnimateTransform,
2759    "set" => SvgSet,
2760    "mpath" => SvgMpath,
2761    // Metadata
2762    "meta" => Meta,
2763    "link" => Link,
2764    "script" => Script,
2765    "style" => Style,
2766    "base" => Base,
2767}
2768
2769/// Default render function for builtin HTML elements.
2770/// Delegates to creating a DOM node of the appropriate `NodeType`.
2771fn builtin_render_fn(
2772    def: &ComponentDef,
2773    data: &ComponentDataModel,
2774    _component_map: &ComponentMap,
2775) -> ResultStyledDomRenderDomError {
2776    let node_type = tag_to_node_type(def.id.name.as_str());
2777    let mut dom = Dom::create_node(node_type);
2778    if let Some(text_str) = data.get_default_string("text") {
2779        let prepared = prepare_string(text_str);
2780        if !prepared.is_empty() {
2781            dom = dom.with_children(alloc::vec![Dom::create_text(prepared)].into());
2782        }
2783    }
2784    let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut dom, Css::empty()));
2785    r.into()
2786}
2787
2788/// Default compile function for builtin HTML elements.
2789/// Generates `Dom::create_node(NodeType::Div)` style code for the target language.
2790fn builtin_compile_fn(
2791    def: &ComponentDef,
2792    target: &CompileTarget,
2793    data: &ComponentDataModel,
2794    indent: usize,
2795) -> ResultStringCompileError {
2796    let node_type = tag_to_node_type(def.id.name.as_str());
2797    let type_name = format!("{node_type:?}"); // "Div", "Body", "P", etc.
2798    let text = data.get_default_string("text");
2799
2800    let r: Result<AzString, CompileError> = match target {
2801        CompileTarget::Rust => {
2802            text.map_or_else(|| Ok(format!("Dom::create_node(NodeType::{type_name})").into()), |text_str| Ok(format!(
2803                    "Dom::create_node(NodeType::{}).with_children(vec![Dom::create_text(\"{}\")])",
2804                    type_name,
2805                    text_str.as_str().replace('\\', "\\\\").replace('"', "\\\"")
2806                ).into()))
2807        }
2808        CompileTarget::C => {
2809            text.map_or_else(|| Ok(format!("AzDom_create{type_name}()").into()), |text_str| Ok(format!(
2810                    "AzDom_createText(AZ_STR(\"{}\"))",
2811                    text_str
2812                        .as_str()
2813                        .replace('\\', "\\\\")
2814                        .replace('"', "\\\"")
2815                )
2816                .into()))
2817        }
2818        CompileTarget::Cpp => Ok(format!("Dom::create_{}()", type_name.to_lowercase()).into()),
2819        CompileTarget::Python => Ok(format!("Dom.create_{}()", type_name.to_lowercase()).into()),
2820    };
2821    r.into()
2822}
2823
2824/// Pushes a `<div>` containing `"field_name: value"` text into the children list.
2825fn push_scalar_field(children: &mut Vec<Dom>, field_name: &str, value: &dyn fmt::Display) {
2826    use crate::dom::{Dom, NodeType};
2827    let text = alloc::format!("{field_name}: {value}");
2828    children.push(
2829        Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(text)].into()),
2830    );
2831}
2832
2833/// Default render function for user-defined (JSON-imported) components.
2834///
2835/// Interprets the `ComponentDef` structure generically:
2836/// 1. Creates a wrapper `<div>` with the component's CSS class
2837/// 2. For each data field, renders content based on type:
2838///    - String fields → text node with current value
2839///    - Bool fields → conditional display
2840///    - `StyledDom` fields → embeds the child DOM subtree
2841///    - StructRef/EnumRef → recursively renders sub-components if found in `ComponentMap`
2842///    - Other scalar fields → text display of the value
2843/// 3. Applies the component's scoped CSS
2844#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
2845#[must_use] pub fn user_defined_render_fn(
2846    def: &ComponentDef,
2847    data: &ComponentDataModel,
2848    component_map: &ComponentMap,
2849) -> ResultStyledDomRenderDomError {
2850    use crate::dom::{Dom, NodeType};
2851    use azul_css::css::Css;
2852
2853    let mut children: Vec<Dom> = Vec::new();
2854
2855    for field in data.fields.as_ref() {
2856        let field_name = field.name.as_str();
2857
2858        // Get the current value from default_value
2859        match &field.default_value {
2860            OptionComponentDefaultValue::None => {
2861                // Required field with no value — skip in preview
2862            }
2863            OptionComponentDefaultValue::Some(default_val) => {
2864                match default_val {
2865                    ComponentDefaultValue::String(s) => {
2866                        let text = s.as_str().trim();
2867                        if !text.is_empty() {
2868                            let label_dom = Dom::create_node(NodeType::Div).with_children(
2869                                alloc::vec![Dom::create_text(text.to_string())].into(),
2870                            );
2871                            children.push(label_dom);
2872                        }
2873                    }
2874                    ComponentDefaultValue::Bool(v) => {
2875                        push_scalar_field(&mut children, field_name, v);
2876                    }
2877                    ComponentDefaultValue::I32(v) => {
2878                        push_scalar_field(&mut children, field_name, v);
2879                    }
2880                    ComponentDefaultValue::I64(v) => {
2881                        push_scalar_field(&mut children, field_name, v);
2882                    }
2883                    ComponentDefaultValue::U32(v) => {
2884                        push_scalar_field(&mut children, field_name, v);
2885                    }
2886                    ComponentDefaultValue::U64(v) => {
2887                        push_scalar_field(&mut children, field_name, v);
2888                    }
2889                    ComponentDefaultValue::Usize(v) => {
2890                        push_scalar_field(&mut children, field_name, v);
2891                    }
2892                    ComponentDefaultValue::F32(v) => {
2893                        push_scalar_field(&mut children, field_name, v);
2894                    }
2895                    ComponentDefaultValue::F64(v) => {
2896                        push_scalar_field(&mut children, field_name, v);
2897                    }
2898                    ComponentDefaultValue::ColorU(c) => {
2899                        let text = alloc::format!(
2900                            "{}: #{:02x}{:02x}{:02x}{:02x}",
2901                            field_name,
2902                            c.r,
2903                            c.g,
2904                            c.b,
2905                            c.a
2906                        );
2907                        children.push(
2908                            Dom::create_node(NodeType::Div)
2909                                .with_children(alloc::vec![Dom::create_text(text)].into()),
2910                        );
2911                    }
2912                    ComponentDefaultValue::ComponentInstance(ci) => {
2913                        // Recursively instantiate sub-component from ComponentMap
2914                        if let Some(sub_comp) =
2915                            component_map.get(ci.library.as_str(), ci.component.as_str())
2916                        {
2917                            let sub_data = sub_comp.data_model.clone();
2918                            match (sub_comp.render_fn)(sub_comp, &sub_data, component_map) {
2919                                ResultStyledDomRenderDomError::Ok(_styled_dom) => {
2920                                    // Sub-component rendered successfully — add a placeholder
2921                                    // (StyledDom cannot be directly converted back to Dom)
2922                                    let text = alloc::format!(
2923                                        "[{}:{}]",
2924                                        ci.library.as_str(),
2925                                        ci.component.as_str()
2926                                    );
2927                                    children.push(
2928                                        Dom::create_node(NodeType::Div).with_children(
2929                                            alloc::vec![Dom::create_text(text)].into(),
2930                                        ),
2931                                    );
2932                                }
2933                                ResultStyledDomRenderDomError::Err(_) => {
2934                                    // On error, show a placeholder
2935                                    let text = alloc::format!(
2936                                        "[Error rendering {}:{}]",
2937                                        ci.library.as_str(),
2938                                        ci.component.as_str()
2939                                    );
2940                                    children.push(
2941                                        Dom::create_node(NodeType::Div).with_children(
2942                                            alloc::vec![Dom::create_text(text)].into(),
2943                                        ),
2944                                    );
2945                                }
2946                            }
2947                        } else {
2948                            let text = alloc::format!(
2949                                "[Unknown component {}:{}]",
2950                                ci.library.as_str(),
2951                                ci.component.as_str()
2952                            );
2953                            children.push(
2954                                Dom::create_node(NodeType::Div)
2955                                    .with_children(alloc::vec![Dom::create_text(text)].into()),
2956                            );
2957                        }
2958                    }
2959                    ComponentDefaultValue::CallbackFnPointer(name) => {
2960                        // Callbacks are not rendered, just acknowledged
2961                        let text = alloc::format!("{}: fn({})", field_name, name.as_str());
2962                        children.push(
2963                            Dom::create_node(NodeType::Div)
2964                                .with_children(alloc::vec![Dom::create_text(text)].into()),
2965                        );
2966                    }
2967                    ComponentDefaultValue::Json(json_str) => {
2968                        let text = alloc::format!("{}: {}", field_name, json_str.as_str());
2969                        children.push(
2970                            Dom::create_node(NodeType::Div)
2971                                .with_children(alloc::vec![Dom::create_text(text)].into()),
2972                        );
2973                    }
2974                    ComponentDefaultValue::None => {
2975                        // No default, skip
2976                    }
2977                }
2978            }
2979        }
2980    }
2981
2982    let mut wrapper = Dom::create_node(NodeType::Div);
2983    if !children.is_empty() {
2984        wrapper = wrapper.with_children(children.into());
2985    }
2986
2987    // Apply component CSS
2988    let css = if def.css.as_str().is_empty() {
2989        Css::empty()
2990    } else {
2991        Css::from_string(def.css.clone())
2992    };
2993
2994    let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut wrapper, css));
2995    r.into()
2996}
2997
2998/// Default compile function for user-defined (JSON-imported) components.
2999///
3000/// Generates source code that creates the component's DOM structure for the
3001/// target language. For each data field, emits the appropriate code:
3002/// - String fields → text node creation
3003/// - Scalar fields → formatted display
3004/// - `ComponentInstance` → function call to sub-component's render function
3005/// - `StyledDom` slots → child parameter pass-through
3006#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3007#[must_use] pub fn user_defined_compile_fn(
3008    def: &ComponentDef,
3009    target: &CompileTarget,
3010    data: &ComponentDataModel,
3011    indent: usize,
3012) -> ResultStringCompileError {
3013    let tag = def.id.name.as_str();
3014    let indent_str = " ".repeat(indent * 4);
3015    let inner_indent = " ".repeat((indent + 1) * 4);
3016
3017    let r: Result<AzString, CompileError> = match target {
3018        CompileTarget::Rust => {
3019            let mut lines = Vec::new();
3020            lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3021            lines.push(alloc::format!(
3022                "{indent_str}let mut children: Vec<Dom> = Vec::new();"
3023            ));
3024
3025            for field in data.fields.as_ref() {
3026                let fname = field.name.as_str();
3027                match &field.default_value {
3028                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3029                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3030                        lines.push(alloc::format!(
3031                            "{inner_indent}children.push(Dom::create_text(\"{escaped}\"));"
3032                        ));
3033                    }
3034                    OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => {
3035                        lines.push(alloc::format!(
3036                            "{inner_indent}children.push(Dom::create_text(format!(\"{{}}: {{}}\", \"{fname}\", {b}).as_str()));"
3037                        ));
3038                    }
3039                    OptionComponentDefaultValue::Some(
3040                        ComponentDefaultValue::ComponentInstance(ci),
3041                    ) => {
3042                        let fn_name =
3043                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3044                        lines.push(alloc::format!(
3045                            "{}children.push({}()); // sub-component {}:{}",
3046                            inner_indent,
3047                            fn_name,
3048                            ci.library.as_str(),
3049                            ci.component.as_str()
3050                        ));
3051                    }
3052                    _ => {
3053                        // For other types, generate a placeholder comment
3054                        lines.push(alloc::format!(
3055                            "{}// field '{}': {:?}",
3056                            inner_indent,
3057                            fname,
3058                            field.field_type
3059                        ));
3060                    }
3061                }
3062            }
3063
3064            lines.push(alloc::format!(
3065                "{indent_str}Dom::create_node(NodeType::Div).with_children(children.into())"
3066            ));
3067            Ok(lines.join("\n").into())
3068        }
3069        CompileTarget::C => {
3070            let mut lines = Vec::new();
3071            lines.push(alloc::format!("{indent_str}/* Component: {tag} */"));
3072            lines.push(alloc::format!(
3073                "{indent_str}AzDom root = AzDom_createDiv();"
3074            ));
3075
3076            for field in data.fields.as_ref() {
3077                let fname = field.name.as_str();
3078                match &field.default_value {
3079                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3080                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3081                        lines.push(alloc::format!(
3082                            "{inner_indent}AzDom_addChild(&root, AzDom_createText(AZ_STR(\"{escaped}\")));"
3083                        ));
3084                    }
3085                    OptionComponentDefaultValue::Some(
3086                        ComponentDefaultValue::ComponentInstance(ci),
3087                    ) => {
3088                        let fn_name =
3089                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3090                        lines.push(alloc::format!(
3091                            "{inner_indent}AzDom_addChild(&root, {fn_name}());"
3092                        ));
3093                    }
3094                    _ => {
3095                        lines.push(alloc::format!("{inner_indent}/* field '{fname}' */"));
3096                    }
3097                }
3098            }
3099
3100            lines.push(alloc::format!("{indent_str}return root;"));
3101            Ok(lines.join("\n").into())
3102        }
3103        CompileTarget::Cpp => {
3104            let mut lines = Vec::new();
3105            lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3106            lines.push(alloc::format!(
3107                "{indent_str}auto root = Dom::create_div();"
3108            ));
3109
3110            for field in data.fields.as_ref() {
3111                let fname = field.name.as_str();
3112                match &field.default_value {
3113                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3114                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3115                        lines.push(alloc::format!(
3116                            "{inner_indent}root.add_child(Dom::create_text(String(\"{escaped}\")));"
3117                        ));
3118                    }
3119                    OptionComponentDefaultValue::Some(
3120                        ComponentDefaultValue::ComponentInstance(ci),
3121                    ) => {
3122                        let fn_name =
3123                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3124                        lines.push(alloc::format!(
3125                            "{inner_indent}root.add_child({fn_name}());"
3126                        ));
3127                    }
3128                    _ => {
3129                        lines.push(alloc::format!("{inner_indent}// field '{fname}'"));
3130                    }
3131                }
3132            }
3133
3134            lines.push(alloc::format!("{indent_str}return root;"));
3135            Ok(lines.join("\n").into())
3136        }
3137        CompileTarget::Python => {
3138            let mut lines = Vec::new();
3139            lines.push(alloc::format!("{indent_str}# Component: {tag}"));
3140            lines.push(alloc::format!("{indent_str}root = Dom.create_div()"));
3141
3142            for field in data.fields.as_ref() {
3143                let fname = field.name.as_str();
3144                match &field.default_value {
3145                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3146                        let escaped = s
3147                            .as_str()
3148                            .replace('\\', "\\\\")
3149                            .replace('"', "\\\"")
3150                            .replace('\'', "\\'");
3151                        lines.push(alloc::format!(
3152                            "{inner_indent}root = root.with_child(Dom.create_text(\"{escaped}\"))"
3153                        ));
3154                    }
3155                    OptionComponentDefaultValue::Some(
3156                        ComponentDefaultValue::ComponentInstance(ci),
3157                    ) => {
3158                        let fn_name =
3159                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3160                        lines.push(alloc::format!(
3161                            "{inner_indent}root = root.with_child({fn_name}())"
3162                        ));
3163                    }
3164                    _ => {
3165                        lines.push(alloc::format!("{inner_indent}# field '{fname}'"));
3166                    }
3167                }
3168            }
3169
3170            lines.push(alloc::format!("{indent_str}return root"));
3171            Ok(lines.join("\n").into())
3172        }
3173    };
3174    r.into()
3175}
3176
3177/// Create a `ComponentDef` for a builtin HTML element.
3178///
3179/// # Arguments
3180/// * `tag` - HTML tag name (e.g. "button", "div")
3181/// * `display_name` - Human-readable name (e.g. "Button", "Div")
3182/// * `default_text` - Default text content for the preview, or `None` if the element has no text.
3183///   Pass `Some("Button text")` for `<button>`, `Some("")` for text elements like `<span>` that
3184///   accept text but have no meaningful default.
3185/// * `css` - Component-level CSS string. For most builtin elements this is `""` because
3186///   styling comes from `ua_css.rs` and the `SystemStyle`. Components that need extra
3187///   styling (e.g. a future high-level button widget) can pass CSS here.
3188fn builtin_component_def(
3189    tag: &str,
3190    display_name: &str,
3191    default_text: Option<&str>,
3192    css: &str,
3193) -> ComponentDef {
3194    let mut fields = builtin_data_model(tag);
3195    // If a default_text is provided, this element accepts text content
3196    if let Some(text) = default_text {
3197        fields.push(data_field(
3198            "text",
3199            ComponentFieldType::String,
3200            Some(ComponentDefaultValue::String(AzString::from(text))),
3201            "Text content of the element",
3202        ));
3203    }
3204    let model_name = format!("{display_name}Data");
3205    ComponentDef {
3206        id: ComponentId::builtin(tag),
3207        display_name: AzString::from(display_name),
3208        description: AzString::from(format!("HTML <{tag}> element").as_str()),
3209        css: AzString::from(css),
3210        source: ComponentSource::Builtin,
3211        data_model: ComponentDataModel {
3212            name: AzString::from(model_name.as_str()),
3213            description: AzString::from(format!("Data model for <{tag}>").as_str()),
3214            fields: fields.into(),
3215        },
3216        render_fn: builtin_render_fn,
3217        compile_fn: builtin_compile_fn,
3218        render_fn_source: None.into(),
3219        compile_fn_source: None.into(),
3220    }
3221}
3222
3223/// Helper to create a `ComponentDataField` with a rich type
3224fn data_field(
3225    name: &str,
3226    ft: ComponentFieldType,
3227    default: Option<ComponentDefaultValue>,
3228    description: &str,
3229) -> ComponentDataField {
3230    let required = default.is_none();
3231    ComponentDataField {
3232        name: AzString::from(name),
3233        field_type: ft,
3234        default_value: default.map_or_else(|| OptionComponentDefaultValue::None, OptionComponentDefaultValue::Some),
3235        required,
3236        description: AzString::from(description),
3237    }
3238}
3239
3240/// Returns the tag-specific data model fields for builtin HTML elements.
3241/// These are the component's "main data model" — the attributes that define
3242/// what the component needs as configuration (e.g., `href` for `<a>`,
3243/// `src` for `<img>`). Universal HTML attributes (id, class, style, etc.)
3244/// are NOT included here — they are added separately by the debug server.
3245#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3246fn builtin_data_model(tag: &str) -> Vec<ComponentDataField> {
3247    use ComponentDefaultValue as D;
3248    use ComponentFieldType::{String, Bool, I32};
3249    match tag {
3250        "a" => alloc::vec![
3251            data_field(
3252                "href",
3253                String,
3254                Some(D::String(AzString::from_const_str(""))),
3255                "URL the link points to"
3256            ),
3257            data_field(
3258                "target",
3259                String,
3260                Some(D::String(AzString::from_const_str(""))),
3261                "Where to open the linked document (_blank, _self, _parent, _top)"
3262            ),
3263            data_field(
3264                "rel",
3265                String,
3266                Some(D::String(AzString::from_const_str(""))),
3267                "Relationship between current and linked document"
3268            ),
3269        ],
3270        "img" | "image" => alloc::vec![
3271            data_field("src", String, None, "URL of the image"),
3272            data_field(
3273                "alt",
3274                String,
3275                Some(D::String(AzString::from_const_str(""))),
3276                "Alternative text for the image"
3277            ),
3278            data_field(
3279                "width",
3280                String,
3281                Some(D::String(AzString::from_const_str(""))),
3282                "Width of the image"
3283            ),
3284            data_field(
3285                "height",
3286                String,
3287                Some(D::String(AzString::from_const_str(""))),
3288                "Height of the image"
3289            ),
3290        ],
3291        "form" => alloc::vec![
3292            data_field(
3293                "action",
3294                String,
3295                Some(D::String(AzString::from_const_str(""))),
3296                "URL where form data is submitted"
3297            ),
3298            data_field(
3299                "method",
3300                String,
3301                Some(D::String(AzString::from_const_str("GET"))),
3302                "HTTP method for form submission (GET or POST)"
3303            ),
3304        ],
3305        "label" => alloc::vec![data_field(
3306            "for",
3307            String,
3308            Some(D::String(AzString::from_const_str(""))),
3309            "ID of the form element this label is for"
3310        ),],
3311        "button" => alloc::vec![
3312            data_field(
3313                "type",
3314                String,
3315                Some(D::String(AzString::from_const_str("button"))),
3316                "Button type (button, submit, reset)"
3317            ),
3318            data_field(
3319                "disabled",
3320                Bool,
3321                Some(D::Bool(false)),
3322                "Whether the button is disabled"
3323            ),
3324        ],
3325        "td" | "th" => alloc::vec![
3326            data_field(
3327                "colspan",
3328                I32,
3329                Some(D::I32(1)),
3330                "Number of columns the cell spans"
3331            ),
3332            data_field(
3333                "rowspan",
3334                I32,
3335                Some(D::I32(1)),
3336                "Number of rows the cell spans"
3337            ),
3338        ],
3339        "icon" => alloc::vec![data_field(
3340            "name",
3341            String,
3342            Some(D::String(AzString::from_const_str(""))),
3343            "Icon name"
3344        ),],
3345        "ol" => alloc::vec![
3346            data_field(
3347                "start",
3348                I32,
3349                Some(D::I32(1)),
3350                "Start value for the ordered list"
3351            ),
3352            data_field(
3353                "type",
3354                String,
3355                Some(D::String(AzString::from_const_str("1"))),
3356                "Numbering type (1, A, a, I, i)"
3357            ),
3358        ],
3359        // Form controls
3360        "input" => alloc::vec![
3361            data_field(
3362                "type",
3363                String,
3364                Some(D::String(AzString::from_const_str("text"))),
3365                "Input type (text, password, email, number, checkbox, radio, etc.)"
3366            ),
3367            data_field(
3368                "name",
3369                String,
3370                Some(D::String(AzString::from_const_str(""))),
3371                "Name of the input for form submission"
3372            ),
3373            data_field(
3374                "value",
3375                String,
3376                Some(D::String(AzString::from_const_str(""))),
3377                "Current value of the input"
3378            ),
3379            data_field(
3380                "placeholder",
3381                String,
3382                Some(D::String(AzString::from_const_str(""))),
3383                "Placeholder text"
3384            ),
3385            data_field(
3386                "disabled",
3387                Bool,
3388                Some(D::Bool(false)),
3389                "Whether the input is disabled"
3390            ),
3391            data_field(
3392                "required",
3393                Bool,
3394                Some(D::Bool(false)),
3395                "Whether the input is required"
3396            ),
3397            data_field(
3398                "readonly",
3399                Bool,
3400                Some(D::Bool(false)),
3401                "Whether the input is read-only"
3402            ),
3403            data_field(
3404                "checked",
3405                Bool,
3406                Some(D::Bool(false)),
3407                "Whether the checkbox/radio is checked"
3408            ),
3409            data_field(
3410                "min",
3411                String,
3412                Some(D::String(AzString::from_const_str(""))),
3413                "Minimum value (for number, range, date)"
3414            ),
3415            data_field(
3416                "max",
3417                String,
3418                Some(D::String(AzString::from_const_str(""))),
3419                "Maximum value (for number, range, date)"
3420            ),
3421            data_field(
3422                "step",
3423                String,
3424                Some(D::String(AzString::from_const_str(""))),
3425                "Step increment (for number, range)"
3426            ),
3427            data_field(
3428                "pattern",
3429                String,
3430                Some(D::String(AzString::from_const_str(""))),
3431                "Regex pattern for validation"
3432            ),
3433            data_field(
3434                "maxlength",
3435                String,
3436                Some(D::String(AzString::from_const_str(""))),
3437                "Maximum number of characters"
3438            ),
3439        ],
3440        "select" => alloc::vec![
3441            data_field(
3442                "name",
3443                String,
3444                Some(D::String(AzString::from_const_str(""))),
3445                "Name for form submission"
3446            ),
3447            data_field(
3448                "multiple",
3449                Bool,
3450                Some(D::Bool(false)),
3451                "Whether multiple options can be selected"
3452            ),
3453            data_field(
3454                "disabled",
3455                Bool,
3456                Some(D::Bool(false)),
3457                "Whether the select is disabled"
3458            ),
3459            data_field(
3460                "required",
3461                Bool,
3462                Some(D::Bool(false)),
3463                "Whether selection is required"
3464            ),
3465            data_field(
3466                "size",
3467                String,
3468                Some(D::String(AzString::from_const_str(""))),
3469                "Number of visible options"
3470            ),
3471        ],
3472        "option" => alloc::vec![
3473            data_field(
3474                "value",
3475                String,
3476                Some(D::String(AzString::from_const_str(""))),
3477                "Value submitted with the form"
3478            ),
3479            data_field(
3480                "selected",
3481                Bool,
3482                Some(D::Bool(false)),
3483                "Whether this option is selected"
3484            ),
3485            data_field(
3486                "disabled",
3487                Bool,
3488                Some(D::Bool(false)),
3489                "Whether this option is disabled"
3490            ),
3491        ],
3492        "optgroup" => alloc::vec![
3493            data_field(
3494                "label",
3495                String,
3496                Some(D::String(AzString::from_const_str(""))),
3497                "Label for the option group"
3498            ),
3499            data_field(
3500                "disabled",
3501                Bool,
3502                Some(D::Bool(false)),
3503                "Whether the group is disabled"
3504            ),
3505        ],
3506        "textarea" => alloc::vec![
3507            data_field(
3508                "name",
3509                String,
3510                Some(D::String(AzString::from_const_str(""))),
3511                "Name for form submission"
3512            ),
3513            data_field(
3514                "placeholder",
3515                String,
3516                Some(D::String(AzString::from_const_str(""))),
3517                "Placeholder text"
3518            ),
3519            data_field("rows", I32, Some(D::I32(2)), "Number of visible text lines"),
3520            data_field(
3521                "cols",
3522                I32,
3523                Some(D::I32(20)),
3524                "Visible width in average character widths"
3525            ),
3526            data_field(
3527                "disabled",
3528                Bool,
3529                Some(D::Bool(false)),
3530                "Whether the textarea is disabled"
3531            ),
3532            data_field(
3533                "required",
3534                Bool,
3535                Some(D::Bool(false)),
3536                "Whether content is required"
3537            ),
3538            data_field(
3539                "readonly",
3540                Bool,
3541                Some(D::Bool(false)),
3542                "Whether the textarea is read-only"
3543            ),
3544            data_field(
3545                "maxlength",
3546                String,
3547                Some(D::String(AzString::from_const_str(""))),
3548                "Maximum number of characters"
3549            ),
3550        ],
3551        "fieldset" => alloc::vec![data_field(
3552            "disabled",
3553            Bool,
3554            Some(D::Bool(false)),
3555            "Whether all controls in the fieldset are disabled"
3556        ),],
3557        "output" => alloc::vec![
3558            data_field(
3559                "for",
3560                String,
3561                Some(D::String(AzString::from_const_str(""))),
3562                "IDs of elements that contributed to the output"
3563            ),
3564            data_field(
3565                "name",
3566                String,
3567                Some(D::String(AzString::from_const_str(""))),
3568                "Name for form submission"
3569            ),
3570        ],
3571        "progress" => alloc::vec![
3572            data_field(
3573                "value",
3574                String,
3575                Some(D::String(AzString::from_const_str(""))),
3576                "Current progress value"
3577            ),
3578            data_field(
3579                "max",
3580                String,
3581                Some(D::String(AzString::from_const_str("1"))),
3582                "Maximum value"
3583            ),
3584        ],
3585        "meter" => alloc::vec![
3586            data_field(
3587                "value",
3588                String,
3589                Some(D::String(AzString::from_const_str(""))),
3590                "Current value"
3591            ),
3592            data_field(
3593                "min",
3594                String,
3595                Some(D::String(AzString::from_const_str("0"))),
3596                "Minimum value"
3597            ),
3598            data_field(
3599                "max",
3600                String,
3601                Some(D::String(AzString::from_const_str("1"))),
3602                "Maximum value"
3603            ),
3604            data_field(
3605                "low",
3606                String,
3607                Some(D::String(AzString::from_const_str(""))),
3608                "Low threshold"
3609            ),
3610            data_field(
3611                "high",
3612                String,
3613                Some(D::String(AzString::from_const_str(""))),
3614                "High threshold"
3615            ),
3616            data_field(
3617                "optimum",
3618                String,
3619                Some(D::String(AzString::from_const_str(""))),
3620                "Optimum value"
3621            ),
3622        ],
3623        // Interactive
3624        "details" => alloc::vec![data_field(
3625            "open",
3626            Bool,
3627            Some(D::Bool(false)),
3628            "Whether the details are visible"
3629        ),],
3630        "dialog" => alloc::vec![data_field(
3631            "open",
3632            Bool,
3633            Some(D::Bool(false)),
3634            "Whether the dialog is active and can be interacted with"
3635        ),],
3636        // Embedded content
3637        "audio" | "video" => alloc::vec![
3638            data_field(
3639                "src",
3640                String,
3641                Some(D::String(AzString::from_const_str(""))),
3642                "URL of the media resource"
3643            ),
3644            data_field(
3645                "controls",
3646                Bool,
3647                Some(D::Bool(false)),
3648                "Whether to show playback controls"
3649            ),
3650            data_field(
3651                "autoplay",
3652                Bool,
3653                Some(D::Bool(false)),
3654                "Whether to start playing automatically"
3655            ),
3656            data_field(
3657                "loop",
3658                Bool,
3659                Some(D::Bool(false)),
3660                "Whether to loop playback"
3661            ),
3662            data_field(
3663                "muted",
3664                Bool,
3665                Some(D::Bool(false)),
3666                "Whether audio is muted"
3667            ),
3668            data_field(
3669                "preload",
3670                String,
3671                Some(D::String(AzString::from_const_str("auto"))),
3672                "Preload hint (none, metadata, auto)"
3673            ),
3674        ],
3675        "source" => alloc::vec![
3676            data_field("src", String, None, "URL of the media resource"),
3677            data_field(
3678                "type",
3679                String,
3680                Some(D::String(AzString::from_const_str(""))),
3681                "MIME type of the resource"
3682            ),
3683        ],
3684        "track" => alloc::vec![
3685            data_field("src", String, None, "URL of the track file"),
3686            data_field(
3687                "kind",
3688                String,
3689                Some(D::String(AzString::from_const_str("subtitles"))),
3690                "Kind of text track (subtitles, captions, descriptions, chapters, metadata)"
3691            ),
3692            data_field(
3693                "srclang",
3694                String,
3695                Some(D::String(AzString::from_const_str(""))),
3696                "Language of the track text"
3697            ),
3698            data_field(
3699                "label",
3700                String,
3701                Some(D::String(AzString::from_const_str(""))),
3702                "User-readable title for the track"
3703            ),
3704            data_field(
3705                "default",
3706                Bool,
3707                Some(D::Bool(false)),
3708                "Whether this is the default track"
3709            ),
3710        ],
3711        "canvas" => alloc::vec![
3712            data_field(
3713                "width",
3714                String,
3715                Some(D::String(AzString::from_const_str("300"))),
3716                "Width of the canvas in pixels"
3717            ),
3718            data_field(
3719                "height",
3720                String,
3721                Some(D::String(AzString::from_const_str("150"))),
3722                "Height of the canvas in pixels"
3723            ),
3724        ],
3725        "embed" => alloc::vec![
3726            data_field("src", String, None, "URL of the resource to embed"),
3727            data_field(
3728                "type",
3729                String,
3730                Some(D::String(AzString::from_const_str(""))),
3731                "MIME type of the embedded content"
3732            ),
3733            data_field(
3734                "width",
3735                String,
3736                Some(D::String(AzString::from_const_str(""))),
3737                "Width"
3738            ),
3739            data_field(
3740                "height",
3741                String,
3742                Some(D::String(AzString::from_const_str(""))),
3743                "Height"
3744            ),
3745        ],
3746        "object" => alloc::vec![
3747            data_field(
3748                "data",
3749                String,
3750                Some(D::String(AzString::from_const_str(""))),
3751                "URL of the resource"
3752            ),
3753            data_field(
3754                "type",
3755                String,
3756                Some(D::String(AzString::from_const_str(""))),
3757                "MIME type of the resource"
3758            ),
3759            data_field(
3760                "width",
3761                String,
3762                Some(D::String(AzString::from_const_str(""))),
3763                "Width"
3764            ),
3765            data_field(
3766                "height",
3767                String,
3768                Some(D::String(AzString::from_const_str(""))),
3769                "Height"
3770            ),
3771        ],
3772        "param" => alloc::vec![
3773            data_field("name", String, None, "Name of the parameter"),
3774            data_field(
3775                "value",
3776                String,
3777                Some(D::String(AzString::from_const_str(""))),
3778                "Value of the parameter"
3779            ),
3780        ],
3781        "area" => alloc::vec![
3782            data_field(
3783                "shape",
3784                String,
3785                Some(D::String(AzString::from_const_str("default"))),
3786                "Shape of the area (default, rect, circle, poly)"
3787            ),
3788            data_field(
3789                "coords",
3790                String,
3791                Some(D::String(AzString::from_const_str(""))),
3792                "Coordinates of the area"
3793            ),
3794            data_field(
3795                "href",
3796                String,
3797                Some(D::String(AzString::from_const_str(""))),
3798                "URL for the area link"
3799            ),
3800            data_field(
3801                "alt",
3802                String,
3803                Some(D::String(AzString::from_const_str(""))),
3804                "Alternative text"
3805            ),
3806            data_field(
3807                "target",
3808                String,
3809                Some(D::String(AzString::from_const_str(""))),
3810                "Where to open the linked document"
3811            ),
3812        ],
3813        "map" => alloc::vec![data_field(
3814            "name",
3815            String,
3816            None,
3817            "Name of the image map (referenced by usemap)"
3818        ),],
3819        // Inline semantics with special attributes
3820        "time" => alloc::vec![data_field(
3821            "datetime",
3822            String,
3823            Some(D::String(AzString::from_const_str(""))),
3824            "Machine-readable date/time value"
3825        ),],
3826        "data" => alloc::vec![data_field(
3827            "value",
3828            String,
3829            Some(D::String(AzString::from_const_str(""))),
3830            "Machine-readable value"
3831        ),],
3832        "abbr" | "acronym" | "dfn" => alloc::vec![data_field(
3833            "title",
3834            String,
3835            Some(D::String(AzString::from_const_str(""))),
3836            "Full expansion or definition"
3837        ),],
3838        "q" | "blockquote" => alloc::vec![data_field(
3839            "cite",
3840            String,
3841            Some(D::String(AzString::from_const_str(""))),
3842            "URL of the source of the quotation"
3843        ),],
3844        "del" | "ins" => alloc::vec![
3845            data_field(
3846                "cite",
3847                String,
3848                Some(D::String(AzString::from_const_str(""))),
3849                "URL explaining the change"
3850            ),
3851            data_field(
3852                "datetime",
3853                String,
3854                Some(D::String(AzString::from_const_str(""))),
3855                "Date/time of the change"
3856            ),
3857        ],
3858        "bdo" => alloc::vec![data_field(
3859            "dir",
3860            String,
3861            Some(D::String(AzString::from_const_str("ltr"))),
3862            "Text direction (ltr, rtl)"
3863        ),],
3864        "col" | "colgroup" => alloc::vec![data_field(
3865            "span",
3866            I32,
3867            Some(D::I32(1)),
3868            "Number of columns the element spans"
3869        ),],
3870        // Metadata
3871        "meta" => alloc::vec![
3872            data_field(
3873                "name",
3874                String,
3875                Some(D::String(AzString::from_const_str(""))),
3876                "Metadata name"
3877            ),
3878            data_field(
3879                "content",
3880                String,
3881                Some(D::String(AzString::from_const_str(""))),
3882                "Metadata value"
3883            ),
3884            data_field(
3885                "charset",
3886                String,
3887                Some(D::String(AzString::from_const_str(""))),
3888                "Character encoding"
3889            ),
3890            data_field(
3891                "http-equiv",
3892                String,
3893                Some(D::String(AzString::from_const_str(""))),
3894                "HTTP header equivalent"
3895            ),
3896        ],
3897        "link" => alloc::vec![
3898            data_field("rel", String, None, "Relationship type"),
3899            data_field(
3900                "href",
3901                String,
3902                Some(D::String(AzString::from_const_str(""))),
3903                "URL of the linked resource"
3904            ),
3905            data_field(
3906                "type",
3907                String,
3908                Some(D::String(AzString::from_const_str(""))),
3909                "MIME type of the linked resource"
3910            ),
3911        ],
3912        "script" => alloc::vec![
3913            data_field(
3914                "src",
3915                String,
3916                Some(D::String(AzString::from_const_str(""))),
3917                "URL of external script"
3918            ),
3919            data_field(
3920                "type",
3921                String,
3922                Some(D::String(AzString::from_const_str(""))),
3923                "MIME type or module"
3924            ),
3925            data_field(
3926                "async",
3927                Bool,
3928                Some(D::Bool(false)),
3929                "Execute asynchronously"
3930            ),
3931            data_field(
3932                "defer",
3933                Bool,
3934                Some(D::Bool(false)),
3935                "Defer execution until page load"
3936            ),
3937        ],
3938        "style" => alloc::vec![data_field(
3939            "type",
3940            String,
3941            Some(D::String(AzString::from_const_str("text/css"))),
3942            "MIME type of the style sheet"
3943        ),],
3944        "base" => alloc::vec![
3945            data_field(
3946                "href",
3947                String,
3948                Some(D::String(AzString::from_const_str(""))),
3949                "Base URL for relative URLs"
3950            ),
3951            data_field(
3952                "target",
3953                String,
3954                Some(D::String(AzString::from_const_str(""))),
3955                "Default target for hyperlinks"
3956            ),
3957        ],
3958        _ => alloc::vec![],
3959    }
3960}
3961
3962impl Default for ComponentMap {
3963    /// Returns an empty `ComponentMap` with no libraries.
3964    ///
3965    /// Use `AppConfig::create()` (which registers the 52 builtins via
3966    /// `register_builtin_components`) followed by `ComponentMap::from_libraries()`
3967    /// to get a fully-populated map.
3968    fn default() -> Self {
3969        Self {
3970            libraries: ComponentLibraryVec::from_const_slice(&[]),
3971        }
3972    }
3973}
3974
3975impl ComponentMap {
3976    #[must_use] pub fn create() -> Self {
3977        Self::default()
3978    }
3979
3980    /// Create a `ComponentMap` with the 52 built-in HTML element components pre-registered.
3981    #[must_use] pub fn with_builtin() -> Self {
3982        Self {
3983            libraries: alloc::vec![register_builtin_components()].into(),
3984        }
3985    }
3986
3987    /// Build a `ComponentMap` from the libraries stored in an `AppConfig`.
3988    ///
3989    /// The `component_libraries` field already contains builtins (registered in
3990    /// `AppConfig::create()`) plus any user-added libraries.  No merging needed —
3991    /// `add_component_library` / `add_component` handle insertion at registration time.
3992    #[must_use] pub fn from_libraries(libs: &ComponentLibraryVec) -> Self {
3993        Self {
3994            libraries: libs.clone(),
3995        }
3996    }
3997}
3998
3999/// Convert XML attributes to a `ComponentDataModel` by cloning the component's
4000/// base data model and overriding field defaults with values from the XML attributes.
4001///
4002/// This is the bridge between the XML parsing layer (key-value string pairs)
4003/// and the typed component data model. For each field in the base model,
4004/// if a matching XML attribute exists, its string value is set as the new default.
4005///
4006/// # Arguments
4007/// * `base_model` - The component's data model template (from `ComponentDef::data_model`)
4008/// * `xml_attributes` - The XML node's attribute map
4009/// * `text_content` - Optional text content from child text nodes
4010///
4011/// # Returns
4012/// A cloned `ComponentDataModel` with overridden defaults
4013fn xml_attrs_to_data_model(
4014    base_model: &ComponentDataModel,
4015    xml_attributes: &XmlAttributeMap,
4016    text_content: Option<&str>,
4017) -> ComponentDataModel {
4018    let mut model = base_model.clone();
4019
4020    // Override defaults from XML attributes
4021    let mut fields_vec = core::mem::replace(
4022        &mut model.fields,
4023        ComponentDataFieldVec::from_const_slice(&[]),
4024    )
4025    .into_library_owned_vec();
4026
4027    for field in &mut fields_vec {
4028        if let Some(attr_value) = xml_attributes.get_key(field.name.as_str()) {
4029            // Override the default_value with the XML attribute's string value
4030            field.default_value = OptionComponentDefaultValue::Some(ComponentDefaultValue::String(
4031                attr_value.clone(),
4032            ));
4033        }
4034    }
4035
4036    model.fields = ComponentDataFieldVec::from_vec(fields_vec);
4037
4038    // Handle text content — set the "text" field if present
4039    if let Some(text) = text_content {
4040        let prepared = prepare_string(text);
4041        if !prepared.is_empty() {
4042            model = model.with_default(
4043                "text",
4044                ComponentDefaultValue::String(AzString::from(prepared.as_str())),
4045            );
4046        }
4047    }
4048
4049    model
4050}
4051
4052// ============================================================================
4053// Structural builtin components: if, for, map
4054// ============================================================================
4055
4056/// `builtin:if` — conditional rendering.
4057/// Takes `condition: Bool`, `then: StyledDom`, and optionally `else: StyledDom`.
4058fn builtin_if_component() -> ComponentDef {
4059    ComponentDef {
4060        id: ComponentId::builtin("if"),
4061        display_name: AzString::from_const_str("If"),
4062        description: AzString::from_const_str("Conditional rendering: shows 'then' if condition is true, else shows 'else' (if provided)."),
4063        css: AzString::from_const_str(""),
4064        source: ComponentSource::Builtin,
4065        data_model: ComponentDataModel {
4066            name: AzString::from_const_str("IfData"),
4067            description: AzString::from_const_str("Data for conditional rendering"),
4068            fields: alloc::vec![
4069                data_field("condition", ComponentFieldType::Bool, Some(ComponentDefaultValue::Bool(false)), "The boolean condition to evaluate"),
4070            ].into(),
4071        },
4072        render_fn: builtin_if_render_fn,
4073        compile_fn: builtin_if_compile_fn,
4074        render_fn_source: None.into(),
4075        compile_fn_source: None.into(),
4076    }
4077}
4078
4079fn builtin_if_render_fn(
4080    _comp: &ComponentDef,
4081    data_model: &ComponentDataModel,
4082    _component_map: &ComponentMap,
4083) -> ResultStyledDomRenderDomError {
4084    // Evaluate the condition field
4085    let condition = data_model
4086        .fields
4087        .iter()
4088        .find(|f| f.name.as_str() == "condition")
4089        .and_then(|f| match &f.default_value {
4090            OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => Some(*b),
4091            _ => None,
4092        })
4093        .unwrap_or(false);
4094
4095    let label = if condition {
4096        "if: true (then branch)"
4097    } else {
4098        "if: false (else branch)"
4099    };
4100    let mut dom =
4101        Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(label)].into());
4102    let css = Css::empty();
4103    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4104}
4105
4106fn builtin_if_compile_fn(
4107    _comp: &ComponentDef,
4108    target: &CompileTarget,
4109    _data: &ComponentDataModel,
4110    _indent: usize,
4111) -> ResultStringCompileError {
4112    match target {
4113        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4114            "if data.condition {\n    // then branch\n    Dom::create_div()\n} else {\n    // else branch\n    Dom::create_div()\n}"
4115        )),
4116        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4117            "if (data.condition) {\n    // then branch\n    AzDom_createDiv();\n} else {\n    // else branch\n    AzDom_createDiv();\n}"
4118        )),
4119        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4120            "if (data.condition) {\n    // then branch\n    Dom::create_div();\n} else {\n    // else branch\n    Dom::create_div();\n}"
4121        )),
4122        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4123            "if data.condition:\n    # then branch\n    Dom.create_div()\nelse:\n    # else branch\n    Dom.create_div()"
4124        )),
4125    }
4126}
4127
4128/// `builtin:for` — iterative rendering.
4129/// Takes `count: U32` (number of iterations), renders children N times.
4130fn builtin_for_component() -> ComponentDef {
4131    ComponentDef {
4132        id: ComponentId::builtin("for"),
4133        display_name: AzString::from_const_str("For Loop"),
4134        description: AzString::from_const_str(
4135            "Iterative rendering: repeats children 'count' times.",
4136        ),
4137        css: AzString::from_const_str(""),
4138        source: ComponentSource::Builtin,
4139        data_model: ComponentDataModel {
4140            name: AzString::from_const_str("ForData"),
4141            description: AzString::from_const_str("Data for iterative rendering"),
4142            fields: alloc::vec![data_field(
4143                "count",
4144                ComponentFieldType::U32,
4145                Some(ComponentDefaultValue::U32(3)),
4146                "Number of iterations"
4147            ),]
4148            .into(),
4149        },
4150        render_fn: builtin_for_render_fn,
4151        compile_fn: builtin_for_compile_fn,
4152        render_fn_source: None.into(),
4153        compile_fn_source: None.into(),
4154    }
4155}
4156
4157fn builtin_for_render_fn(
4158    _comp: &ComponentDef,
4159    data_model: &ComponentDataModel,
4160    _component_map: &ComponentMap,
4161) -> ResultStyledDomRenderDomError {
4162    let count = data_model
4163        .fields
4164        .iter()
4165        .find(|f| f.name.as_str() == "count")
4166        .and_then(|f| match &f.default_value {
4167            OptionComponentDefaultValue::Some(ComponentDefaultValue::U32(n)) => Some(*n),
4168            _ => None,
4169        })
4170        .unwrap_or(3);
4171
4172    let mut items: Vec<Dom> = Vec::new();
4173    for i in 0..count {
4174        items.push(
4175            Dom::create_node(NodeType::Div)
4176                .with_children(alloc::vec![Dom::create_text(alloc::format!("Item {i}"))].into()),
4177        );
4178    }
4179    let mut dom = Dom::create_node(NodeType::Div).with_children(items.into());
4180    let css = Css::empty();
4181    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4182}
4183
4184fn builtin_for_compile_fn(
4185    _comp: &ComponentDef,
4186    target: &CompileTarget,
4187    _data: &ComponentDataModel,
4188    _indent: usize,
4189) -> ResultStringCompileError {
4190    match target {
4191        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4192            "let mut children = Vec::new();\nfor i in 0..data.count {\n    children.push(Dom::create_div());\n}\nDom::create_div().with_children(children)"
4193        )),
4194        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4195            "AzDom container = AzDom_createDiv();\nfor (uint32_t i = 0; i < data.count; i++) {\n    AzDom_addChild(&container, AzDom_createDiv());\n}"
4196        )),
4197        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4198            "auto container = Dom::create_div();\nfor (uint32_t i = 0; i < data.count; i++) {\n    container.add_child(Dom::create_div());\n}"
4199        )),
4200        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4201            "container = Dom.create_div()\nfor i in range(data.count):\n    container = container.with_child(Dom.create_div())"
4202        )),
4203    }
4204}
4205
4206/// `builtin:map` — map data to DOM.
4207/// Takes `data_json: String` (JSON array) + maps each element.
4208fn builtin_map_component() -> ComponentDef {
4209    ComponentDef {
4210        id: ComponentId::builtin("map"),
4211        display_name: AzString::from_const_str("Map"),
4212        description: AzString::from_const_str(
4213            "Map data to DOM: applies a template to each item in a collection.",
4214        ),
4215        css: AzString::from_const_str(""),
4216        source: ComponentSource::Builtin,
4217        data_model: ComponentDataModel {
4218            name: AzString::from_const_str("MapData"),
4219            description: AzString::from_const_str("Data for map rendering"),
4220            fields: alloc::vec![data_field(
4221                "data_json",
4222                ComponentFieldType::String,
4223                Some(ComponentDefaultValue::String(AzString::from_const_str(
4224                    "[]"
4225                ))),
4226                "JSON array of items to map over"
4227            ),]
4228            .into(),
4229        },
4230        render_fn: builtin_map_render_fn,
4231        compile_fn: builtin_map_compile_fn,
4232        render_fn_source: None.into(),
4233        compile_fn_source: None.into(),
4234    }
4235}
4236
4237fn builtin_map_render_fn(
4238    _comp: &ComponentDef,
4239    data_model: &ComponentDataModel,
4240    _component_map: &ComponentMap,
4241) -> ResultStyledDomRenderDomError {
4242    // For now, render a placeholder — actual mapping requires callback support
4243    let data_str = data_model
4244        .fields
4245        .iter()
4246        .find(|f| f.name.as_str() == "data_json")
4247        .and_then(|f| match &f.default_value {
4248            OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
4249                Some(s.as_str().to_string())
4250            }
4251            _ => None,
4252        })
4253        .unwrap_or_else(|| "[]".to_string());
4254
4255    let label = alloc::format!("map: data_json={data_str}");
4256    let mut dom =
4257        Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(label)].into());
4258    let css = Css::empty();
4259    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4260}
4261
4262fn builtin_map_compile_fn(
4263    _comp: &ComponentDef,
4264    target: &CompileTarget,
4265    _data: &ComponentDataModel,
4266    _indent: usize,
4267) -> ResultStringCompileError {
4268    match target {
4269        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4270            "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)"
4271        )),
4272        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4273            "// Parse data.data_json and map each item\nAzDom container = AzDom_createDiv();\n// TODO: iterate parsed JSON array"
4274        )),
4275        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4276            "// Parse data.data_json and map each item\nauto container = Dom::create_div();\n// TODO: iterate parsed JSON array"
4277        )),
4278        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4279            "import json\nitems = json.loads(data.data_json)\ncontainer = Dom.create_div()\nfor item in items:\n    container = container.with_child(Dom.create_div())"
4280        )),
4281    }
4282}
4283
4284/// Register the 52 built-in HTML element components.
4285///
4286/// This is an `extern "C"` function pointer compatible with
4287/// `RegisterComponentLibraryFnType`, so it can be passed directly to
4288/// `AppConfig::add_component_library()`.
4289///
4290/// Called once during `AppConfig::create()` — the framework dogfoods
4291/// its own component registration system for builtins.
4292#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
4293#[must_use] pub extern "C" fn register_builtin_components() -> ComponentLibrary {
4294    ComponentLibrary {
4295        name: AzString::from_const_str("builtin"),
4296        version: AzString::from_const_str("1.0.0"),
4297        description: AzString::from_const_str("Built-in HTML elements"),
4298        exportable: false,
4299        modifiable: false,
4300        data_models: Vec::new().into(),
4301        enum_models: Vec::new().into(),
4302        components: alloc::vec![
4303            // Structural
4304            builtin_component_def("html", "HTML", None, ""),
4305            builtin_component_def("head", "Head", None, ""),
4306            builtin_component_def("title", "Title", Some(""), ""),
4307            builtin_component_def("body", "Body", None, ""),
4308            // Block-level
4309            builtin_component_def("div", "Div", None, ""),
4310            builtin_component_def("header", "Header", None, ""),
4311            builtin_component_def("footer", "Footer", None, ""),
4312            builtin_component_def("section", "Section", None, ""),
4313            builtin_component_def("article", "Article", None, ""),
4314            builtin_component_def("aside", "Aside", None, ""),
4315            builtin_component_def("nav", "Nav", None, ""),
4316            builtin_component_def("main", "Main", None, ""),
4317            builtin_component_def("figure", "Figure", None, ""),
4318            builtin_component_def("figcaption", "Figure Caption", Some(""), ""),
4319            builtin_component_def("address", "Address", Some(""), ""),
4320            builtin_component_def("details", "Details", None, ""),
4321            builtin_component_def("summary", "Summary", Some("Details"), ""),
4322            builtin_component_def("dialog", "Dialog", None, ""),
4323            // Headings — default text is the heading level name so preview is visible
4324            builtin_component_def("h1", "Heading 1", Some("Heading 1"), ""),
4325            builtin_component_def("h2", "Heading 2", Some("Heading 2"), ""),
4326            builtin_component_def("h3", "Heading 3", Some("Heading 3"), ""),
4327            builtin_component_def("h4", "Heading 4", Some("Heading 4"), ""),
4328            builtin_component_def("h5", "Heading 5", Some("Heading 5"), ""),
4329            builtin_component_def("h6", "Heading 6", Some("Heading 6"), ""),
4330            // Text content
4331            builtin_component_def("p", "Paragraph", Some("Paragraph text"), ""),
4332            builtin_component_def("span", "Span", Some(""), ""),
4333            builtin_component_def("pre", "Preformatted", Some(""), ""),
4334            builtin_component_def("code", "Code", Some(""), ""),
4335            builtin_component_def("blockquote", "Blockquote", Some(""), ""),
4336            builtin_component_def("br", "Line Break", None, ""),
4337            builtin_component_def("hr", "Horizontal Rule", None, ""),
4338            builtin_component_def("icon", "Icon", Some(""), ""),
4339            // Lists
4340            builtin_component_def("ul", "Unordered List", None, ""),
4341            builtin_component_def("ol", "Ordered List", None, ""),
4342            builtin_component_def("li", "List Item", Some("List item"), ""),
4343            builtin_component_def("dl", "Description List", None, ""),
4344            builtin_component_def("dt", "Description Term", Some(""), ""),
4345            builtin_component_def("dd", "Description Details", Some(""), ""),
4346            builtin_component_def("menu", "Menu", None, ""),
4347            builtin_component_def("menuitem", "Menu Item", Some(""), ""),
4348            builtin_component_def("dir", "Directory List", None, ""),
4349            // Tables
4350            builtin_component_def("table", "Table", None, ""),
4351            builtin_component_def("caption", "Table Caption", Some(""), ""),
4352            builtin_component_def("thead", "Table Head", None, ""),
4353            builtin_component_def("tbody", "Table Body", None, ""),
4354            builtin_component_def("tfoot", "Table Foot", None, ""),
4355            builtin_component_def("tr", "Table Row", None, ""),
4356            builtin_component_def("th", "Table Header Cell", Some("Header"), ""),
4357            builtin_component_def("td", "Table Data Cell", Some(""), ""),
4358            builtin_component_def("colgroup", "Column Group", None, ""),
4359            builtin_component_def("col", "Column", None, ""),
4360            // Inline
4361            builtin_component_def("a", "Link", Some("Link text"), ""),
4362            builtin_component_def("strong", "Strong", Some(""), ""),
4363            builtin_component_def("em", "Emphasis", Some(""), ""),
4364            builtin_component_def("b", "Bold", Some(""), ""),
4365            builtin_component_def("i", "Italic", Some(""), ""),
4366            builtin_component_def("u", "Underline", Some(""), ""),
4367            builtin_component_def("s", "Strikethrough", Some(""), ""),
4368            builtin_component_def("small", "Small", Some(""), ""),
4369            builtin_component_def("mark", "Mark", Some(""), ""),
4370            builtin_component_def("del", "Deleted Text", Some(""), ""),
4371            builtin_component_def("ins", "Inserted Text", Some(""), ""),
4372            builtin_component_def("sub", "Subscript", Some(""), ""),
4373            builtin_component_def("sup", "Superscript", Some(""), ""),
4374            builtin_component_def("samp", "Sample Output", Some(""), ""),
4375            builtin_component_def("kbd", "Keyboard Input", Some(""), ""),
4376            builtin_component_def("var", "Variable", Some(""), ""),
4377            builtin_component_def("cite", "Citation", Some(""), ""),
4378            builtin_component_def("dfn", "Definition", Some(""), ""),
4379            builtin_component_def("abbr", "Abbreviation", Some(""), ""),
4380            builtin_component_def("acronym", "Acronym", Some(""), ""),
4381            builtin_component_def("q", "Inline Quote", Some(""), ""),
4382            builtin_component_def("time", "Time", Some(""), ""),
4383            builtin_component_def("big", "Big", Some(""), ""),
4384            builtin_component_def("bdo", "BiDi Override", Some(""), ""),
4385            builtin_component_def("bdi", "BiDi Isolate", Some(""), ""),
4386            builtin_component_def("wbr", "Word Break Opportunity", None, ""),
4387            builtin_component_def("ruby", "Ruby Annotation", None, ""),
4388            builtin_component_def("rt", "Ruby Text", Some(""), ""),
4389            builtin_component_def("rtc", "Ruby Text Container", None, ""),
4390            builtin_component_def("rp", "Ruby Parenthesis", Some(""), ""),
4391            builtin_component_def("data", "Data", Some(""), ""),
4392            // Forms
4393            builtin_component_def("form", "Form", None, ""),
4394            builtin_component_def("fieldset", "Field Set", None, ""),
4395            builtin_component_def("legend", "Legend", Some("Legend"), ""),
4396            builtin_component_def("label", "Label", Some("Label"), ""),
4397            builtin_component_def("input", "Input", None, ""),
4398            builtin_component_def("button", "Button", Some("Button text"), ""),
4399            builtin_component_def("select", "Select", None, ""),
4400            builtin_component_def("optgroup", "Option Group", None, ""),
4401            builtin_component_def("option", "Option", Some(""), ""),
4402            builtin_component_def("textarea", "Text Area", Some(""), ""),
4403            builtin_component_def("output", "Output", Some(""), ""),
4404            builtin_component_def("progress", "Progress", None, ""),
4405            builtin_component_def("meter", "Meter", None, ""),
4406            builtin_component_def("datalist", "Data List", None, ""),
4407            // Embedded content
4408            builtin_component_def("canvas", "Canvas", None, ""),
4409            builtin_component_def("object", "Object", None, ""),
4410            builtin_component_def("param", "Parameter", None, ""),
4411            builtin_component_def("embed", "Embed", None, ""),
4412            builtin_component_def("audio", "Audio", None, ""),
4413            builtin_component_def("video", "Video", None, ""),
4414            builtin_component_def("source", "Source", None, ""),
4415            builtin_component_def("track", "Track", None, ""),
4416            builtin_component_def("map", "Image Map", None, ""),
4417            builtin_component_def("area", "Map Area", None, ""),
4418            builtin_component_def("svg", "SVG", None, ""),
4419            // Metadata
4420            builtin_component_def("meta", "Meta", None, ""),
4421            builtin_component_def("link", "Link (Resource)", None, ""),
4422            builtin_component_def("script", "Script", Some(""), ""),
4423            builtin_component_def("style", "Style", Some(""), ""),
4424            builtin_component_def("base", "Base URL", None, ""),
4425            // Structural control-flow builtins (F1-F3)
4426            builtin_if_component(),
4427            builtin_for_component(),
4428            builtin_map_component(),
4429        ]
4430        .into(),
4431    }
4432}
4433
4434// ============================================================================
4435// End new component system types
4436// ============================================================================
4437
4438/// Wrapper for the XML parser - necessary to easily create a Dom from
4439/// XML without putting an XML solver into `azul-core`.
4440#[derive(Debug, Default)]
4441pub struct DomXml {
4442    pub parsed_dom: StyledDom,
4443}
4444
4445impl DomXml {
4446    /// Convenience function, only available in tests, useful for quickly writing UI tests.
4447    /// Wraps the XML string in the required `<app></app>` braces, panics if the XML couldn't be
4448    /// parsed.
4449    ///
4450    /// ## Example
4451    ///
4452    /// ```rust,ignore
4453    /// # use azul::dom::Dom;
4454    /// # use azul::xml::DomXml;
4455    /// let dom = DomXml::mock("<div id='test' />");
4456    /// dom.assert_eq(Dom::create_div().with_id("test"));
4457    /// ```
4458    ///
4459    /// # Panics
4460    ///
4461    /// Panics if the rendered DOM does not equal `other` (this is a test-only
4462    /// assertion helper).
4463    #[cfg(test)]
4464    pub fn assert_eq(self, other: StyledDom) {
4465        let mut body = Dom::create_body();
4466        let mut fixed = StyledDom::create(&mut body, Css::empty());
4467        fixed.append_child(other);
4468        assert!(!(self.parsed_dom != fixed), 
4469                "\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: \
4470                 ----------\r\n{}\r\n",
4471                self.parsed_dom.get_html_string("", "", true),
4472                fixed.get_html_string("", "", true)
4473            );
4474    }
4475
4476    #[must_use] pub fn into_styled_dom(self) -> StyledDom {
4477        self.into()
4478    }
4479}
4480
4481impl From<DomXml> for StyledDom {
4482    fn from(val: DomXml) -> Self {
4483        val.parsed_dom
4484    }
4485}
4486
4487/// Represents a child of an XML node - either an element or text
4488#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4489#[repr(C, u8)]
4490pub enum XmlNodeChild {
4491    /// A text node
4492    Text(AzString),
4493    /// An element node
4494    Element(XmlNode),
4495}
4496
4497impl_option!(
4498    XmlNodeChild,
4499    OptionXmlNodeChild,
4500    copy = false,
4501    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4502);
4503
4504impl XmlNodeChild {
4505    /// Get the text content if this is a text node
4506    #[must_use] pub fn as_text(&self) -> Option<&str> {
4507        match self {
4508            Self::Text(s) => Some(s.as_str()),
4509            Self::Element(_) => None,
4510        }
4511    }
4512
4513    /// Get the element if this is an element node
4514    #[must_use] pub const fn as_element(&self) -> Option<&XmlNode> {
4515        match self {
4516            Self::Text(_) => None,
4517            Self::Element(node) => Some(node),
4518        }
4519    }
4520
4521    /// Get the element mutably if this is an element node
4522    pub const fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
4523        match self {
4524            Self::Text(_) => None,
4525            Self::Element(node) => Some(node),
4526        }
4527    }
4528}
4529
4530impl_vec!(
4531    XmlNodeChild,
4532    XmlNodeChildVec,
4533    XmlNodeChildVecDestructor,
4534    XmlNodeChildVecDestructorType,
4535    XmlNodeChildVecSlice,
4536    OptionXmlNodeChild
4537);
4538impl_vec_mut!(XmlNodeChild, XmlNodeChildVec);
4539impl_vec_debug!(XmlNodeChild, XmlNodeChildVec);
4540impl_vec_partialeq!(XmlNodeChild, XmlNodeChildVec);
4541impl_vec_eq!(XmlNodeChild, XmlNodeChildVec);
4542impl_vec_partialord!(XmlNodeChild, XmlNodeChildVec);
4543impl_vec_ord!(XmlNodeChild, XmlNodeChildVec);
4544impl_vec_hash!(XmlNodeChild, XmlNodeChildVec);
4545impl_vec_clone!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor);
4546
4547/// Represents one XML node tag
4548#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4549#[repr(C)]
4550pub struct XmlNode {
4551    /// Type of the node
4552    pub node_type: XmlTagName,
4553    /// Attributes of an XML node (note: not yet filtered and / or broken into function arguments!)
4554    pub attributes: XmlAttributeMap,
4555    /// Direct children of this node (can be text or element nodes)
4556    pub children: XmlNodeChildVec,
4557}
4558
4559impl_option!(
4560    XmlNode,
4561    OptionXmlNode,
4562    copy = false,
4563    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4564);
4565
4566impl XmlNode {
4567    pub fn create<I: Into<XmlTagName>>(node_type: I) -> Self {
4568        Self {
4569            node_type: node_type.into(),
4570            ..Default::default()
4571        }
4572    }
4573    #[must_use] pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
4574        Self {
4575            children: v.into(),
4576            ..self
4577        }
4578    }
4579
4580    /// Get all text content concatenated from direct children
4581    #[must_use] pub fn get_text_content(&self) -> String {
4582        self.children
4583            .as_ref()
4584            .iter()
4585            .filter_map(|child| child.as_text())
4586            .collect::<Vec<_>>()
4587            .join("")
4588    }
4589
4590    /// Check if this node has only text children (no element children)
4591    #[must_use] pub fn has_only_text_children(&self) -> bool {
4592        self.children
4593            .as_ref()
4594            .iter()
4595            .all(|child| matches!(child, XmlNodeChild::Text(_)))
4596    }
4597}
4598
4599impl_vec!(
4600    XmlNode,
4601    XmlNodeVec,
4602    XmlNodeVecDestructor,
4603    XmlNodeVecDestructorType,
4604    XmlNodeVecSlice,
4605    OptionXmlNode
4606);
4607impl_vec_mut!(XmlNode, XmlNodeVec);
4608impl_vec_debug!(XmlNode, XmlNodeVec);
4609impl_vec_partialeq!(XmlNode, XmlNodeVec);
4610impl_vec_eq!(XmlNode, XmlNodeVec);
4611impl_vec_partialord!(XmlNode, XmlNodeVec);
4612impl_vec_ord!(XmlNode, XmlNodeVec);
4613impl_vec_hash!(XmlNode, XmlNodeVec);
4614impl_vec_clone!(XmlNode, XmlNodeVec, XmlNodeVecDestructor);
4615
4616#[derive(Debug, Clone, PartialEq)]
4617#[repr(C, u8)]
4618pub enum DomXmlParseError {
4619    /// No `<html></html>` node component present
4620    NoHtmlNode,
4621    /// Multiple `<html>` nodes
4622    MultipleHtmlRootNodes,
4623    /// No ´<body></body>´ node in the root HTML
4624    NoBodyInHtml,
4625    /// The DOM can only have one <body> node, not multiple.
4626    MultipleBodyNodes,
4627    /// Note: Sadly, the error type can only be a string because xmlparser
4628    /// returns all errors as strings. There is an open PR to fix
4629    /// this deficiency, but since the XML parsing is only needed for
4630    /// hot-reloading and compiling, it doesn't matter that much.
4631    Xml(XmlError),
4632    /// Invalid hierarchy close tags, i.e `<app></p></app>`
4633    MalformedHierarchy(MalformedHierarchyError),
4634    /// A component raised an error while rendering the DOM - holds the component name + error
4635    /// string
4636    RenderDom(RenderDomError),
4637    /// Something went wrong while parsing an XML component
4638    Component(ComponentParseError),
4639    /// Error parsing global CSS in head node
4640    Css(CssParseErrorOwned),
4641}
4642
4643impl From<XmlError> for DomXmlParseError {
4644    fn from(e: XmlError) -> Self {
4645        Self::Xml(e)
4646    }
4647}
4648
4649impl From<ComponentParseError> for DomXmlParseError {
4650    fn from(e: ComponentParseError) -> Self {
4651        Self::Component(e)
4652    }
4653}
4654
4655impl From<RenderDomError> for DomXmlParseError {
4656    fn from(e: RenderDomError) -> Self {
4657        Self::RenderDom(e)
4658    }
4659}
4660
4661impl From<CssParseErrorOwned> for DomXmlParseError {
4662    fn from(e: CssParseErrorOwned) -> Self {
4663        Self::Css(e)
4664    }
4665}
4666
4667/// Error that can happen from the translation from XML code to Rust code -
4668/// stringified, since it is only used for printing and is not exposed in the public API
4669#[derive(Debug, Clone, PartialEq)]
4670#[repr(C, u8)]
4671pub enum CompileError {
4672    Dom(RenderDomError),
4673    Xml(DomXmlParseError),
4674    Css(CssParseErrorOwned),
4675}
4676
4677impl From<ComponentError> for CompileError {
4678    fn from(e: ComponentError) -> Self {
4679        Self::Dom(RenderDomError::Component(e))
4680    }
4681}
4682
4683impl From<CssParseErrorOwned> for CompileError {
4684    fn from(e: CssParseErrorOwned) -> Self {
4685        Self::Css(e)
4686    }
4687}
4688
4689impl fmt::Display for CompileError {
4690    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4691        use self::CompileError::{Dom, Xml, Css};
4692        match self {
4693            Dom(d) => write!(f, "{d}"),
4694            Xml(s) => write!(f, "{s}"),
4695            Css(s) => write!(f, "{}", s.to_shared()),
4696        }
4697    }
4698}
4699
4700impl From<RenderDomError> for CompileError {
4701    fn from(e: RenderDomError) -> Self {
4702        Self::Dom(e)
4703    }
4704}
4705
4706impl From<DomXmlParseError> for CompileError {
4707    fn from(e: DomXmlParseError) -> Self {
4708        Self::Xml(e)
4709    }
4710}
4711
4712/// Wrapper for `UselessFunctionArgument` error data.
4713#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4714#[repr(C)]
4715pub struct UselessFunctionArgumentError {
4716    pub component_name: AzString,
4717    pub argument_name: AzString,
4718    pub valid_args: StringVec,
4719}
4720
4721#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4722#[repr(C, u8)]
4723pub enum ComponentError {
4724    /// While instantiating a component, a function argument
4725    /// was encountered that the component won't use or react to.
4726    UselessFunctionArgument(UselessFunctionArgumentError),
4727    /// A certain node type can't be rendered, because the
4728    /// renderer for this node is not available isn't available
4729    ///
4730    /// `UnknownComponent(component_name)`
4731    UnknownComponent(AzString),
4732}
4733
4734#[derive(Debug, Clone, PartialEq)]
4735#[repr(C, u8)]
4736pub enum RenderDomError {
4737    Component(ComponentError),
4738    /// Error parsing the CSS on the component style
4739    CssError(CssParseErrorOwned),
4740}
4741
4742impl From<ComponentError> for RenderDomError {
4743    fn from(e: ComponentError) -> Self {
4744        Self::Component(e)
4745    }
4746}
4747
4748impl From<CssParseErrorOwned> for RenderDomError {
4749    fn from(e: CssParseErrorOwned) -> Self {
4750        Self::CssError(e)
4751    }
4752}
4753
4754/// Wrapper for `MissingType` error data.
4755#[derive(Debug, Clone, PartialEq, Eq)]
4756#[repr(C)]
4757pub struct MissingTypeError {
4758    pub arg_pos: usize,
4759    pub arg_name: AzString,
4760}
4761
4762/// Wrapper for `WhiteSpaceInComponentName` error data.
4763#[derive(Debug, Clone, PartialEq, Eq)]
4764#[repr(C)]
4765pub struct WhiteSpaceInComponentNameError {
4766    pub arg_pos: usize,
4767    pub arg_name: AzString,
4768}
4769
4770/// Wrapper for `WhiteSpaceInComponentType` error data.
4771#[derive(Debug, Clone, PartialEq, Eq)]
4772#[repr(C)]
4773pub struct WhiteSpaceInComponentTypeError {
4774    pub arg_pos: usize,
4775    pub arg_name: AzString,
4776    pub arg_type: AzString,
4777}
4778
4779#[derive(Debug, Clone, PartialEq)]
4780#[repr(C, u8)]
4781pub enum ComponentParseError {
4782    /// Given `XmlNode` is not a `<component />` node.
4783    NotAComponent,
4784    /// A `<component>` node does not have a `name` attribute.
4785    UnnamedComponent,
4786    /// Argument at position `usize` is either empty or has no name
4787    MissingName(usize),
4788    /// Argument at position `usize` with the name
4789    /// `String` doesn't have a `: type`
4790    MissingType(MissingTypeError),
4791    /// Component name may not contain a whitespace
4792    /// (probably missing a `:` between the name and the type)
4793    WhiteSpaceInComponentName(WhiteSpaceInComponentNameError),
4794    /// Component type may not contain a whitespace
4795    /// (probably missing a `,` between the type and the next name)
4796    WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError),
4797    /// Error parsing the <style> tag / CSS
4798    CssError(CssParseErrorOwned),
4799}
4800
4801impl fmt::Display for DomXmlParseError {
4802    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4803        use self::DomXmlParseError::{NoHtmlNode, MultipleHtmlRootNodes, NoBodyInHtml, MultipleBodyNodes, Xml, MalformedHierarchy, RenderDom, Component, Css};
4804        match self {
4805            NoHtmlNode => write!(
4806                f,
4807                "No <html> node found as the root of the file - empty file?"
4808            ),
4809            MultipleHtmlRootNodes => write!(
4810                f,
4811                "Multiple <html> nodes found as the root of the file - only one root node allowed"
4812            ),
4813            NoBodyInHtml => write!(
4814                f,
4815                "No <body> node found as a direct child of an <html> node - malformed DOM \
4816                 hierarchy?"
4817            ),
4818            MultipleBodyNodes => write!(
4819                f,
4820                "Multiple <body> nodes present, only one <body> node is allowed"
4821            ),
4822            Xml(e) => write!(f, "Error parsing XML: {e}"),
4823            MalformedHierarchy(e) => write!(
4824                f,
4825                "Invalid </{}> tag: expected </{}>",
4826                e.got.as_str(),
4827                e.expected.as_str()
4828            ),
4829            RenderDom(e) => write!(f, "Error rendering DOM: {e}"),
4830            Component(c) => write!(f, "Error parsing component in <head> node:\r\n{c}"),
4831            Css(c) => write!(f, "Error parsing CSS in <head> node:\r\n{}", c.to_shared()),
4832        }
4833    }
4834}
4835
4836impl fmt::Display for ComponentParseError {
4837    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4838        use self::ComponentParseError::{NotAComponent, UnnamedComponent, MissingName, MissingType, WhiteSpaceInComponentName, WhiteSpaceInComponentType, CssError};
4839        match self {
4840            NotAComponent => write!(f, "Expected <component/> node, found no such node"),
4841            UnnamedComponent => write!(
4842                f,
4843                "Found <component/> tag with out a \"name\" attribute, component must have a name"
4844            ),
4845            MissingName(arg_pos) => write!(
4846                f,
4847                "Argument at position {arg_pos} is either empty or has no name"
4848            ),
4849            MissingType(e) => write!(
4850                f,
4851                "Argument \"{}\" at position {} doesn't have a `: type`",
4852                e.arg_name, e.arg_pos
4853            ),
4854            WhiteSpaceInComponentName(e) => {
4855                write!(
4856                    f,
4857                    "Missing `:` between the name and the type in argument {} (around \"{}\")",
4858                    e.arg_pos, e.arg_name
4859                )
4860            }
4861            WhiteSpaceInComponentType(e) => {
4862                write!(
4863                    f,
4864                    "Missing `,` between two arguments (in argument {}, position {}, around \
4865                     \"{}\")",
4866                    e.arg_name, e.arg_pos, e.arg_type
4867                )
4868            }
4869            CssError(lsf) => write!(f, "Error parsing <style> tag: {}", lsf.to_shared()),
4870        }
4871    }
4872}
4873
4874impl fmt::Display for ComponentError {
4875    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4876        use self::ComponentError::{UselessFunctionArgument, UnknownComponent};
4877        match self {
4878            UselessFunctionArgument(e) => {
4879                write!(
4880                    f,
4881                    "Useless component argument \"{}\": \"{}\" - available args are: {:#?}",
4882                    e.component_name, e.argument_name, e.valid_args
4883                )
4884            }
4885            UnknownComponent(name) => write!(f, "Unknown component: \"{name}\""),
4886        }
4887    }
4888}
4889
4890impl fmt::Display for RenderDomError {
4891    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4892        use self::RenderDomError::{Component, CssError};
4893        match self {
4894            Component(c) => write!(f, "{c}"),
4895            CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
4896        }
4897    }
4898}
4899
4900/// Find the one and only `<body>` node, return error if
4901/// there is no app node or there are multiple app nodes
4902#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
4903/// # Errors
4904///
4905/// Returns an error if the document has no `<html>` root node.
4906pub fn get_html_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
4907    let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
4908        if let XmlNodeChild::Element(node) = child {
4909            let node_type_normalized = normalize_casing(&node.node_type);
4910            if &node_type_normalized == "html" {
4911                Some(node)
4912            } else {
4913                None
4914            }
4915        } else {
4916            None
4917        }
4918    });
4919
4920    let html_node = html_node_iterator
4921        .next()
4922        .ok_or(DomXmlParseError::NoHtmlNode)?;
4923    if html_node_iterator.next().is_some() {
4924        Err(DomXmlParseError::MultipleHtmlRootNodes)
4925    } else {
4926        Ok(html_node)
4927    }
4928}
4929
4930/// Find the one and only `<body>` node, return error if
4931/// there is no app node or there are multiple app nodes
4932#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
4933/// # Errors
4934///
4935/// Returns an error if the document has no `<body>` node.
4936pub fn get_body_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
4937    fn find_body_recursive(nodes: &[XmlNodeChild], depth: usize) -> Option<&XmlNode> {
4938        // AUDIT 2026-07-08: bound recursion depth to avoid a stack overflow on
4939        // pathologically deep markup while hunting for the <body> element.
4940        if depth > MAX_XML_NESTING_DEPTH {
4941            return None;
4942        }
4943        for child in nodes {
4944            if let XmlNodeChild::Element(node) = child {
4945                let node_type_normalized = normalize_casing(&node.node_type);
4946                if &node_type_normalized == "body" {
4947                    return Some(node);
4948                }
4949                // Recurse into children
4950                if let Some(found) = find_body_recursive(node.children.as_ref(), depth + 1) {
4951                    return Some(found);
4952                }
4953            }
4954        }
4955        None
4956    }
4957
4958    // First try to find body as a direct child (proper HTML structure)
4959    let direct_body = root_nodes.iter().find_map(|child| {
4960        if let XmlNodeChild::Element(node) = child {
4961            let node_type_normalized = normalize_casing(&node.node_type);
4962            if &node_type_normalized == "body" {
4963                Some(node)
4964            } else {
4965                None
4966            }
4967        } else {
4968            None
4969        }
4970    });
4971
4972    if let Some(body) = direct_body {
4973        return Ok(body);
4974    }
4975
4976    // If not found as direct child, search recursively (for malformed HTML like example.com)
4977    // where <body> might be nested inside <head> due to missing </head> tag
4978    find_body_recursive(root_nodes, 0).ok_or(DomXmlParseError::NoBodyInHtml)
4979}
4980
4981/// Searches in the the `root_nodes` for a `node_type`, convenience function in order to
4982/// for example find the first <blah /> node in all these nodes.
4983/// This function searches recursively through the entire tree.
4984fn find_node_by_type<'a>(root_nodes: &'a [XmlNodeChild], node_type: &str) -> Option<&'a XmlNode> {
4985    // First check direct children
4986    for child in root_nodes {
4987        if let XmlNodeChild::Element(node) = child {
4988            if normalize_casing(&node.node_type).as_str() == node_type {
4989                return Some(node);
4990            }
4991        }
4992    }
4993
4994    // If not found, search recursively (for malformed HTML)
4995    for child in root_nodes {
4996        if let XmlNodeChild::Element(node) = child {
4997            if let Some(found) = find_node_by_type(node.children.as_ref(), node_type) {
4998                return Some(found);
4999            }
5000        }
5001    }
5002
5003    None
5004}
5005
5006#[must_use] pub fn find_attribute<'a>(node: &'a XmlNode, attribute: &str) -> Option<&'a AzString> {
5007    node.attributes
5008        .iter()
5009        .find(|n| normalize_casing(n.key.as_str()).as_str() == attribute)
5010        .map(|s| &s.value)
5011}
5012
5013/// Normalizes input such as `abcDef`, `AbcDef`, `abc-def` to the normalized form of `abc_def`
5014#[must_use] pub fn normalize_casing(input: &str) -> String {
5015    let mut words: Vec<String> = Vec::new();
5016    let mut cur_str = Vec::new();
5017
5018    for ch in input.chars() {
5019        if ch.is_uppercase() || ch == '_' || ch == '-' {
5020            if !cur_str.is_empty() {
5021                words.push(cur_str.iter().collect());
5022                cur_str.clear();
5023            }
5024            if ch.is_uppercase() {
5025                cur_str.extend(ch.to_lowercase());
5026            }
5027        } else {
5028            cur_str.extend(ch.to_lowercase());
5029        }
5030    }
5031
5032    if !cur_str.is_empty() {
5033        words.push(cur_str.iter().collect());
5034        cur_str.clear();
5035    }
5036
5037    words.join("_")
5038}
5039
5040/// Given a root node, traverses along the hierarchy, and returns a
5041/// mutable reference to the last child node of the root node
5042#[allow(trivial_casts)]
5043pub fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> {
5044    let mut hierarchy = hierarchy.to_vec();
5045    hierarchy.reverse();
5046    let Some(item) = hierarchy.pop() else {
5047        return Some(root_node);
5048    };
5049    let child = root_node.children.as_mut().get_mut(item)?;
5050    match child {
5051        XmlNodeChild::Element(node) => get_item_internal(&mut hierarchy, node),
5052        XmlNodeChild::Text(_) => None, // Can't traverse into text nodes
5053    }
5054}
5055
5056fn get_item_internal<'a>(
5057    hierarchy: &mut Vec<usize>,
5058    root_node: &'a mut XmlNode,
5059) -> Option<&'a mut XmlNode> {
5060    if hierarchy.is_empty() {
5061        return Some(root_node);
5062    }
5063    let Some(cur_item) = hierarchy.pop() else {
5064        return Some(root_node);
5065    };
5066    let child = root_node.children.as_mut().get_mut(cur_item)?;
5067    match child {
5068        XmlNodeChild::Element(node) => get_item_internal(hierarchy, node),
5069        XmlNodeChild::Text(_) => None, // Can't traverse into text nodes
5070    }
5071}
5072
5073/// Parses an XML string and returns a `StyledDom` with the components instantiated in the
5074/// `<app></app>`
5075#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5076/// # Errors
5077///
5078/// Returns an error if the XML cannot be parsed into a DOM (malformed markup or an unknown component).
5079pub fn str_to_dom<'a>(
5080    root_nodes: &'a [XmlNodeChild],
5081    component_map: &'a ComponentMap,
5082    max_width: Option<f32>,
5083) -> Result<StyledDom, DomXmlParseError> {
5084    // Delegate to the fast path (Dom::Fast / CompactDom arena).
5085    str_to_dom_fast(root_nodes, component_map, max_width)
5086}
5087
5088/// Parse XML to `StyledDom` via arena-based `FastDom` (no tree intermediary).
5089///
5090/// **Note**: `str_to_dom()` now delegates to this function, so you can use
5091/// either one. This function is kept for backward compatibility.
5092#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5093fn str_to_dom_fast<'a>(
5094    root_nodes: &'a [XmlNodeChild],
5095    component_map: &'a ComponentMap,
5096    max_width: Option<f32>,
5097) -> Result<StyledDom, DomXmlParseError> {
5098    let html_node = get_html_node(root_nodes)?;
5099    let body_node = get_body_node(html_node.children.as_ref())?;
5100
5101    let mut global_style = None;
5102
5103    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5104        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5105            let text = style_node.get_text_content();
5106            if !text.is_empty() {
5107                let parsed_css = Css::from_string(text.into());
5108                global_style = Some(parsed_css);
5109            }
5110        }
5111    }
5112
5113    render_dom_from_body_node_fast(body_node, global_style, component_map, max_width)
5114        .map_err(Into::into)
5115}
5116
5117/// Parses XML nodes and returns a `Dom` with CSS stylesheets attached (but not applied).
5118///
5119/// Unlike `str_to_dom` which returns a fully styled `StyledDom`, this function
5120/// returns an unstyled `Dom` whose `css` field carries the parsed `<style>` rules.
5121/// The layout framework will apply the CSS during the cascade pass.
5122///
5123/// This is the correct function for building a `Dom` from XML in layout callbacks
5124/// (which must return `Dom`, not `StyledDom`).
5125#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5126/// # Errors
5127///
5128/// Returns an error if the XML cannot be parsed into a DOM (malformed markup or an unknown component).
5129pub fn str_to_dom_unstyled<'a>(
5130    root_nodes: &'a [XmlNodeChild],
5131    component_map: &'a ComponentMap,
5132) -> Result<Dom, DomXmlParseError> {
5133    let html_node = get_html_node(root_nodes)?;
5134    let body_node = get_body_node(html_node.children.as_ref())?;
5135
5136    let mut global_style = None;
5137
5138    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5139        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5140            let text = style_node.get_text_content();
5141            if !text.is_empty() {
5142                let parsed_css = Css::from_string(text.into());
5143                global_style = Some(parsed_css);
5144            }
5145        }
5146    }
5147
5148    // Build the DOM tree from the body node
5149    let body_dom = xml_node_to_dom_fast(body_node, component_map, false, 0)
5150        .map_err(DomXmlParseError::from)?;
5151
5152    // Wrap in proper HTML structure (NodeType is imported at module top)
5153    let root_node_type = body_dom.root.node_type.clone();
5154
5155    let mut full_dom = match root_node_type {
5156        NodeType::Html => body_dom,
5157        NodeType::Body => Dom::create_html().with_child(body_dom),
5158        _ => {
5159            let body_wrapper = Dom::create_body().with_child(body_dom);
5160            Dom::create_html().with_child(body_wrapper)
5161        }
5162    };
5163
5164    // Attach CSS to the Dom's css field instead of applying it immediately
5165    if let Some(css) = global_style {
5166        full_dom.css = alloc::vec![css].into();
5167    }
5168
5169    Ok(full_dom)
5170}
5171
5172/// Parses an XML string and returns a `String`, which contains the Rust source code
5173/// (i.e. it compiles the XML to valid Rust)
5174#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5175/// # Errors
5176///
5177/// Returns an error if the XML cannot be parsed or compiled to Rust code.
5178pub fn str_to_rust_code<'a>(
5179    root_nodes: &'a [XmlNodeChild],
5180    imports: &str,
5181    component_map: &'a ComponentMap,
5182) -> Result<String, CompileError> {
5183    let html_node = get_html_node(root_nodes)?;
5184    let body_node = get_body_node(html_node.children.as_ref())?;
5185    let mut global_style = Css::empty();
5186
5187    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5188        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5189            let text = style_node.get_text_content();
5190            if !text.is_empty() {
5191                let parsed_css = azul_css::parser2::new_from_str(&text).0;
5192                global_style = parsed_css;
5193            }
5194        }
5195    }
5196
5197    global_style.sort_by_specificity();
5198
5199    let mut css_blocks = BTreeMap::new();
5200    let mut extra_blocks = VecContents::default();
5201    let app_source = compile_body_node_to_rust_code(
5202        body_node,
5203        component_map,
5204        &mut extra_blocks,
5205        &mut css_blocks,
5206        &global_style,
5207        CssMatcher {
5208            path: Vec::new(),
5209            indices_in_parent: vec![0],
5210            children_length: vec![body_node.children.as_ref().len()],
5211        },
5212    )?;
5213
5214    let app_source = app_source
5215        .lines()
5216        .map(|l| format!("        {l}"))
5217        .collect::<Vec<String>>()
5218        .join("\r\n");
5219
5220    // NOTE: `css_blocks` / `extra_blocks` are no longer emitted — per-node styles
5221    // are now inlined as `.with_css("..")` strings (public API) rather than as
5222    // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` blocks (that API was
5223    // removed in 32d44ed8a). The maps stay in the signatures for compatibility.
5224    let _ = (&css_blocks, &extra_blocks);
5225
5226    let main_func = "
5227
5228use azul::{
5229    app::{App, AppConfig},
5230    dom::Dom,
5231    callbacks::{RefAny, LayoutCallbackInfo},
5232    window::WindowCreateOptions,
5233};
5234
5235struct Data { }
5236
5237extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> Dom {
5238    crate::ui::render()
5239}
5240
5241fn main() {
5242    let config = AppConfig::create();
5243    let app = App::create(RefAny::new(Data { }), config);
5244    let window = WindowCreateOptions::create(render);
5245    app.run(window);
5246}";
5247
5248    let ui_module = format!(
5249        "#[allow(unused_imports)]\r\npub mod ui {{
5250
5251    use azul::prelude::*;
5252    use azul::dom::{{NodeType, TabIndex, SmallAriaInfo}};
5253    use azul::str::String as AzString;
5254
5255    pub fn render() -> Dom {{\r\n{app_source}\r\n    }}\r\n}}"
5256    );
5257    let source_code = format!(
5258        "#![windows_subsystem = \"windows\"]\r\n//! Auto-generated UI source \
5259         code\r\n{}\r\n{}\r\n\r\n{}{}",
5260        imports,
5261        compile_components(Vec::new()), // no user-defined components to compile
5262        ui_module,
5263        main_func,
5264    );
5265
5266    Ok(source_code)
5267}
5268
5269// Compile all components to source code
5270#[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
5271fn compile_components(
5272    components: Vec<(
5273        ComponentName,
5274        CompiledComponent,
5275        ComponentArguments,
5276        BTreeMap<String, String>,
5277    )>,
5278) -> String {
5279    let cs = components
5280        .iter()
5281        .map(|(name, function_body, function_args, css_blocks)| {
5282            let name = &normalize_casing(name);
5283            let f = compile_component(name, function_args, function_body)
5284                .lines()
5285                .map(|l| format!("    {l}"))
5286                .collect::<Vec<String>>()
5287                .join("\r\n");
5288
5289            // let css_blocks = ...
5290
5291            format!(
5292                "#[allow(unused_imports)]\r\npub mod {name} {{\r\n    use azul::dom::Dom;\r\n    use \
5293                 azul::str::String as AzString;\r\n{f}\r\n}}"
5294            )
5295        })
5296        .collect::<Vec<String>>()
5297        .join("\r\n\r\n");
5298
5299    let cs = cs
5300        .lines()
5301        .map(|l| format!("    {l}"))
5302        .collect::<Vec<String>>()
5303        .join("\r\n");
5304
5305    if cs.is_empty() {
5306        cs
5307    } else {
5308        format!("pub mod components {{\r\n{cs}\r\n}}")
5309    }
5310}
5311
5312fn format_component_args(component_args: &ComponentArgumentVec) -> String {
5313    let mut args = component_args
5314        .iter()
5315        .map(|a| format!("{}: {}", a.name, a.arg_type))
5316        .collect::<Vec<String>>();
5317
5318    args.sort_by(|a, b| b.cmp(a));
5319
5320    args.join(", ")
5321}
5322
5323#[must_use] pub fn compile_component(
5324    component_name: &str,
5325    component_args: &ComponentArguments,
5326    component_function_body: &str,
5327) -> String {
5328    let component_name = &normalize_casing(component_name);
5329    let function_args = format_component_args(&component_args.args);
5330    let component_function_body = component_function_body
5331        .lines()
5332        .map(|l| format!("    {l}"))
5333        .collect::<Vec<String>>()
5334        .join("\r\n");
5335    let should_inline = component_function_body.lines().count() == 1;
5336    format!(
5337        "{}pub fn render({}{}{}) -> Dom {{\r\n{}\r\n}}",
5338        if should_inline { "#[inline]\r\n" } else { "" },
5339        // pass the text content as the first
5340        if component_args.accepts_text {
5341            "text: AzString"
5342        } else {
5343            ""
5344        },
5345        if function_args.is_empty() || !component_args.accepts_text {
5346            ""
5347        } else {
5348            ", "
5349        },
5350        function_args,
5351        component_function_body,
5352    )
5353}
5354
5355/// Parse an SVG numeric attribute value to f32.
5356fn parse_svg_float(attr: Option<&AzString>) -> Option<f32> {
5357    attr?.as_str().trim().parse::<f32>().ok()
5358}
5359
5360/// Parse an SVG `points` attribute (used by `<polygon>` and `<polyline>`).
5361fn parse_svg_points(pts: &str, close: bool) -> Option<crate::svg::SvgMultiPolygon> {
5362    let nums: Vec<f32> = pts
5363        .split(|c: char| c == ',' || c.is_ascii_whitespace())
5364        .filter(|s| !s.is_empty())
5365        .filter_map(|s| s.parse::<f32>().ok())
5366        .collect();
5367    if nums.len() < 4 || !nums.len().is_multiple_of(2) {
5368        return None;
5369    }
5370    let mut elements = Vec::new();
5371    let points: Vec<azul_css::props::basic::SvgPoint> = nums
5372        .chunks_exact(2)
5373        .map(|c| azul_css::props::basic::SvgPoint { x: c[0], y: c[1] })
5374        .collect();
5375    for w in points.windows(2) {
5376        elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5377            w[0], w[1],
5378        )));
5379    }
5380    if close && points.len() >= 2 {
5381        let first = points[0];
5382        let last = *points.last().unwrap();
5383        if (first.x - last.x).abs() > 0.001 || (first.y - last.y).abs() > 0.001 {
5384            elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5385                last, first,
5386            )));
5387        }
5388    }
5389    Some(crate::svg::SvgMultiPolygon {
5390        rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5391            items: crate::svg::SvgPathElementVec::from_vec(elements),
5392        }]),
5393    })
5394}
5395
5396/// Fast XML to Dom conversion that builds Dom tree directly without intermediate `StyledDom`
5397/// This is O(n) instead of O(n²) for large documents
5398/// Apply the shared set of XML attributes onto a single [`NodeData`] node.
5399///
5400/// Handles `<img src>` rebuild, `id`/`class`, `focusable`, `tabindex`, inline
5401/// `style`, and SVG-shape geometry — the block that was previously duplicated
5402/// verbatim between [`xml_node_to_dom_fast`] (operating on `dom.root`) and
5403/// [`xml_node_to_fast_dom`] (operating on the arena `NodeData`). `component_name`
5404/// must already be normalized (lowercased); the caller computes `child_inside_svg`.
5405#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
5406fn apply_xml_node_attributes(
5407    node: &mut crate::dom::NodeData,
5408    xml_node: &XmlNode,
5409    component_name: &str,
5410    inside_svg: bool,
5411) {
5412    use crate::dom::{IdOrClass, NodeType, TabIndex};
5413
5414    // `<img src="...">`: rebuild the placeholder Image node so its `NullImage`
5415    // carries the `src` string (as UTF-8 bytes in `tag`). The bytes are NOT
5416    // resolved here — a downstream renderer (printpdf, the compositor, ...) uses
5417    // the tag to look up and embed the actual image. Optional `width`/`height`
5418    // attributes set the intrinsic size used for layout (CSS still overrides).
5419    if component_name == "img" {
5420        if let Some(src) = xml_node.attributes.get_key("src") {
5421            let width = xml_node
5422                .attributes
5423                .get_key("width")
5424                .and_then(|w| {
5425                    w.as_str()
5426                        .trim()
5427                        .trim_end_matches("px")
5428                        .trim()
5429                        .parse::<usize>()
5430                        .ok()
5431                })
5432                .unwrap_or(0);
5433            let height = xml_node
5434                .attributes
5435                .get_key("height")
5436                .and_then(|h| {
5437                    h.as_str()
5438                        .trim()
5439                        .trim_end_matches("px")
5440                        .trim()
5441                        .parse::<usize>()
5442                        .ok()
5443                })
5444                .unwrap_or(0);
5445            let image_ref = crate::resources::ImageRef::null_image(
5446                width,
5447                height,
5448                crate::resources::RawImageFormat::RGBA8,
5449                src.as_str().as_bytes().to_vec(),
5450            );
5451            node
5452                .set_node_type(NodeType::Image(azul_css::css::BoxOrStatic::heap(image_ref)));
5453        }
5454    }
5455
5456    // Set id and class attributes
5457    let mut ids_and_classes = Vec::new();
5458    if let Some(id_str) = xml_node.attributes.get_key("id") {
5459        for id in id_str.split_whitespace() {
5460            ids_and_classes.push(IdOrClass::Id(id.into()));
5461        }
5462    }
5463    if let Some(class_str) = xml_node.attributes.get_key("class") {
5464        for class in class_str.split_whitespace() {
5465            ids_and_classes.push(IdOrClass::Class(class.into()));
5466        }
5467    }
5468    if !ids_and_classes.is_empty() {
5469        node.set_ids_and_classes(ids_and_classes.into());
5470    }
5471
5472    // Handle focusable attribute
5473    if let Some(focusable) = xml_node
5474        .attributes
5475        .get_key("focusable")
5476        .and_then(|f| parse_bool(f.as_str()))
5477    {
5478        if focusable { node.set_tab_index(TabIndex::Auto) } else { node.set_tab_index(TabIndex::NoKeyboardFocus) }
5479    }
5480
5481    // Handle tabindex attribute
5482    if let Some(tab_index) = xml_node
5483        .attributes
5484        .get_key("tabindex")
5485        .and_then(|val| val.parse::<isize>().ok())
5486    {
5487        match tab_index {
5488            0 => node.set_tab_index(TabIndex::Auto),
5489            i if i > 0 => node.set_tab_index(TabIndex::OverrideInParent(u32::try_from(i).unwrap_or(u32::MAX))),
5490            _ => node.set_tab_index(TabIndex::NoKeyboardFocus),
5491        }
5492    }
5493
5494    // Handle inline style attribute
5495    if let Some(style) = xml_node.attributes.get_key("style") {
5496        let css_key_map = azul_css::props::property::get_css_key_map();
5497        let mut attributes = Vec::new();
5498        for s in style.as_str().split(';') {
5499            let mut s = s.split(':');
5500            let Some(key) = s.next() else {
5501                continue;
5502            };
5503            let Some(value) = s.next() else {
5504                continue;
5505            };
5506            // Called for its side effect (writes parsed props into `attributes`);
5507            // the returned value is intentionally discarded.
5508            drop(azul_css::parser2::parse_css_declaration(
5509                key.trim(),
5510                value.trim(),
5511                azul_css::parser2::ErrorLocationRange::default(),
5512                &css_key_map,
5513                &mut Vec::new(),
5514                &mut attributes,
5515            ));
5516        }
5517        let props = attributes
5518            .into_iter()
5519            .filter_map(|s| {
5520                use azul_css::dynamic_selector::CssPropertyWithConditions;
5521                match s {
5522                    CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
5523                    CssDeclaration::Dynamic(_) => None,
5524                }
5525            })
5526            .collect::<Vec<_>>();
5527        if !props.is_empty() {
5528            node.set_css_props(props.into());
5529        }
5530    }
5531
5532    // Handle SVG shape elements when inside an <svg> context
5533    let tag = component_name;
5534    let is_svg_shape = inside_svg
5535        && matches!(
5536            tag,
5537            "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline"
5538        );
5539
5540    if is_svg_shape {
5541        let clip = match tag {
5542            "path" => xml_node
5543                .attributes
5544                .get_key("d")
5545                .and_then(|d| crate::path_parser::parse_svg_path_d(d.as_str()).ok()),
5546            "circle" => {
5547                let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
5548                let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
5549                let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
5550                if r > 0.0 {
5551                    Some(crate::svg::SvgMultiPolygon {
5552                        rings: crate::svg::SvgPathVec::from_vec(vec![
5553                            crate::path_parser::svg_circle_to_paths(cx, cy, r),
5554                        ]),
5555                    })
5556                } else {
5557                    None
5558                }
5559            }
5560            "rect" => {
5561                let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
5562                let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
5563                let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
5564                let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
5565                let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
5566                let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
5567                if w > 0.0 && h > 0.0 {
5568                    Some(crate::svg::SvgMultiPolygon {
5569                        rings: crate::svg::SvgPathVec::from_vec(vec![
5570                            crate::path_parser::svg_rect_to_path(x, y, w, h, rx, ry),
5571                        ]),
5572                    })
5573                } else {
5574                    None
5575                }
5576            }
5577            "ellipse" => {
5578                let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
5579                let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
5580                let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
5581                let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
5582                if rx > 0.0 && ry > 0.0 {
5583                    // Approximate ellipse with 4 cubic beziers (using rx for x-kappa, ry for y-kappa)
5584                    use azul_css::props::basic::{SvgCubicCurve, SvgPoint};
5585                    const KAPPA: f32 = 0.552_284_8;
5586                    let kx = rx * KAPPA;
5587                    let ky = ry * KAPPA;
5588                    let elements = vec![
5589                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5590                            start: SvgPoint { x: cx, y: cy - ry },
5591                            ctrl_1: SvgPoint {
5592                                x: cx + kx,
5593                                y: cy - ry,
5594                            },
5595                            ctrl_2: SvgPoint {
5596                                x: cx + rx,
5597                                y: cy - ky,
5598                            },
5599                            end: SvgPoint { x: cx + rx, y: cy },
5600                        }),
5601                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5602                            start: SvgPoint { x: cx + rx, y: cy },
5603                            ctrl_1: SvgPoint {
5604                                x: cx + rx,
5605                                y: cy + ky,
5606                            },
5607                            ctrl_2: SvgPoint {
5608                                x: cx + kx,
5609                                y: cy + ry,
5610                            },
5611                            end: SvgPoint { x: cx, y: cy + ry },
5612                        }),
5613                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5614                            start: SvgPoint { x: cx, y: cy + ry },
5615                            ctrl_1: SvgPoint {
5616                                x: cx - kx,
5617                                y: cy + ry,
5618                            },
5619                            ctrl_2: SvgPoint {
5620                                x: cx - rx,
5621                                y: cy + ky,
5622                            },
5623                            end: SvgPoint { x: cx - rx, y: cy },
5624                        }),
5625                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5626                            start: SvgPoint { x: cx - rx, y: cy },
5627                            ctrl_1: SvgPoint {
5628                                x: cx - rx,
5629                                y: cy - ky,
5630                            },
5631                            ctrl_2: SvgPoint {
5632                                x: cx - kx,
5633                                y: cy - ry,
5634                            },
5635                            end: SvgPoint { x: cx, y: cy - ry },
5636                        }),
5637                    ];
5638                    Some(crate::svg::SvgMultiPolygon {
5639                        rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5640                            items: crate::svg::SvgPathElementVec::from_vec(elements),
5641                        }]),
5642                    })
5643                } else {
5644                    None
5645                }
5646            }
5647            "line" => {
5648                let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
5649                let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
5650                let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
5651                let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
5652                Some(crate::svg::SvgMultiPolygon {
5653                    rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5654                        items: crate::svg::SvgPathElementVec::from_vec(vec![
5655                            crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5656                                azul_css::props::basic::SvgPoint { x: x1, y: y1 },
5657                                azul_css::props::basic::SvgPoint { x: x2, y: y2 },
5658                            )),
5659                        ]),
5660                    }]),
5661                })
5662            }
5663            "polygon" | "polyline" => xml_node
5664                .attributes
5665                .get_key("points")
5666                .and_then(|pts| parse_svg_points(pts.as_str(), tag == "polygon")),
5667            _ => None,
5668        };
5669
5670        if let Some(mp) = clip {
5671            node.set_svg_data(crate::dom::SvgNodeData::Path(mp));
5672        }
5673    }
5674}
5675
5676#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5677// component_map is threaded through the whole fast-DOM pipeline for parity with the
5678// component-expanding interpreter path (see ~xml.rs:2845); this fast path never expands
5679// components, so it only forwards the map into recursive calls. Removing it here would
5680// cascade unused-param removals up the entire pipeline.
5681#[allow(clippy::only_used_in_recursion)]
5682fn xml_node_to_dom_fast<'a>(
5683    xml_node: &'a XmlNode,
5684    component_map: &'a ComponentMap,
5685    inside_svg: bool,
5686    depth: usize,
5687) -> Result<Dom, RenderDomError> {
5688    use crate::dom::Dom;
5689
5690    let component_name = normalize_casing(&xml_node.node_type);
5691
5692    // Look up the component definition
5693    let node_type = tag_to_node_type(&component_name);
5694    let mut dom = Dom::create_node(node_type);
5695
5696    apply_xml_node_attributes(&mut dom.root, xml_node, &component_name, inside_svg);
5697
5698    let child_inside_svg = inside_svg || component_name == "svg";
5699
5700    // AUDIT 2026-07-08: bound recursion depth to avoid a native stack overflow on
5701    // pathologically deep markup. At the cap, this node is emitted without its
5702    // children (truncation) rather than crashing the process.
5703    // AUDIT-TODO: a worklist-based iterative builder would preserve deep subtrees.
5704    if depth >= MAX_XML_NESTING_DEPTH {
5705        return Ok(dom);
5706    }
5707
5708    // Recursively convert children
5709    let mut children = Vec::new();
5710    for child in xml_node.children.as_ref() {
5711        match child {
5712            XmlNodeChild::Element(child_node) => {
5713                let child_dom =
5714                    xml_node_to_dom_fast(child_node, component_map, child_inside_svg, depth + 1)?;
5715                children.push(child_dom);
5716            }
5717            XmlNodeChild::Text(text) => {
5718                let text_dom = Dom::create_text(AzString::from(text.as_str()));
5719                children.push(text_dom);
5720            }
5721        }
5722    }
5723
5724    if !children.is_empty() {
5725        dom = dom.with_children(children.into());
5726    }
5727
5728    Ok(dom)
5729}
5730
5731/// Builder for arena-based DOM construction (`FastDom`).
5732/// Builds two parallel Vecs (hierarchy + `node_data`) in a single DFS pass.
5733#[derive(Debug)]
5734pub struct CompactDomBuilder {
5735    hierarchy: Vec<crate::styled_dom::NodeHierarchyItem>,
5736    node_data: Vec<crate::dom::NodeData>,
5737    css: Vec<crate::dom::CssWithNodeId>,
5738    /// Stack of (`node_index`, `previous_child_index`) for open elements
5739    stack: Vec<(usize, Option<usize>)>,
5740}
5741
5742impl Default for CompactDomBuilder {
5743    fn default() -> Self {
5744        Self::new()
5745    }
5746}
5747
5748impl CompactDomBuilder {
5749    #[must_use] pub const fn new() -> Self {
5750        Self {
5751            hierarchy: Vec::new(),
5752            node_data: Vec::new(),
5753            css: Vec::new(),
5754            stack: Vec::new(),
5755        }
5756    }
5757
5758    #[must_use] pub fn with_capacity(cap: usize) -> Self {
5759        Self {
5760            hierarchy: Vec::with_capacity(cap),
5761            node_data: Vec::with_capacity(cap),
5762            css: Vec::new(),
5763            stack: Vec::new(),
5764        }
5765    }
5766
5767    /// Open a new element node. Must be paired with `close_node()`.
5768    pub fn open_node(&mut self, node_data: crate::dom::NodeData) {
5769        use crate::id::NodeId;
5770        use crate::styled_dom::NodeHierarchyItem;
5771
5772        let idx = self.hierarchy.len();
5773
5774        // Determine parent from stack
5775        let parent_raw = if let Some(&(parent_idx, _)) = self.stack.last() {
5776            NodeId::into_raw(&Some(NodeId::new(parent_idx)))
5777        } else {
5778            0 // No parent (root)
5779        };
5780
5781        // Determine previous sibling from parent's last child tracking
5782        let prev_sibling_raw = if let Some(&(_, prev_child)) = self.stack.last() {
5783            prev_child
5784                .map_or(0, |pi| NodeId::into_raw(&Some(NodeId::new(pi))))
5785        } else {
5786            0
5787        };
5788
5789        // If there's a previous sibling, set its next_sibling to us
5790        if let Some(&(_, Some(prev_idx))) = self.stack.last() {
5791            self.hierarchy[prev_idx].next_sibling = NodeId::into_raw(&Some(NodeId::new(idx)));
5792        }
5793
5794        // Update parent's "last seen child" to us
5795        if let Some(parent) = self.stack.last_mut() {
5796            parent.1 = Some(idx);
5797        }
5798
5799        // Push the hierarchy item (last_child will be set in close_node)
5800        self.hierarchy.push(NodeHierarchyItem {
5801            parent: parent_raw,
5802            previous_sibling: prev_sibling_raw,
5803            next_sibling: 0, // Will be set by next sibling's open_node
5804            last_child: 0,   // Will be set in close_node
5805        });
5806        self.node_data.push(node_data);
5807
5808        // Push onto stack: this node is now the "open" element, no children yet
5809        self.stack.push((idx, None));
5810    }
5811
5812    /// Close the current element. Sets the `last_child` pointer.
5813    pub fn close_node(&mut self) {
5814        use crate::id::NodeId;
5815
5816        if let Some((idx, last_child_idx)) = self.stack.pop() {
5817            // Set last_child on this node's hierarchy item
5818            self.hierarchy[idx].last_child = last_child_idx
5819                .map_or(0, |lc| NodeId::into_raw(&Some(NodeId::new(lc))));
5820        }
5821    }
5822
5823    /// Add a leaf node (text, br, hr, etc.) that has no children.
5824    pub fn add_leaf(&mut self, node_data: crate::dom::NodeData) {
5825        self.open_node(node_data);
5826        self.close_node();
5827    }
5828
5829    /// Add a CSS stylesheet scoped to a node ID.
5830    pub fn add_css(&mut self, node_id: usize, css: Css) {
5831        self.css.push(crate::dom::CssWithNodeId { node_id, css });
5832    }
5833
5834    /// Finish building and produce a `FastDom`.
5835    #[must_use] pub fn finish(self) -> crate::dom::FastDom {
5836        crate::dom::FastDom {
5837            node_hierarchy: self.hierarchy.into(),
5838            node_data: self.node_data.into(),
5839            css: self.css.into(),
5840        }
5841    }
5842}
5843
5844/// Convert an XML node tree into a `FastDom` (arena-based) in a single DFS pass.
5845/// This is the fast path equivalent of `xml_node_to_dom_fast`.
5846#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5847// See xml_node_to_dom_fast: component_map is forwarded for pipeline parity, not read here.
5848#[allow(clippy::only_used_in_recursion)]
5849fn xml_node_to_fast_dom<'a>(
5850    xml_node: &'a XmlNode,
5851    component_map: &'a ComponentMap,
5852    inside_svg: bool,
5853    builder: &mut CompactDomBuilder,
5854    depth: usize,
5855) -> Result<(), RenderDomError> {
5856    use crate::dom::NodeData;
5857
5858    let component_name = normalize_casing(&xml_node.node_type);
5859    let node_type = tag_to_node_type(&component_name);
5860    let mut node_data = NodeData::create_node(node_type);
5861
5862    apply_xml_node_attributes(&mut node_data, xml_node, &component_name, inside_svg);
5863
5864    let child_inside_svg = inside_svg || component_name == "svg";
5865
5866    // Open this node in the builder
5867    builder.open_node(node_data);
5868
5869    // AUDIT 2026-07-08: bound recursion depth to avoid a native stack overflow on
5870    // pathologically deep markup. At the cap, children are dropped (the node is
5871    // still opened+closed) rather than crashing the process.
5872    // AUDIT-TODO: a worklist-based iterative builder would preserve deep subtrees.
5873    if depth < MAX_XML_NESTING_DEPTH {
5874        // Recursively convert children
5875        for child in xml_node.children.as_ref() {
5876            match child {
5877                XmlNodeChild::Element(child_node) => {
5878                    xml_node_to_fast_dom(
5879                        child_node,
5880                        component_map,
5881                        child_inside_svg,
5882                        builder,
5883                        depth + 1,
5884                    )?;
5885                }
5886                XmlNodeChild::Text(text) => {
5887                    builder.add_leaf(NodeData::create_text(AzString::from(text.as_str())));
5888                }
5889            }
5890        }
5891    }
5892
5893    // Close this node
5894    builder.close_node();
5895
5896    Ok(())
5897}
5898
5899/// Render a DOM from an XML body node using the fast arena-based path.
5900/// Builds a `FastDom` directly (no tree intermediary), then creates `StyledDom`.
5901#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5902fn render_dom_from_body_node_fast<'a>(
5903    body_node: &'a XmlNode,
5904    mut global_css: Option<Css>,
5905    component_map: &'a ComponentMap,
5906    max_width: Option<f32>,
5907) -> Result<StyledDom, RenderDomError> {
5908    use crate::dom::{NodeData, NodeType};
5909
5910    let mut builder = CompactDomBuilder::new();
5911
5912    // Build the HTML > Body wrapper + body content in one pass
5913    // Open <html>
5914    builder.open_node(NodeData::create_node(NodeType::Html));
5915    // Open <body> (the body_node content goes inside)
5916    xml_node_to_fast_dom(body_node, component_map, false, &mut builder, 0)?;
5917    // Close <html>
5918    builder.close_node();
5919
5920    // Collect CSS rules from each source.
5921    let mut combined_rules: Vec<CssRuleBlock> = Vec::new();
5922    if let Some(max_width) = max_width {
5923        let max_width_css =
5924            Css::from_string(format!("html {{ max-width: {max_width}px; }}").into());
5925        combined_rules.extend(max_width_css.rules.into_library_owned_vec());
5926    }
5927    if let Some(css) = global_css.take() {
5928        combined_rules.extend(css.rules.into_library_owned_vec());
5929    }
5930    let combined_css = Css::new(combined_rules);
5931
5932    // Add CSS to the FastDom
5933    let mut fast_dom = builder.finish();
5934    fast_dom.css = vec![crate::dom::CssWithNodeId {
5935        node_id: 0, // Global scope (root)
5936        css: combined_css,
5937    }]
5938    .into();
5939
5940    // Create StyledDom via the fast path (no tree→arena conversion)
5941    let styled = StyledDom::create_from_fast_dom(fast_dom);
5942    Ok(styled)
5943}
5944
5945// render_dom_from_body_node() removed — use render_dom_from_body_node_fast() or str_to_dom()
5946
5947fn set_stringified_attributes(
5948    dom_string: &mut String,
5949    xml_attributes: &XmlAttributeMap,
5950    filtered_xml_attributes: &ComponentArgumentVec,
5951    tabs: usize,
5952) {
5953    let t0 = String::from("    ").repeat(tabs);
5954    let t = String::from("    ").repeat(tabs + 1);
5955
5956    // push ids and classes as chained `.with_id("..")` / `.with_class("..")`
5957    // calls (public builder API; both take `Into<AzString>`, so bare &str works).
5958    let _ = &t;
5959    for id in xml_attributes
5960        .get_key("id")
5961        .map(|s| s.split_whitespace().collect::<Vec<_>>())
5962        .unwrap_or_default()
5963    {
5964        let _ = write!(
5965            dom_string,
5966            "\r\n{}.with_id(\"{}\")",
5967            t0,
5968            format_args_dynamic(id, filtered_xml_attributes)
5969        );
5970    }
5971
5972    for class in xml_attributes
5973        .get_key("class")
5974        .map(|s| s.split_whitespace().collect::<Vec<_>>())
5975        .unwrap_or_default()
5976    {
5977        let _ = write!(
5978            dom_string,
5979            "\r\n{}.with_class(\"{}\")",
5980            t0,
5981            format_args_dynamic(class, filtered_xml_attributes)
5982        );
5983    }
5984
5985    if let Some(focusable) = xml_attributes
5986        .get_key("focusable")
5987        .map(|f| format_args_dynamic(f, filtered_xml_attributes))
5988        .and_then(|f| parse_bool(&f))
5989    {
5990        if focusable { let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)"); } else { let _ = write!(dom_string,
5991            "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
5992        ); }
5993    }
5994
5995    if let Some(tab_index) = xml_attributes
5996        .get_key("tabindex")
5997        .map(|val| format_args_dynamic(val, filtered_xml_attributes))
5998        .and_then(|val| val.parse::<isize>().ok())
5999    {
6000        match tab_index {
6001            0 => { let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)"); },
6002            i if i > 0 => { let _ = write!(dom_string,
6003                "\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
6004                t, usize::try_from(i).unwrap_or(0)
6005            ); },
6006            _ => { let _ = write!(dom_string,
6007                "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6008            ); },
6009        }
6010    }
6011}
6012
6013/// Item of a split string - either a variable name (with optional format spec) or a string
6014#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6015pub enum DynamicItem {
6016    /// A variable reference, e.g. {counter} or {counter:?} or {price:.2}
6017    Var {
6018        name: String,
6019        /// Optional format specifier after the colon: "?" for debug, ".2" for precision, etc.
6020        format_spec: Option<String>,
6021    },
6022    Str(String),
6023}
6024
6025/// Splits a string into formatting arguments, supporting format specifiers like `{var:?}`
6026/// ```rust
6027/// # use azul_core::xml::DynamicItem::*;
6028/// # use azul_core::xml::split_dynamic_string;
6029/// let s = "hello {a}, {b}{{ {c} }}";
6030/// let split = split_dynamic_string(s);
6031/// let output = vec![
6032///     Str("hello ".to_string()),
6033///     Var { name: "a".to_string(), format_spec: None },
6034///     Str(", ".to_string()),
6035///     Var { name: "b".to_string(), format_spec: None },
6036///     Str("{ ".to_string()),
6037///     Var { name: "c".to_string(), format_spec: None },
6038///     Str(" }".to_string()),
6039/// ];
6040/// assert_eq!(output, split);
6041/// ```
6042#[must_use] pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
6043    use self::DynamicItem::{Str, Var};
6044
6045    let input: Vec<char> = input.chars().collect();
6046    let input_chars_len = input.len();
6047
6048    let mut items = Vec::new();
6049    let mut current_idx = 0;
6050    let mut last_idx = 0;
6051
6052    while current_idx < input_chars_len {
6053        let c = input[current_idx];
6054        match c {
6055            '{' if input.get(current_idx + 1).copied() != Some('{') => {
6056                // variable start, search until next closing brace or whitespace or end of string
6057                let mut start_offset = 1;
6058                let mut has_found_variable = false;
6059                while let Some(c) = input.get(current_idx + start_offset) {
6060                    if c.is_whitespace() {
6061                        break;
6062                    }
6063                    if *c == '}' && input.get(current_idx + start_offset + 1).copied() != Some('}')
6064                    {
6065                        start_offset += 1;
6066                        has_found_variable = true;
6067                        break;
6068                    }
6069                    start_offset += 1;
6070                }
6071
6072                // advance current_idx accordingly
6073                // on fail, set cursor to end
6074                // set last_idx accordingly
6075                if has_found_variable {
6076                    if last_idx != current_idx {
6077                        items.push(Str(input[last_idx..current_idx].iter().collect()));
6078                    }
6079
6080                    // subtract 1 from start for opening brace, one from end for closing brace
6081                    let var_content: String = input
6082                        [(current_idx + 1)..(current_idx + start_offset - 1)]
6083                        .iter()
6084                        .collect();
6085                    // Split on first ':' to separate variable name from format specifier
6086                    let (var_name, format_spec) = if let Some(colon_pos) = var_content.find(':') {
6087                        let name = var_content[..colon_pos].to_string();
6088                        let spec = var_content[(colon_pos + 1)..].to_string();
6089                        (name, Some(spec))
6090                    } else {
6091                        (var_content, None)
6092                    };
6093                    items.push(Var {
6094                        name: var_name,
6095                        format_spec,
6096                    });
6097                    current_idx += start_offset;
6098                    last_idx = current_idx;
6099                } else {
6100                    current_idx += start_offset;
6101                }
6102            }
6103            _ => {
6104                current_idx += 1;
6105            }
6106        }
6107    }
6108
6109    if current_idx != last_idx {
6110        items.push(Str(input[last_idx..].iter().collect()));
6111    }
6112
6113    for item in &mut items {
6114        // replace {{ with { in strings
6115        if let Str(s) = item {
6116            *s = s.replace("{{", "{").replace("}}", "}");
6117        }
6118    }
6119
6120    items
6121}
6122
6123/// Combines the split string back into its original form while replacing the variables with their
6124/// values
6125///
6126/// let variables = btreemap!{ "a" => "value1", "b" => "value2" };
6127/// [Str("hello "), Var("a"), Str(", "), Var("b"), Str("{ "), Var("c"), Str(" }}")]
6128/// => "hello value1, valuec{ {c} }"
6129fn combine_and_replace_dynamic_items(
6130    input: &[DynamicItem],
6131    variables: &ComponentArgumentVec,
6132) -> String {
6133    let mut s = String::new();
6134
6135    for item in input {
6136        match item {
6137            DynamicItem::Var { name, format_spec } => {
6138                let variable_name = normalize_casing(name.trim());
6139                if let Some(resolved_var) = variables
6140                    .iter()
6141                    .find(|s| s.name.as_str() == variable_name)
6142                    .map(|q| &q.arg_type) {
6143                    // Format specifiers are applied at compile time, not at runtime replacement
6144                    s.push_str(resolved_var);
6145                } else {
6146                    s.push('{');
6147                    s.push_str(name);
6148                    if let Some(spec) = format_spec {
6149                        s.push(':');
6150                        s.push_str(spec);
6151                    }
6152                    s.push('}');
6153                }
6154            }
6155            DynamicItem::Str(dynamic_str) => {
6156                s.push_str(dynamic_str);
6157            }
6158        }
6159    }
6160
6161    s
6162}
6163
6164/// Given a string and a key => value mapping, replaces parts of the string with the value, i.e.:
6165///
6166/// ```rust
6167/// # use azul_core::xml::{format_args_dynamic, ComponentArgument, ComponentArgumentVec};
6168/// # use azul_css::AzString;
6169/// let variables: ComponentArgumentVec = vec![
6170///     ComponentArgument { name: AzString::from("a"), arg_type: AzString::from("value1") },
6171///     ComponentArgument { name: AzString::from("b"), arg_type: AzString::from("value2") },
6172/// ].into();
6173///
6174/// let initial = "hello {a}, {b}{{ {c} }}";
6175/// let expected = "hello value1, value2{ {c} }".to_string();
6176/// assert_eq!(format_args_dynamic(initial, &variables), expected);
6177/// ```
6178///
6179/// Note: the number (0, 1, etc.) is the order of the argument, it is irrelevant for
6180/// runtime formatting, only important for keeping the component / function arguments
6181/// in order when compiling the arguments to Rust code
6182#[must_use] pub fn format_args_dynamic(input: &str, variables: &ComponentArgumentVec) -> String {
6183    let dynamic_str_items = split_dynamic_string(input);
6184    combine_and_replace_dynamic_items(&dynamic_str_items, variables)
6185}
6186
6187/// Decode a numeric character reference body (the part between `&` and `;`),
6188/// e.g. `"#65"` -> `'A'`, `"#x41"` -> `'A'`. Returns `None` if it is not a valid
6189/// numeric reference.
6190fn decode_numeric_entity(entity: &str) -> Option<char> {
6191    let num = entity.strip_prefix('#')?;
6192    let code = if let Some(hex) = num.strip_prefix(['x', 'X']) {
6193        u32::from_str_radix(hex, 16).ok()?
6194    } else {
6195        num.parse::<u32>().ok()?
6196    };
6197    char::from_u32(code)
6198}
6199
6200/// Decode the common HTML/XML entities in a single left-to-right pass.
6201///
6202/// Handles `&lt;` `&gt;` `&amp;` `&quot;` `&apos;` and numeric references
6203/// (`&#NN;` / `&#xHH;`). `&nbsp;` and any unrecognized `&...;` sequence are left
6204/// verbatim. The single pass guarantees `&amp;` never double-decodes a following
6205/// entity. See [`prepare_string`] for why `&nbsp;` is deliberately preserved.
6206fn decode_entities(input: &str) -> String {
6207    // Longest handled entity body is a hex numeric ref like `#x10FFFF` (8 bytes);
6208    // cap the `;` search window so a stray `&` far from a `;` stays cheap.
6209    const MAX_ENTITY_BODY: usize = 12;
6210
6211    let mut out = String::with_capacity(input.len());
6212    let bytes = input.as_bytes();
6213    let mut i = 0;
6214    while i < input.len() {
6215        if bytes[i] == b'&' {
6216            if let Some(semi_rel) = input[i + 1..].find(';') {
6217                if semi_rel <= MAX_ENTITY_BODY {
6218                    let body = &input[i + 1..i + 1 + semi_rel];
6219                    let end = i + 1 + semi_rel; // index of ';'
6220                    // Leave &nbsp; for the per-line pass in prepare_string.
6221                    if body.eq_ignore_ascii_case("nbsp") {
6222                        out.push_str(&input[i..=end]);
6223                        i = end + 1;
6224                        continue;
6225                    }
6226                    let decoded = match body {
6227                        "lt" => Some('<'),
6228                        "gt" => Some('>'),
6229                        "amp" => Some('&'),
6230                        "quot" => Some('"'),
6231                        "apos" => Some('\''),
6232                        _ => decode_numeric_entity(body),
6233                    };
6234                    if let Some(c) = decoded {
6235                        out.push(c);
6236                        i = end + 1;
6237                        continue;
6238                    }
6239                }
6240            }
6241            // Not a recognized entity: emit the '&' literally.
6242            out.push('&');
6243            i += 1;
6244        } else {
6245            // Copy one whole UTF-8 char (i is always on a char boundary here).
6246            let ch = input[i..].chars().next().unwrap_or('\u{FFFD}');
6247            out.push(ch);
6248            i += ch.len_utf8();
6249        }
6250    }
6251    out
6252}
6253
6254// NOTE: Two sequential returns count as a single return, while single returns get ignored.
6255#[must_use] pub fn prepare_string(input: &str) -> String {
6256    const SPACE: &str = " ";
6257    const RETURN: &str = "\n";
6258
6259    let input = input.trim();
6260
6261    if input.is_empty() {
6262        return String::new();
6263    }
6264
6265    // AUDIT 2026-07-08: previously only `&lt;`/`&gt;` were decoded. Decode the full
6266    // common named-entity set (`&lt;` `&gt;` `&amp;` `&quot;` `&apos;`) plus numeric
6267    // references (`&#NN;` decimal and `&#xHH;` hex) in a single left-to-right pass.
6268    // A single pass is used deliberately so `&amp;` cannot double-decode a following
6269    // entity (e.g. "&amp;lt;" -> literal "&lt;", not "<"). `&nbsp;` is intentionally
6270    // left untouched here so the per-line pass below (which runs AFTER trimming) can
6271    // still turn it into a space that survives leading/trailing trim.
6272    let input = decode_entities(input);
6273
6274    let input_len = input.len();
6275    let mut final_lines: Vec<String> = Vec::new();
6276    let mut last_line_was_empty = false;
6277
6278    for line in input.lines() {
6279        let line = line.trim();
6280        let line = line.replace("&nbsp;", " ");
6281        let current_line_is_empty = line.is_empty();
6282
6283        if !current_line_is_empty {
6284            if last_line_was_empty {
6285                final_lines.push(format!("{RETURN}{line}"));
6286            } else {
6287                final_lines.push(line.to_string());
6288            }
6289        }
6290
6291        last_line_was_empty = current_line_is_empty;
6292    }
6293
6294    let line_len = final_lines.len();
6295    let mut target = String::with_capacity(input_len);
6296    for (line_idx, line) in final_lines.iter().enumerate() {
6297        if !(line.starts_with(RETURN) || line_idx == 0 || line_idx == line_len.saturating_sub(1)) {
6298            target.push_str(SPACE);
6299        }
6300        target.push_str(line);
6301    }
6302    target
6303}
6304
6305/// Parses a string ("true" or "false")
6306#[must_use] pub fn parse_bool(input: &str) -> Option<bool> {
6307    match input {
6308        "true" => Some(true),
6309        "false" => Some(false),
6310        _ => None,
6311    }
6312}
6313
6314#[derive(Debug, Clone)]
6315pub struct CssMatcher {
6316    path: Vec<CssPathSelector>,
6317    indices_in_parent: Vec<usize>,
6318    children_length: Vec<usize>,
6319}
6320
6321impl CssMatcher {
6322    fn get_hash(&self) -> u64 {
6323        use core::hash::Hash;
6324
6325        use core::hash::Hasher;
6326
6327        let mut hasher = crate::hash::DefaultHasher::new();
6328        for p in &self.path {
6329            p.hash(&mut hasher);
6330        }
6331        hasher.finish()
6332    }
6333}
6334
6335impl CssMatcher {
6336    fn matches(&self, path: &CssPath) -> bool {
6337        use azul_css::css::CssPathSelector::*;
6338
6339        use crate::style::{CssGroupIterator, CssGroupSplitReason};
6340
6341        if self.path.is_empty() {
6342            return false;
6343        }
6344        if path.selectors.as_ref().is_empty() {
6345            return false;
6346        }
6347
6348        // self_matcher is only ever going to contain "Children" selectors, never "DirectChildren"
6349        let mut path_groups = CssGroupIterator::new(path.selectors.as_ref()).collect::<Vec<_>>();
6350        path_groups.reverse();
6351
6352        if path_groups.is_empty() {
6353            return false;
6354        }
6355        let mut self_groups = CssGroupIterator::new(self.path.as_ref()).collect::<Vec<_>>();
6356        self_groups.reverse();
6357        if self_groups.is_empty() {
6358            return false;
6359        }
6360
6361        if self.indices_in_parent.len() != self_groups.len() {
6362            return false;
6363        }
6364        if self.children_length.len() != self_groups.len() {
6365            return false;
6366        }
6367
6368        // self_groups = [ // HTML
6369        //     "body",
6370        //     "div.__azul_native-ribbon-container"
6371        //     "div.__azul_native-ribbon-tabs"
6372        //     "p.home"
6373        // ]
6374        //
6375        // path_groups = [ // CSS
6376        //     ".__azul_native-ribbon-tabs"
6377        //     "div.after-tabs"
6378        // ]
6379
6380        // get the first path group and see if it matches anywhere in the self group
6381        let mut cur_selfgroup_scan = 0;
6382        let mut cur_pathgroup_scan = 0;
6383        let mut valid = false;
6384        let mut path_group = path_groups[cur_pathgroup_scan].clone();
6385
6386        while cur_selfgroup_scan < self_groups.len() {
6387            let mut advance = None;
6388
6389            // scan all remaining path groups
6390            for (id, cg) in self_groups[cur_selfgroup_scan..].iter().enumerate() {
6391                let gm = group_matches(
6392                    &path_group.0,
6393                    &self_groups[cur_selfgroup_scan + id].0,
6394                    self.indices_in_parent[cur_selfgroup_scan + id],
6395                    self.children_length[cur_selfgroup_scan + id],
6396                );
6397
6398                if gm {
6399                    // ok: ".__azul_native-ribbon-tabs" was found within self_groups
6400                    // advance the self_groups by n
6401                    advance = Some(id);
6402                    break;
6403                }
6404            }
6405
6406            match advance {
6407                Some(n) => {
6408                    // group was found in remaining items
6409                    // advance cur_pathgroup_scan by 1 and cur_selfgroup_scan by n
6410                    if cur_pathgroup_scan == path_groups.len() - 1 {
6411                        // last path group
6412                        return cur_selfgroup_scan + n == self_groups.len() - 1;
6413                    }
6414                    cur_pathgroup_scan += 1;
6415                    cur_selfgroup_scan += n;
6416                    path_group = path_groups[cur_pathgroup_scan].clone();
6417                }
6418                None => return false, // group was not found in remaining items
6419            }
6420        }
6421
6422        // only return true if all path_groups matched
6423        cur_pathgroup_scan == path_groups.len() - 1
6424    }
6425}
6426
6427// does p.home match div.after-tabs?
6428// a: div.after-tabs
6429fn group_matches(
6430    a: &[&CssPathSelector],
6431    b: &[&CssPathSelector],
6432    idx_in_parent: usize,
6433    parent_children: usize,
6434) -> bool {
6435    use azul_css::css::{CssNthChildSelector, CssPathPseudoSelector, CssPathSelector::{Global, PseudoSelector, Type, Class, Id}};
6436
6437    for selector in a {
6438        match selector {
6439            // always matches
6440            Global |
6441PseudoSelector(CssPathPseudoSelector::Hover | CssPathPseudoSelector::Active |
6442CssPathPseudoSelector::Focus) => {}
6443
6444            Type(tag) => {
6445                if !b.iter().any(|t| **t == Type(*tag)) {
6446                    return false;
6447                }
6448            }
6449            Class(class) => {
6450                if !b.iter().any(|t| **t == Class(class.clone())) {
6451                    return false;
6452                }
6453            }
6454            Id(id) => {
6455                if !b.iter().any(|t| **t == Id(id.clone())) {
6456                    return false;
6457                }
6458            }
6459            PseudoSelector(CssPathPseudoSelector::First) => {
6460                if idx_in_parent != 0 {
6461                    return false;
6462                }
6463            }
6464            PseudoSelector(CssPathPseudoSelector::Last) => {
6465                if idx_in_parent != parent_children.saturating_sub(1) {
6466                    return false;
6467                }
6468            }
6469            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(i))) => {
6470                if idx_in_parent != *i as usize {
6471                    return false;
6472                }
6473            }
6474            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even)) => {
6475                if !idx_in_parent.is_multiple_of(2) {
6476                    return false;
6477                }
6478            }
6479            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
6480                if idx_in_parent.is_multiple_of(2) {
6481                    return false;
6482                }
6483            }
6484            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
6485                if !idx_in_parent.saturating_sub(p.offset as usize).is_multiple_of(p.pattern_repeat as usize)
6486                {
6487                    return false;
6488                }
6489            }
6490
6491            _ => return false, // can't happen
6492        }
6493    }
6494
6495    true
6496}
6497
6498struct CssBlock {
6499    ending: Option<CssPathPseudoSelector>,
6500    block: CssRuleBlock,
6501}
6502
6503#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
6504/// # Errors
6505///
6506/// Returns an error if the body node cannot be compiled to Rust code.
6507pub fn compile_body_node_to_rust_code<'a>(
6508    body_node: &'a XmlNode,
6509    component_map: &'a ComponentMap,
6510    extra_blocks: &mut VecContents,
6511    css_blocks: &mut BTreeMap<String, String>,
6512    css: &Css,
6513    mut matcher: CssMatcher,
6514) -> Result<String, CompileError> {
6515    use azul_css::css::CssDeclaration;
6516
6517    let t = "";
6518    let t2 = "    ";
6519    let mut dom_string = String::from("Dom::create_body()");
6520    let node_type = CssPathSelector::Type(NodeTypeTag::Body);
6521    matcher.path.push(node_type);
6522
6523    let ids = body_node
6524        .attributes
6525        .get_key("id")
6526        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6527        .unwrap_or_default();
6528    matcher.path.extend(
6529        ids.into_iter()
6530            .map(|id| CssPathSelector::Id(id.to_string().into())),
6531    );
6532    let classes = body_node
6533        .attributes
6534        .get_key("class")
6535        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6536        .unwrap_or_default();
6537    matcher.path.extend(
6538        classes
6539            .into_iter()
6540            .map(|class| CssPathSelector::Class(class.to_string().into())),
6541    );
6542
6543    let matcher_hash = matcher.get_hash();
6544    let css_blocks_for_this_node = get_css_blocks(css, &matcher);
6545    if !css_blocks_for_this_node.is_empty() {
6546        // Track property types for the helper-const machinery, then emit the
6547        // matched declarations as an inline CSS string. (The old path emitted a
6548        // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` + `.with_inline_css_props`,
6549        // but that API was removed in 32d44ed8a; `.with_css(<str>)` is the
6550        // current equivalent and parses pseudo blocks too.)
6551        for css_block in &css_blocks_for_this_node {
6552            for declaration in css_block.block.declarations.as_ref() {
6553                let prop = match declaration {
6554                    CssDeclaration::Static(s) => s,
6555                    CssDeclaration::Dynamic(d) => &d.default_value,
6556                };
6557                extra_blocks.insert_from_css_property(prop);
6558            }
6559        }
6560
6561        let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
6562        if !inline_css.is_empty() {
6563            let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
6564            let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
6565        }
6566        let _ = (&mut *css_blocks, matcher_hash); // retained for signature compat
6567    }
6568
6569    if !body_node.children.as_ref().is_empty() {
6570        use azul_css::codegen::format::GetHash;
6571        let children_hash = body_node.children.as_ref().get_hash();
6572        dom_string.push_str("\r\n.with_children(vec![\r\n");
6573
6574        for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
6575            match child {
6576                XmlNodeChild::Element(child_node) => {
6577                    let mut matcher = matcher.clone();
6578                    matcher.path.push(CssPathSelector::Children);
6579                    matcher.indices_in_parent.push(child_idx);
6580                    matcher.children_length.push(body_node.children.len());
6581
6582                    let _ = write!(dom_string,
6583                        "{}{},\r\n",
6584                        t,
6585                        compile_node_to_rust_code_inner(
6586                            child_node,
6587                            component_map,
6588                            1,
6589                            extra_blocks,
6590                            css_blocks,
6591                            css,
6592                            matcher,
6593                        )?
6594                    );
6595                }
6596                XmlNodeChild::Text(text) => {
6597                    let text = text.trim();
6598                    if !text.is_empty() {
6599                        let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
6600                        let _ = write!(dom_string,
6601                            "{t}Dom::create_text(\"{escaped}\"),\r\n"
6602                        );
6603                    }
6604                }
6605            }
6606        }
6607        let _ = write!(dom_string, "\r\n{t}])");
6608    }
6609
6610    let dom_string = dom_string.trim();
6611    Ok(dom_string.to_string())
6612}
6613
6614/// Serialize the CSS blocks matched for a node into one inline CSS string for
6615/// `Dom::with_css(...)`. `with_css` parses via `Css::parse_inline`, which runs
6616/// the full selector+nesting machinery, so `:hover`/`:active`/`:focus` are
6617/// emitted as nested pseudo blocks and round-trip faithfully; plain rules are
6618/// emitted flat as `key: value;` (via `CssProperty::key()` / `value()`).
6619fn css_blocks_to_inline_string(blocks: &[CssBlock]) -> String {
6620    fn decls_of(block: &CssBlock) -> Vec<String> {
6621        block
6622            .block
6623            .declarations
6624            .as_ref()
6625            .iter()
6626            .map(|d| {
6627                let prop = match d {
6628                    CssDeclaration::Static(s) => s,
6629                    CssDeclaration::Dynamic(dy) => &dy.default_value,
6630                };
6631                format!("{}: {};", prop.key(), prop.value())
6632            })
6633            .collect()
6634    }
6635
6636    let mut normal: Vec<String> = Vec::new();
6637    let mut pseudo: Vec<String> = Vec::new();
6638    for block in blocks {
6639        let pseudo_sel = match block.ending {
6640            Some(CssPathPseudoSelector::Hover) => Some(":hover"),
6641            Some(CssPathPseudoSelector::Active) => Some(":active"),
6642            Some(CssPathPseudoSelector::Focus) => Some(":focus"),
6643            _ => None,
6644        };
6645        match pseudo_sel {
6646            None => normal.extend(decls_of(block)),
6647            Some(sel) => pseudo.push(format!("{} {{ {} }}", sel, decls_of(block).join(" "))),
6648        }
6649    }
6650
6651    let mut parts = normal;
6652    parts.extend(pseudo);
6653    parts.join(" ")
6654}
6655
6656fn get_css_blocks(css: &Css, matcher: &CssMatcher) -> Vec<CssBlock> {
6657    let mut blocks = Vec::new();
6658
6659    for css_block in css.rules.as_ref() {
6660        if matcher.matches(&css_block.path) {
6661            let mut ending = None;
6662
6663            if let Some(CssPathSelector::PseudoSelector(p)) =
6664                css_block.path.selectors.as_ref().last()
6665            {
6666                ending = Some(p.clone());
6667            }
6668
6669            blocks.push(CssBlock {
6670                ending,
6671                block: css_block.clone(),
6672            });
6673        }
6674    }
6675
6676    blocks
6677}
6678
6679fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
6680    use self::DynamicItem::{Var, Str};
6681    if input.is_empty() {
6682        String::from("AzString::from_const_str(\"\")")
6683    } else if input.len() == 1 {
6684        // common: there is only one "dynamic item" - skip the "format!()" macro
6685        match &input[0] {
6686            Var { name, format_spec } => {
6687                let var_name = normalize_casing(name.trim());
6688                if let Some(spec) = format_spec {
6689                    format!("format!(\"{{:{spec}}}\", {var_name}).into()")
6690                } else {
6691                    var_name
6692                }
6693            }
6694            Str(s) => format!("AzString::from_const_str(\"{s}\")"),
6695        }
6696    } else {
6697        // build a "format!("{var}, blah", var)" string
6698        let mut formatted_str = String::from("format!(\"");
6699        let mut variables = Vec::new();
6700        for item in input {
6701            match item {
6702                Var { name, format_spec } => {
6703                    let variable_name = normalize_casing(name.trim());
6704                    if let Some(spec) = format_spec {
6705                        let _ = write!(formatted_str, "{{{variable_name}:{spec}}}");
6706                    } else {
6707                        let _ = write!(formatted_str, "{{{variable_name}}}");
6708                    }
6709                    variables.push(variable_name.clone());
6710                }
6711                Str(s) => {
6712                    let s = s.replace('"', "\\\"");
6713                    formatted_str.push_str(&s);
6714                }
6715            }
6716        }
6717
6718        formatted_str.push('\"');
6719        if !variables.is_empty() {
6720            formatted_str.push_str(", ");
6721        }
6722
6723        formatted_str.push_str(&variables.join(", "));
6724        formatted_str.push_str(").into()");
6725        formatted_str
6726    }
6727}
6728
6729fn format_args_for_rust_code(input: &str) -> String {
6730    let dynamic_str_items = split_dynamic_string(input);
6731    compile_and_format_dynamic_items(&dynamic_str_items)
6732}
6733
6734#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
6735// component_map is forwarded through the codegen recursion for parity with the
6736// component-expanding path; this Rust-codegen path only threads it into recursive calls.
6737#[allow(clippy::only_used_in_recursion)]
6738#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
6739fn compile_node_to_rust_code_inner(
6740    node: &XmlNode,
6741    component_map: &ComponentMap,
6742    tabs: usize,
6743    extra_blocks: &mut VecContents,
6744    css_blocks: &mut BTreeMap<String, String>,
6745    css: &Css,
6746    mut matcher: CssMatcher,
6747) -> Result<String, CompileError> {
6748    use azul_css::css::CssDeclaration;
6749
6750    let t = String::from("    ").repeat(tabs - 1);
6751    let t2 = String::from("    ").repeat(tabs);
6752
6753    let component_name = normalize_casing(&node.node_type);
6754
6755    // Look up the CSS NodeTypeTag
6756    let node_type_tag = tag_to_node_type_tag(&component_name);
6757    let node_type = CssPathSelector::Type(node_type_tag);
6758
6759    // Emit a plain `create_node(<Tag>)` for the base node. Do NOT route through
6760    // the component `compile_fn`: its Rust arm bakes inline text into a
6761    // `.with_children(..)`, which the child-walk below would then OVERWRITE with
6762    // a second `.with_children(..)` — silently dropping the text on any node
6763    // that has BOTH text and element children. The child-walk handles ALL
6764    // children (text + elements) in order, so the base node must stay childless.
6765    // Interactive/data tags (Button/Input/…) whose NodeType carries data fall
6766    // back to `div`, matching the C/C++/Python walkers (`safe_container_tag`).
6767    let ctor = analyze_node_ctor(&component_name, node);
6768    let mut dom_string = ctor.render_rust().map_or_else(|| {
6769        let tag = safe_container_tag(&format!("{:?}", tag_to_node_type(&component_name)));
6770        format!("{t2}Dom::create_node(NodeType::{tag})")
6771    }, |expr| format!("{t2}{expr}"));
6772
6773    matcher.path.push(node_type);
6774    let ids = node
6775        .attributes
6776        .get_key("id")
6777        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6778        .unwrap_or_default();
6779
6780    matcher.path.extend(
6781        ids.into_iter()
6782            .map(|id| CssPathSelector::Id(id.to_string().into())),
6783    );
6784
6785    let classes = node
6786        .attributes
6787        .get_key("class")
6788        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6789        .unwrap_or_default();
6790
6791    matcher.path.extend(
6792        classes
6793            .into_iter()
6794            .map(|class| CssPathSelector::Class(class.to_string().into())),
6795    );
6796
6797    let matcher_hash = matcher.get_hash();
6798    let css_blocks_for_this_node = get_css_blocks(css, &matcher);
6799    if !css_blocks_for_this_node.is_empty() {
6800        // Track property types for the helper-const machinery, then emit the
6801        // matched declarations as an inline CSS string. (The old path emitted a
6802        // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` + `.with_inline_css_props`,
6803        // but that API was removed in 32d44ed8a; `.with_css(<str>)` is the
6804        // current equivalent and parses pseudo blocks too.)
6805        for css_block in &css_blocks_for_this_node {
6806            for declaration in css_block.block.declarations.as_ref() {
6807                let prop = match declaration {
6808                    CssDeclaration::Static(s) => s,
6809                    CssDeclaration::Dynamic(d) => &d.default_value,
6810                };
6811                extra_blocks.insert_from_css_property(prop);
6812            }
6813        }
6814
6815        let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
6816        if !inline_css.is_empty() {
6817            let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
6818            let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
6819        }
6820        let _ = (&mut *css_blocks, matcher_hash); // retained for signature compat
6821    }
6822
6823    set_stringified_attributes(
6824        &mut dom_string,
6825        &node.attributes,
6826        &ComponentArgumentVec::new(),
6827        tabs,
6828    );
6829
6830    // Text folded into the ctor (Tier A/C) is skipped, as is a `<caption>`
6831    // already injected by `create_table`.
6832    let mut caption_skipped = false;
6833    let mut children_string = node
6834        .children
6835        .as_ref()
6836        .iter()
6837        .enumerate()
6838        .filter_map(|(child_idx, c)| match c {
6839            XmlNodeChild::Element(child_node) => {
6840                if ctor.skip_caption()
6841                    && !caption_skipped
6842                    && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
6843                {
6844                    caption_skipped = true;
6845                    return None;
6846                }
6847                let mut matcher = matcher.clone();
6848                matcher.path.push(CssPathSelector::Children);
6849                matcher.indices_in_parent.push(child_idx);
6850                matcher.children_length.push(node.children.len());
6851
6852                Some(compile_node_to_rust_code_inner(
6853                    child_node,
6854                    component_map,
6855                    tabs + 1,
6856                    extra_blocks,
6857                    css_blocks,
6858                    css,
6859                    matcher,
6860                ))
6861            }
6862            XmlNodeChild::Text(text) => {
6863                if ctor.consumes_text() {
6864                    return None;
6865                }
6866                let text = text.trim();
6867                if text.is_empty() {
6868                    None
6869                } else {
6870                    let t2 = String::from("    ").repeat(tabs);
6871                    let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
6872                    Some(Ok(format!(
6873                        "{t2}Dom::create_text(\"{escaped}\")"
6874                    )))
6875                }
6876            }
6877        })
6878        .collect::<Result<Vec<_>, _>>()?
6879        .join(",\r\n");
6880
6881    if !children_string.is_empty() {
6882        let _ = write!(dom_string,
6883            "\r\n{t2}.with_children(vec![\r\n{children_string}\r\n{t2}])"
6884        );
6885    }
6886
6887    Ok(dom_string)
6888}
6889
6890// ───────────────────────────────────────────────────────────────────────────
6891// Generic FLUENT DOM-builder emitter (C++ / Python).
6892//
6893// Rust has its own dedicated walker above (`compile_*_to_rust_code`). C++ and
6894// Python share this generic walker because their builder APIs are also fluent
6895// (`Dom::create_*().with_css(..).with_child(..)`); only the surface tokens
6896// differ, captured in `FluentSyntax`. Plain C is imperative and has its own
6897// walker (`compile_*_to_c_code`).
6898// ───────────────────────────────────────────────────────────────────────────
6899
6900/// Tags with a zero-arg per-tag creator (`create_<tag>()` / `AzDom_create<Tag>()`
6901/// / `create_node(NodeType::<Tag>)`). Interactive / data elements (Button, Input,
6902/// Img, Select, Textarea, Label, A, Table, …) take constructor arguments, so an
6903/// exported page maps them to a plain `div` container (structure preserved; the
6904/// user re-wires behavior). Keep these CamelCase to match `NodeTypeTag` debug names.
6905const SAFE_CONTAINER_TAGS: &[&str] = &[
6906    "Abbr", "Acronym", "Address", "Article", "Aside", "B", "Bdi", "Bdo", "Big",
6907    "Blockquote", "Body", "Br", "Caption", "Cite", "Code", "Colgroup", "Dd",
6908    "Del", "Dfn", "Dir", "Div", "Dl", "Dt", "Em", "Embed", "Figcaption",
6909    "Figure", "Footer", "H1", "H2", "H3", "H4", "H5", "H6", "Head", "Header",
6910    "Hr", "Html", "I", "Ins", "Kbd", "Li", "Link", "Main", "Map", "Mark",
6911    "Meta", "Nav", "Object", "Ol", "P", "Pre", "Q", "Rp", "Rt", "Rtc", "Ruby",
6912    "S", "Samp", "Script", "Section", "Small", "Span", "Strong", "Style", "Sub",
6913    "Sup", "Svg", "Tbody", "Td", "Tfoot", "Th", "Thead", "Title", "Tr", "U",
6914    "Ul", "Var", "Wbr",
6915];
6916
6917/// The CamelCase tag to actually emit a creator for: the tag itself if it has a
6918/// zero-arg creator, else `"Div"`.
6919fn safe_container_tag(tag_dbg: &str) -> &'static str {
6920    SAFE_CONTAINER_TAGS.iter().copied().find(|t| *t == tag_dbg).unwrap_or("Div")
6921}
6922
6923// ───────────────────────────────────────────────────────────────────────────
6924// Semantic / accessibility-aware constructor selection.
6925//
6926// Instead of mapping every element to a plain `div`, an exported live page
6927// picks the *most specific* Azul constructor so the generated app keeps the
6928// page's semantics + accessibility tree:
6929//
6930//   • Tier A  `create_<tag>_with_text(text)` — a tag with a single text child
6931//             and no element children (P, Span, H1-H6, Li, Td, Code, …).
6932//   • Tier B  aria-only / void widgets (Details, Summary, Form, Canvas, Area,
6933//             …) — `create_<tag>(SmallAriaInfo::label(..))` when `aria-label`
6934//             is present, else `create_<tag>_no_a11y()`.
6935//   • Tier C  multi-arg widgets (Button, A, Label, Input, Select, Option,
6936//             Optgroup, Textarea, Table) — args pulled from HTML attributes.
6937//   • Tier D  scalar-driven widgets (Progress, Meter, Dialog) — the `*_no_a11y`
6938//             form with extracted numeric args (the full aria structs are
6939//             complex; the NoA11y form is simplest + correct).
6940//
6941// Every symbol emitted here is verified to exist in `target/codegen/azul.h` (C)
6942// and `azul20.hpp` (C++); anything else falls back to `safe_container_tag`
6943// (`div`). The four walkers share `analyze_node_ctor` and each renders the
6944// result with its own surface tokens.
6945// ───────────────────────────────────────────────────────────────────────────
6946
6947/// A single positional argument of a semantic constructor. String payloads are
6948/// RAW — escaping happens at render time (matching the walkers).
6949#[derive(Debug, Clone)]
6950enum CtorArg {
6951    /// Plain string literal (`AzString` / `String` / `"…"`).
6952    Str(String),
6953    /// `SmallAriaInfo` built from an accessible label.
6954    Aria(String),
6955    /// `f32` numeric literal.
6956    Float(f32),
6957    /// `OptionString::Some(text)`.
6958    OptSome(String),
6959    /// `OptionString::None`.
6960    OptNone,
6961}
6962
6963/// The constructor chosen for an element node.
6964enum NodeCtor {
6965    /// Plain container — keep each walker's existing `create_<tag>()` path.
6966    Plain,
6967    /// A specific semantic constructor.
6968    Semantic {
6969        /// Canonical CamelCase suffix after `create` / `AzDom_create`
6970        /// (e.g. `Button`, `ButtonNoA11y`, `PWithText`, `A`, `ANoA11y`).
6971        suffix: String,
6972        args: Vec<CtorArg>,
6973        /// The node's direct text is folded into the ctor — skip text children
6974        /// in the walk so it isn't emitted twice.
6975        consumes_text: bool,
6976        /// The table aria form injects its own `<caption>` child — drop the
6977        /// first literal `<caption>` element so it isn't duplicated.
6978        skip_caption: bool,
6979    },
6980}
6981
6982/// Uppercase the first character (`button` → `Button`, `h1` → `H1`). HTML tags
6983/// are single lowercase tokens, so this yields the exact `AzDom_create<Suffix>`
6984/// spelling.
6985fn cap_first(tag: &str) -> String {
6986    let mut c = tag.chars();
6987    c.next().map_or_else(String::new, |f| f.to_uppercase().collect::<String>() + c.as_str())
6988}
6989
6990/// CamelCase → `snake_case` for the C++/Python/Rust method names
6991/// (`ButtonNoA11y` → `button_no_a11y`, `PWithText` → `p_with_text`,
6992/// `ANoA11y` → `a_no_a11y`, `H1WithText` → `h1_with_text`).
6993fn camel_to_snake(s: &str) -> String {
6994    let chars: Vec<char> = s.chars().collect();
6995    let mut out = String::new();
6996    for (i, &ch) in chars.iter().enumerate() {
6997        if ch.is_ascii_uppercase() && i > 0 {
6998            let prev = chars[i - 1];
6999            let next_lower = chars.get(i + 1).is_some_and(char::is_ascii_lowercase);
7000            if prev.is_ascii_lowercase()
7001                || prev.is_ascii_digit()
7002                || (prev.is_ascii_uppercase() && next_lower)
7003            {
7004                out.push('_');
7005            }
7006        }
7007        out.extend(ch.to_lowercase());
7008    }
7009    out
7010}
7011
7012/// Escape `\` and `"` for a double-quoted string literal.
7013fn esc_lit(s: &str) -> String {
7014    s.replace('\\', "\\\\").replace('"', "\\\"")
7015}
7016
7017/// Format an `f32` as a valid float literal with a decimal point (`1` → `1.0`).
7018fn fmt_f32_lit(f: f32) -> String {
7019    let s = format!("{f}");
7020    if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") {
7021        s
7022    } else {
7023        format!("{s}.0")
7024    }
7025}
7026
7027/// Joined, trimmed text of a node's *direct* text children (`"  Go  "` → `"Go"`).
7028fn node_direct_text(node: &XmlNode) -> String {
7029    node.children
7030        .as_ref()
7031        .iter()
7032        .filter_map(|c| match c {
7033            XmlNodeChild::Text(t) => {
7034                let t = t.trim();
7035                if t.is_empty() { None } else { Some(t.to_string()) }
7036            }
7037            XmlNodeChild::Element(_) => None,
7038        })
7039        .collect::<Vec<_>>()
7040        .join(" ")
7041}
7042
7043/// Non-empty `aria-label` attribute value, if present.
7044fn node_aria_label(node: &XmlNode) -> Option<String> {
7045    node.attributes.get_key("aria-label").and_then(|v| {
7046        let v = v.as_str().trim();
7047        if v.is_empty() { None } else { Some(v.to_string()) }
7048    })
7049}
7050
7051/// Attribute value, or `default` when absent.
7052fn node_attr_or(node: &XmlNode, key: &str, default: &str) -> String {
7053    node.attributes
7054        .get_key(key).map_or_else(|| default.to_string(), |v| v.as_str().to_string())
7055}
7056
7057/// Attribute parsed as `f32`, or `default` when absent / unparsable.
7058fn node_attr_f32(node: &XmlNode, key: &str, default: f32) -> f32 {
7059    node.attributes
7060        .get_key(key)
7061        .and_then(|v| v.as_str().trim().parse::<f32>().ok())
7062        .unwrap_or(default)
7063}
7064
7065/// Text of the node's first `<caption>` element child, if any (non-empty).
7066fn first_caption_text(node: &XmlNode) -> Option<String> {
7067    node.children.as_ref().iter().find_map(|c| match c {
7068        XmlNodeChild::Element(e) if e.node_type.as_str().eq_ignore_ascii_case("caption") => {
7069            let t = e.get_text_content();
7070            let t = t.trim();
7071            if t.is_empty() { None } else { Some(t.to_string()) }
7072        }
7073        _ => None,
7074    })
7075}
7076
7077/// Tags with a single-arg `create_<tag>_with_text(text)` constructor (Tier A).
7078const WITH_TEXT_TAGS: &[&str] = &[
7079    "acronym", "b", "bdi", "bdo", "big", "blockquote", "cite", "code", "del",
7080    "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "ins", "kbd", "li",
7081    "mark", "p", "pre", "rp", "rt", "s", "samp", "small", "span", "strong",
7082    "style", "sub", "sup", "td", "th", "title", "u", "var",
7083];
7084
7085/// Pick the semantic constructor for `tag` (lowercase HTML tag) + `node`.
7086#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
7087fn analyze_node_ctor(tag: &str, node: &XmlNode) -> NodeCtor {
7088    // Helper for the common "no caption skip" case.
7089    fn sem(suffix: impl Into<String>, args: Vec<CtorArg>, consumes_text: bool) -> NodeCtor {
7090        NodeCtor::Semantic {
7091            suffix: suffix.into(),
7092            args,
7093            consumes_text,
7094            skip_caption: false,
7095        }
7096    }
7097
7098    let aria = node_aria_label(node);
7099    let has_aria = aria.is_some();
7100    let label = aria.unwrap_or_default();
7101    // `has_only_text_children()` is also true for childless nodes; pair it with
7102    // `has_text` so empty elements stay plain containers.
7103    let pure_text = node.has_only_text_children();
7104    let text = node_direct_text(node);
7105    let has_text = !text.is_empty();
7106    let cap = cap_first(tag);
7107
7108    // Tier A — *_with_text (single text child, no element children).
7109    if WITH_TEXT_TAGS.contains(&tag) {
7110        if pure_text && has_text {
7111            return sem(format!("{cap}WithText"), vec![CtorArg::Str(text)], true);
7112        }
7113        return NodeCtor::Plain;
7114    }
7115
7116    match tag {
7117        // Tier B — aria-only / void widgets.
7118        "details" | "form" | "fieldset" | "legend" | "menu" | "output"
7119        | "datalist" | "canvas" | "audio" | "video" | "area" => {
7120            if has_aria {
7121                sem(cap, vec![CtorArg::Aria(label)], false)
7122            } else {
7123                sem(format!("{cap}NoA11y"), vec![], false)
7124            }
7125        }
7126        // Summary is Tier B but also has a WithText form for a single text child.
7127        "summary" => {
7128            if pure_text && has_text {
7129                if has_aria {
7130                    sem("SummaryWithText", vec![CtorArg::Str(text), CtorArg::Aria(label)], true)
7131                } else {
7132                    sem("SummaryWithTextNoA11y", vec![CtorArg::Str(text)], true)
7133                }
7134            } else if has_aria {
7135                sem("Summary", vec![CtorArg::Aria(label)], false)
7136            } else {
7137                sem("SummaryNoA11y", vec![], false)
7138            }
7139        }
7140
7141        // Tier C — multi-arg widgets (args from HTML attributes).
7142        "button" => {
7143            if has_aria {
7144                sem("Button", vec![CtorArg::Str(text), CtorArg::Aria(label)], true)
7145            } else {
7146                sem("ButtonNoA11y", vec![CtorArg::Str(text)], true)
7147            }
7148        }
7149        "a" => {
7150            let href = node_attr_or(node, "href", "");
7151            if has_aria {
7152                sem("A", vec![CtorArg::Str(href), CtorArg::Str(text), CtorArg::Aria(label)], true)
7153            } else {
7154                let lbl = if has_text { CtorArg::OptSome(text) } else { CtorArg::OptNone };
7155                sem("ANoA11y", vec![CtorArg::Str(href), lbl], true)
7156            }
7157        }
7158        "label" => {
7159            let for_id = node_attr_or(node, "for", "");
7160            if has_aria {
7161                sem("Label", vec![CtorArg::Str(for_id), CtorArg::Str(text), CtorArg::Aria(label)], true)
7162            } else {
7163                sem("LabelNoA11y", vec![CtorArg::Str(for_id), CtorArg::Str(text)], true)
7164            }
7165        }
7166        "input" => {
7167            let ty = node_attr_or(node, "type", "text");
7168            let name = node_attr_or(node, "name", "");
7169            if has_aria {
7170                sem("Input", vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7171            } else {
7172                sem("InputNoA11y", vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label)], false)
7173            }
7174        }
7175        "textarea" => {
7176            let name = node_attr_or(node, "name", "");
7177            if has_aria {
7178                sem("Textarea", vec![CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7179            } else {
7180                sem("TextareaNoA11y", vec![CtorArg::Str(name), CtorArg::Str(label)], false)
7181            }
7182        }
7183        "select" => {
7184            let name = node_attr_or(node, "name", "");
7185            if has_aria {
7186                sem("Select", vec![CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7187            } else {
7188                sem("SelectNoA11y", vec![CtorArg::Str(name), CtorArg::Str(label)], false)
7189            }
7190        }
7191        "option" => {
7192            let value = node_attr_or(node, "value", "");
7193            if has_aria {
7194                sem("Option", vec![CtorArg::Str(value), CtorArg::Str(text), CtorArg::Aria(label)], true)
7195            } else {
7196                sem("OptionNoA11y", vec![CtorArg::Str(value), CtorArg::Str(text)], true)
7197            }
7198        }
7199        "optgroup" => {
7200            let lbl = node_attr_or(node, "label", "");
7201            if has_aria {
7202                sem("Optgroup", vec![CtorArg::Str(lbl), CtorArg::Aria(label)], false)
7203            } else {
7204                sem("OptgroupNoA11y", vec![CtorArg::Str(lbl)], false)
7205            }
7206        }
7207        "table" => {
7208            if has_aria {
7209                // The aria form injects a caption child, so take the caption from
7210                // the literal <caption> (or the aria label) and drop the literal.
7211                let caption = first_caption_text(node).unwrap_or_else(|| label.clone());
7212                NodeCtor::Semantic {
7213                    suffix: "Table".to_string(),
7214                    args: vec![CtorArg::Str(caption), CtorArg::Aria(label)],
7215                    consumes_text: false,
7216                    skip_caption: true,
7217                }
7218            } else {
7219                sem("TableNoA11y", vec![], false)
7220            }
7221        }
7222
7223        // Tier D — scalar-driven widgets (NoA11y form with extracted numbers).
7224        "progress" => sem(
7225            "ProgressNoA11y",
7226            vec![
7227                CtorArg::Float(node_attr_f32(node, "value", 0.0)),
7228                CtorArg::Float(node_attr_f32(node, "max", 1.0)),
7229            ],
7230            false,
7231        ),
7232        "meter" => sem(
7233            "MeterNoA11y",
7234            vec![
7235                CtorArg::Float(node_attr_f32(node, "value", 0.0)),
7236                CtorArg::Float(node_attr_f32(node, "min", 0.0)),
7237                CtorArg::Float(node_attr_f32(node, "max", 1.0)),
7238            ],
7239            false,
7240        ),
7241        "dialog" => sem("DialogNoA11y", vec![], false),
7242
7243        _ => NodeCtor::Plain,
7244    }
7245}
7246
7247impl CtorArg {
7248    /// Rust expression for this argument (`AzString::from(..)` works for both the
7249    /// `Into<AzString>` and the concrete `AzString` parameter forms).
7250    fn render_rust(&self) -> String {
7251        match self {
7252            Self::Str(s) => format!("AzString::from(\"{}\")", esc_lit(s)),
7253            Self::Aria(s) => format!("SmallAriaInfo::label(AzString::from(\"{}\"))", esc_lit(s)),
7254            Self::Float(f) => fmt_f32_lit(*f),
7255            Self::OptSome(s) => format!("OptionString::Some(AzString::from(\"{}\"))", esc_lit(s)),
7256            Self::OptNone => "OptionString::None".to_string(),
7257        }
7258    }
7259    fn render_c(&self) -> String {
7260        match self {
7261            Self::Str(s) => format!("AZ_STR(\"{}\")", esc_lit(s)),
7262            Self::Aria(s) => format!("AzSmallAriaInfo_label(AZ_STR(\"{}\"))", esc_lit(s)),
7263            Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
7264            Self::OptSome(s) => format!("AzOptionString_some(AZ_STR(\"{}\"))", esc_lit(s)),
7265            Self::OptNone => "AzOptionString_none()".to_string(),
7266        }
7267    }
7268    fn render_cpp(&self) -> String {
7269        match self {
7270            Self::Str(s) => format!("String(\"{}\")", esc_lit(s)),
7271            Self::Aria(s) => format!("SmallAriaInfo::label(String(\"{}\"))", esc_lit(s)),
7272            Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
7273            Self::OptSome(s) => format!("OptionString::some(String(\"{}\"))", esc_lit(s)),
7274            Self::OptNone => "OptionString::none()".to_string(),
7275        }
7276    }
7277    fn render_python(&self) -> String {
7278        match self {
7279            Self::Str(s) => format!("\"{}\"", esc_lit(s)),
7280            Self::Aria(s) => format!("azul.SmallAriaInfo.label(\"{}\")", esc_lit(s)),
7281            Self::Float(f) => fmt_f32_lit(*f),
7282            Self::OptSome(s) => format!("azul.OptionString.some(\"{}\")", esc_lit(s)),
7283            Self::OptNone => "azul.OptionString.none()".to_string(),
7284        }
7285    }
7286}
7287
7288impl NodeCtor {
7289    const fn consumes_text(&self) -> bool {
7290        matches!(self, Self::Semantic { consumes_text: true, .. })
7291    }
7292    const fn skip_caption(&self) -> bool {
7293        matches!(self, Self::Semantic { skip_caption: true, .. })
7294    }
7295    /// `Dom::create_…(args)` for Rust, or `None` for a plain container.
7296    fn render_rust(&self) -> Option<String> {
7297        match self {
7298            Self::Plain => None,
7299            Self::Semantic { suffix, args, .. } => Some(format!(
7300                "Dom::create_{}({})",
7301                camel_to_snake(suffix),
7302                args.iter().map(CtorArg::render_rust).collect::<Vec<_>>().join(", ")
7303            )),
7304        }
7305    }
7306    /// `AzDom_create…(args)` for C, or `None` for a plain container.
7307    fn render_c(&self) -> Option<String> {
7308        match self {
7309            Self::Plain => None,
7310            Self::Semantic { suffix, args, .. } => Some(format!(
7311                "AzDom_create{}({})",
7312                suffix,
7313                args.iter().map(CtorArg::render_c).collect::<Vec<_>>().join(", ")
7314            )),
7315        }
7316    }
7317    /// Fluent `Dom::create_…` (C++) / `azul.Dom.create_…` (Python), or `None`.
7318    fn render_fluent(&self, target: &CompileTarget) -> Option<String> {
7319        match self {
7320            Self::Plain => None,
7321            Self::Semantic { suffix, args, .. } => {
7322                let snake = camel_to_snake(suffix);
7323                let (prefix, rendered) = match target {
7324                    CompileTarget::Cpp => (
7325                        format!("Dom::create_{snake}"),
7326                        args.iter().map(CtorArg::render_cpp).collect::<Vec<_>>(),
7327                    ),
7328                    CompileTarget::Python => (
7329                        format!("azul.Dom.create_{snake}"),
7330                        args.iter().map(CtorArg::render_python).collect::<Vec<_>>(),
7331                    ),
7332                    _ => return None,
7333                };
7334                Some(format!("{}({})", prefix, rendered.join(", ")))
7335            }
7336        }
7337    }
7338}
7339
7340/// Per-language token hooks for the fluent walker. The `&str` args are already
7341/// escaped for a double-quoted string literal.
7342struct FluentSyntax {
7343    target: CompileTarget,
7344    /// tag debug-name (e.g. "Div") -> full create expression
7345    create_node: fn(&str) -> String,
7346    /// escaped text -> create-text expression
7347    create_text: fn(&str) -> String,
7348    /// escaped css -> `.with_css(..)` call
7349    with_css: fn(&str) -> String,
7350    /// escaped class -> `.with_class(..)` call
7351    with_class: fn(&str) -> String,
7352    /// escaped id -> `.with_id(..)` call
7353    with_id: fn(&str) -> String,
7354    /// escaped child expression -> `.with_child(..)` call (children are chained)
7355    with_child: fn(&str) -> String,
7356}
7357
7358const CPP_SYNTAX: FluentSyntax = FluentSyntax {
7359    target: CompileTarget::Cpp,
7360    // Use per-tag creators (Dom::create_div(), create_p(), create_body(), …)
7361    // — `NodeType` is a tagged union, so `create_node` would need union
7362    // construction; the per-tag creators exist for every common HTML element.
7363    create_node: |tag| alloc::format!("Dom::create_{}()", tag.to_lowercase()),
7364    create_text: |s| alloc::format!("Dom::create_text(String(\"{s}\"))"),
7365    with_css: |s| alloc::format!(".with_css(String(\"{s}\"))"),
7366    with_class: |s| alloc::format!(".with_class(String(\"{s}\"))"),
7367    with_id: |s| alloc::format!(".with_id(String(\"{s}\"))"),
7368    with_child: |c| alloc::format!(".with_child({c})"),
7369};
7370
7371const PYTHON_SYNTAX: FluentSyntax = FluentSyntax {
7372    target: CompileTarget::Python,
7373    // Per-tag creators (azul.Dom.create_div(), …) — see CPP_SYNTAX note.
7374    create_node: |tag| alloc::format!("azul.Dom.create_{}()", tag.to_lowercase()),
7375    create_text: |s| alloc::format!("azul.Dom.create_text(\"{s}\")"),
7376    with_css: |s| alloc::format!(".with_css(\"{s}\")"),
7377    with_class: |s| alloc::format!(".with_class(\"{s}\")"),
7378    with_id: |s| alloc::format!(".with_id(\"{s}\")"),
7379    with_child: |c| alloc::format!(".with_child({c})"),
7380};
7381
7382/// Walk one element node, emitting a fluent create-expression for `syntax`'s
7383/// language. Mirrors `compile_node_to_rust_code_inner` but token-parameterized.
7384#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7385// See compile_node_to_rust_code_inner: component_map is forwarded for codegen-path parity.
7386#[allow(clippy::only_used_in_recursion)]
7387fn compile_node_fluent(
7388    node: &XmlNode,
7389    syntax: &FluentSyntax,
7390    component_map: &ComponentMap,
7391    css: &Css,
7392    mut matcher: CssMatcher,
7393) -> Result<String, CompileError> {
7394    use azul_css::css::CssDeclaration;
7395
7396    let component_name = normalize_casing(&node.node_type);
7397    let node_type_tag = tag_to_node_type_tag(&component_name);
7398    let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
7399
7400    // Base create-expression. For an exported live page every node is a plain
7401    // HTML element, so emit a per-tag creator directly via the language hooks
7402    // (universal + verified) rather than the per-component `compile_fn`, whose
7403    // C++/Python arms emit stale placeholder syntax (`Dom.div()` etc.).
7404    // Interactive/data tags (whose creators need args) fall back to `div`. Any
7405    // element text shows up as a Text child below and is handled there.
7406    let ctor = analyze_node_ctor(&component_name, node);
7407    let mut s = ctor.render_fluent(&syntax.target).map_or_else(|| (syntax.create_node)(safe_container_tag(&tag_dbg)), |expr| expr);
7408
7409    matcher.path.push(CssPathSelector::Type(node_type_tag));
7410    let ids: Vec<String> = node.attributes.get_key("id")
7411        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7412        .unwrap_or_default();
7413    matcher.path.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
7414    let classes: Vec<String> = node.attributes.get_key("class")
7415        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7416        .unwrap_or_default();
7417    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7418
7419    // Inline CSS (matched rules -> `.with_css("..")`, pseudo blocks included).
7420    let blocks = get_css_blocks(css, &matcher);
7421    if !blocks.is_empty() {
7422        let inline_css = css_blocks_to_inline_string(&blocks);
7423        if !inline_css.is_empty() {
7424            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7425            s.push_str(&(syntax.with_css)(&esc));
7426        }
7427    }
7428    for id in &ids {
7429        s.push_str(&(syntax.with_id)(&id.replace('\\', "\\\\").replace('"', "\\\"")));
7430    }
7431    for class in &classes {
7432        s.push_str(&(syntax.with_class)(&class.replace('\\', "\\\\").replace('"', "\\\"")));
7433    }
7434
7435    // Children (chained `.with_child(..)`). Text folded into the ctor (Tier A/C)
7436    // is skipped here, as is a `<caption>` already injected by `create_table`.
7437    let mut caption_skipped = false;
7438    for (child_idx, child) in node.children.as_ref().iter().enumerate() {
7439        match child {
7440            XmlNodeChild::Element(child_node) => {
7441                if ctor.skip_caption()
7442                    && !caption_skipped
7443                    && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
7444                {
7445                    caption_skipped = true;
7446                    continue;
7447                }
7448                let mut m = matcher.clone();
7449                m.path.push(CssPathSelector::Children);
7450                m.indices_in_parent.push(child_idx);
7451                m.children_length.push(node.children.len());
7452                let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
7453                s.push_str(&(syntax.with_child)(&child_src));
7454            }
7455            XmlNodeChild::Text(text) => {
7456                if ctor.consumes_text() {
7457                    continue;
7458                }
7459                let text = text.trim();
7460                if !text.is_empty() {
7461                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7462                    s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
7463                }
7464            }
7465        }
7466    }
7467
7468    Ok(s)
7469}
7470
7471/// Build the `<body>` render-expression for `syntax`'s language.
7472#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7473fn compile_body_fluent<'a>(
7474    body_node: &'a XmlNode,
7475    syntax: &FluentSyntax,
7476    component_map: &'a ComponentMap,
7477    css: &Css,
7478    mut matcher: CssMatcher,
7479) -> Result<String, CompileError> {
7480    let mut s = (syntax.create_node)("Body");
7481    matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
7482    let classes: Vec<String> = body_node.attributes.get_key("class")
7483        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7484        .unwrap_or_default();
7485    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7486
7487    let blocks = get_css_blocks(css, &matcher);
7488    if !blocks.is_empty() {
7489        let inline_css = css_blocks_to_inline_string(&blocks);
7490        if !inline_css.is_empty() {
7491            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7492            s.push_str(&(syntax.with_css)(&esc));
7493        }
7494    }
7495    for class in &classes {
7496        s.push_str(&(syntax.with_class)(&class.replace('\\', "\\\\").replace('"', "\\\"")));
7497    }
7498
7499    for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
7500        match child {
7501            XmlNodeChild::Element(child_node) => {
7502                let mut m = matcher.clone();
7503                m.path.push(CssPathSelector::Children);
7504                m.indices_in_parent.push(child_idx);
7505                m.children_length.push(body_node.children.len());
7506                let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
7507                s.push_str(&(syntax.with_child)(&child_src));
7508            }
7509            XmlNodeChild::Text(text) => {
7510                let text = text.trim();
7511                if !text.is_empty() {
7512                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7513                    s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
7514                }
7515            }
7516        }
7517    }
7518    Ok(s)
7519}
7520
7521/// Parse the page's `<style>` and seed a matcher rooted at `<body>`. Shared by
7522/// the C++/Python/C entry points (mirrors the head of `str_to_rust_code`).
7523#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7524fn parse_page_style_and_body(
7525    root_nodes: &[XmlNodeChild],
7526) -> Result<(Css, &XmlNode), CompileError> {
7527    let html_node = get_html_node(root_nodes)?;
7528    let body_node = get_body_node(html_node.children.as_ref())?;
7529    let mut global_style = Css::empty();
7530    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
7531        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
7532            let text = style_node.get_text_content();
7533            if !text.is_empty() {
7534                global_style = azul_css::parser2::new_from_str(&text).0;
7535            }
7536        }
7537    }
7538    global_style.sort_by_specificity();
7539    Ok((global_style, body_node))
7540}
7541
7542fn body_matcher(body_node: &XmlNode) -> CssMatcher {
7543    CssMatcher {
7544        path: Vec::new(),
7545        indices_in_parent: vec![0],
7546        children_length: vec![body_node.children.as_ref().len()],
7547    }
7548}
7549
7550/// Compile a full HTML page to a compilable **C++** Azul app.
7551#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7552/// # Errors
7553///
7554/// Returns an error if the XML cannot be parsed or compiled to C++ code.
7555pub fn str_to_cpp_code<'a>(
7556    root_nodes: &'a [XmlNodeChild],
7557    component_map: &'a ComponentMap,
7558) -> Result<String, CompileError> {
7559    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7560    let render = compile_body_fluent(body_node, &CPP_SYNTAX, component_map, &global_style, body_matcher(body_node))?;
7561    Ok(alloc::format!(
7562        "// Auto-generated UI source code (C++). Build:\n\
7563         //   clang++ -std=c++20 -I <azul>/target/codegen main.cpp -lazul\n\
7564         #include \"azul20.hpp\"\n\
7565         using namespace azul;\n\n\
7566         struct Data {{}};\n\n\
7567         AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n    \
7568         return {render};\n}}\n\n\
7569         int main() {{\n    \
7570         RefAny data = RefAny::create(Data{{}});\n    \
7571         WindowCreateOptions window = WindowCreateOptions::create(render);\n    \
7572         App app = App::create(std::move(data), AppConfig::default_());\n    \
7573         app.run(std::move(window));\n    \
7574         return 0;\n}}\n"
7575    ))
7576}
7577
7578/// Compile a full HTML page to a compilable **Python** Azul app.
7579#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7580/// # Errors
7581///
7582/// Returns an error if the XML cannot be parsed or compiled to Python code.
7583pub fn str_to_python_code<'a>(
7584    root_nodes: &'a [XmlNodeChild],
7585    component_map: &'a ComponentMap,
7586) -> Result<String, CompileError> {
7587    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7588    let render = compile_body_fluent(body_node, &PYTHON_SYNTAX, component_map, &global_style, body_matcher(body_node))?;
7589    Ok(alloc::format!(
7590        "# Auto-generated UI source code (Python). Run: python3 main.py\n\
7591         import azul\n\n\
7592         class Data:\n    pass\n\n\
7593         def render(data, info):\n    return (\n        {}\n    )\n\n\
7594         def main():\n    \
7595         app = azul.App.create(Data(), azul.AppConfig.create())\n    \
7596         window = azul.WindowCreateOptions.create(render)\n    \
7597         app.run(window)\n\n\
7598         if __name__ == \"__main__\":\n    main()\n",
7599        render.replace("\r\n", "\n        ")
7600    ))
7601}
7602
7603// ───────────────────────────────────────────────────────────────────────────
7604// Imperative C emitter. C has no fluent builder: each node is a statement that
7605// creates an `AzDom` local, applies css/class (by-value, returns), and pushes
7606// children via `AzDom_addChild(&parent, child)`. A recursive walk emits the
7607// statements bottom-up and returns the variable name holding each node.
7608// ───────────────────────────────────────────────────────────────────────────
7609
7610/// C per-tag creator suffix: `NodeTypeTag` debug name with first char kept and
7611/// the rest lowercased (`Div`->`Div`, `BlockQuote`->`Blockquote`, `H1`->`H1`),
7612/// matching `AzDom_create<Suffix>` in azul.h.
7613fn c_creator_suffix(tag_dbg: &str) -> String {
7614    let mut chars = tag_dbg.chars();
7615    chars.next().map_or_else(|| "Div".to_string(), |first| {
7616            let rest: String = chars.as_str().to_lowercase();
7617            alloc::format!("{first}{rest}")
7618        })
7619}
7620
7621#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7622fn compile_node_c(
7623    node: &XmlNode,
7624    component_map: &ComponentMap,
7625    css: &Css,
7626    mut matcher: CssMatcher,
7627    counter: &mut usize,
7628    out: &mut String,
7629) -> Result<String, CompileError> {
7630    let _ = component_map;
7631    let component_name = normalize_casing(&node.node_type);
7632    let node_type_tag = tag_to_node_type_tag(&component_name);
7633    let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
7634
7635    let var = alloc::format!("n{}", *counter);
7636    *counter += 1;
7637    let ctor = analyze_node_ctor(&component_name, node);
7638    match ctor.render_c() {
7639        Some(expr) => { let _ = writeln!(out, "    AzDom {var} = {expr};"); },
7640        None => { let _ = writeln!(out,
7641            "    AzDom {} = AzDom_create{}();",
7642            var,
7643            c_creator_suffix(safe_container_tag(&tag_dbg))
7644        ); },
7645    }
7646
7647    matcher.path.push(CssPathSelector::Type(node_type_tag));
7648    let ids: Vec<String> = node.attributes.get_key("id")
7649        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7650        .unwrap_or_default();
7651    matcher.path.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
7652    let classes: Vec<String> = node.attributes.get_key("class")
7653        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7654        .unwrap_or_default();
7655    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7656
7657    let blocks = get_css_blocks(css, &matcher);
7658    if !blocks.is_empty() {
7659        let inline_css = css_blocks_to_inline_string(&blocks);
7660        if !inline_css.is_empty() {
7661            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7662            let _ = writeln!(out, "    {var} = AzDom_withCss({var}, AZ_STR(\"{esc}\"));");
7663        }
7664    }
7665    for id in &ids {
7666        let esc = id.replace('\\', "\\\\").replace('"', "\\\"");
7667        let _ = writeln!(out, "    {var} = AzDom_withId({var}, AZ_STR(\"{esc}\"));");
7668    }
7669    for class in &classes {
7670        let esc = class.replace('\\', "\\\\").replace('"', "\\\"");
7671        let _ = writeln!(out, "    {var} = AzDom_withClass({var}, AZ_STR(\"{esc}\"));");
7672    }
7673
7674    let mut caption_skipped = false;
7675    for (child_idx, child) in node.children.as_ref().iter().enumerate() {
7676        match child {
7677            XmlNodeChild::Element(child_node) => {
7678                if ctor.skip_caption()
7679                    && !caption_skipped
7680                    && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
7681                {
7682                    caption_skipped = true;
7683                    continue;
7684                }
7685                let mut m = matcher.clone();
7686                m.path.push(CssPathSelector::Children);
7687                m.indices_in_parent.push(child_idx);
7688                m.children_length.push(node.children.len());
7689                let child_var = compile_node_c(child_node, component_map, css, m, counter, out)?;
7690                let _ = writeln!(out, "    AzDom_addChild(&{var}, {child_var});");
7691            }
7692            XmlNodeChild::Text(text) => {
7693                if ctor.consumes_text() {
7694                    continue;
7695                }
7696                let text = text.trim();
7697                if !text.is_empty() {
7698                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7699                    let _ = writeln!(out,
7700                        "    AzDom_addChild(&{var}, AzDom_createText(AZ_STR(\"{esc}\")));"
7701                    );
7702                }
7703            }
7704        }
7705    }
7706    Ok(var)
7707}
7708
7709/// Compile a full HTML page to a compilable **C** Azul app.
7710#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7711/// # Errors
7712///
7713/// Returns an error if the XML cannot be parsed or compiled to C code.
7714pub fn str_to_c_code<'a>(
7715    root_nodes: &'a [XmlNodeChild],
7716    component_map: &'a ComponentMap,
7717) -> Result<String, CompileError> {
7718    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7719    let mut body = String::new();
7720    let mut counter = 0usize;
7721
7722    // Emit the body as the root node, then its children.
7723    let root = alloc::format!("n{counter}");
7724    counter += 1;
7725    let _ = writeln!(body, "    AzDom {root} = AzDom_createBody();");
7726
7727    let mut matcher = body_matcher(body_node);
7728    matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
7729    let classes: Vec<String> = body_node.attributes.get_key("class")
7730        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7731        .unwrap_or_default();
7732    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7733    let blocks = get_css_blocks(&global_style, &matcher);
7734    if !blocks.is_empty() {
7735        let inline_css = css_blocks_to_inline_string(&blocks);
7736        if !inline_css.is_empty() {
7737            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7738            let _ = writeln!(body, "    {root} = AzDom_withCss({root}, AZ_STR(\"{esc}\"));");
7739        }
7740    }
7741    for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
7742        match child {
7743            XmlNodeChild::Element(child_node) => {
7744                let mut m = matcher.clone();
7745                m.path.push(CssPathSelector::Children);
7746                m.indices_in_parent.push(child_idx);
7747                m.children_length.push(body_node.children.len());
7748                let child_var = compile_node_c(child_node, component_map, &global_style, m, &mut counter, &mut body)?;
7749                let _ = writeln!(body, "    AzDom_addChild(&{root}, {child_var});");
7750            }
7751            XmlNodeChild::Text(text) => {
7752                let text = text.trim();
7753                if !text.is_empty() {
7754                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7755                    let _ = writeln!(body,
7756                        "    AzDom_addChild(&{root}, AzDom_createText(AZ_STR(\"{esc}\")));"
7757                    );
7758                }
7759            }
7760        }
7761    }
7762
7763    Ok(alloc::format!(
7764        "/* Auto-generated UI source code (C). Build:\n\
7765         *   clang -I <azul>/target/codegen main.c -lazul\n */\n\
7766         #include \"azul.h\"\n\
7767         #include <string.h>\n\
7768         #define AZ_STR(s) AzString_copyFromBytes((const uint8_t*)(s), 0, strlen(s))\n\n\
7769         AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n\
7770         {body}    return {root};\n}}\n\n\
7771         int main(void) {{\n    \
7772         AzString data_type = AZ_STR(\"Data\");\n    \
7773         AzRefAny data = AzRefAny_newC((AzGlVoidPtrConst){{ .ptr = NULL }}, 0, 1, 0, data_type, NULL, 0, 0);\n    \
7774         AzApp app = AzApp_create(data, AzAppConfig_create());\n    \
7775         AzWindowCreateOptions window = AzWindowCreateOptions_create(render);\n    \
7776         AzApp_run(&app, window);\n    \
7777         AzApp_delete(&app);\n    \
7778         return 0;\n}}\n"
7779    ))
7780}
7781
7782#[cfg(test)]
7783mod tests {
7784    use super::*;
7785    use crate::dom::{Dom, NodeType};
7786
7787    #[test]
7788    fn test_inline_span_parsing() {
7789        // This test verifies that HTML with inline spans is parsed correctly
7790        // The DOM structure should preserve text nodes before, inside, and after the span
7791
7792        let html = r#"<p>Text before <span class="highlight">inline text</span> text after.</p>"#;
7793
7794        // Expected DOM structure:
7795        // <p>
7796        //   ├─ TextNode: "Text before "
7797        //   ├─ <span class="highlight">
7798        //   │   └─ TextNode: "inline text"
7799        //   └─ TextNode: " text after."
7800
7801        // For this test, we'll create the DOM structure manually
7802        // since we're testing the parsing logic
7803        let expected_dom = Dom::create_p().with_children(
7804            vec![
7805                Dom::create_text("Text before "),
7806                Dom::create_node(NodeType::Span)
7807                    .with_children(vec![Dom::create_text("inline text")].into()),
7808                Dom::create_text(" text after."),
7809            ]
7810            .into(),
7811        );
7812
7813        // Verify the structure has 3 children at the top level
7814        assert_eq!(expected_dom.children.as_ref().len(), 3);
7815
7816        // Verify the middle child is a span
7817        match &expected_dom.children.as_ref()[1].root.node_type {
7818            NodeType::Span => {}
7819            other => panic!("Expected Span, got {:?}", other),
7820        }
7821
7822        // Verify the span has 1 child (the text node)
7823        assert_eq!(expected_dom.children.as_ref()[1].children.as_ref().len(), 1);
7824
7825        println!("Test passed: Inline span parsing structure is correct");
7826    }
7827
7828    #[test]
7829    fn test_xml_node_structure() {
7830        // Test the basic XmlNode structure to ensure text content is preserved
7831        // Updated to use XmlNodeChild enum (Text/Element)
7832
7833        let node = XmlNode {
7834            node_type: "p".into(),
7835            attributes: XmlAttributeMap {
7836                inner: StringPairVec::from_const_slice(&[]),
7837            },
7838            children: vec![
7839                XmlNodeChild::Text("Before ".into()),
7840                XmlNodeChild::Element(XmlNode {
7841                    node_type: "span".into(),
7842                    children: vec![XmlNodeChild::Text("inline".into())].into(),
7843                    ..Default::default()
7844                }),
7845                XmlNodeChild::Text(" after".into()),
7846            ]
7847            .into(),
7848        };
7849
7850        // Verify structure
7851        assert_eq!(node.children.as_ref().len(), 3);
7852        assert_eq!(node.children.as_ref()[0].as_text(), Some("Before "));
7853        assert_eq!(
7854            node.children.as_ref()[1]
7855                .as_element()
7856                .unwrap()
7857                .node_type
7858                .as_str(),
7859            "span"
7860        );
7861        assert_eq!(node.children.as_ref()[2].as_text(), Some(" after"));
7862
7863        // Verify span's child
7864        let span = node.children.as_ref()[1].as_element().unwrap();
7865        assert_eq!(span.children.as_ref().len(), 1);
7866        assert_eq!(span.children.as_ref()[0].as_text(), Some("inline"));
7867
7868        println!("Test passed: XmlNode structure preserves text nodes correctly");
7869    }
7870
7871    #[test]
7872    fn test_img_tag_becomes_image_node_with_src_tag() {
7873        // `<img src="cat.jpg" width="300" height="169">` must become a
7874        // `NodeType::Image` whose `NullImage` carries the `src` string as its
7875        // `tag` (so a renderer can resolve the bytes later), plus the declared
7876        // intrinsic size.
7877        use crate::resources::DecodedImage;
7878        use crate::window::{AzStringPair, StringPairVec};
7879
7880        let img_node = XmlNode {
7881            node_type: "img".into(),
7882            attributes: XmlAttributeMap::from(StringPairVec::from_vec(alloc::vec![
7883                AzStringPair {
7884                    key: "src".into(),
7885                    value: "cat.jpg".into()
7886                },
7887                AzStringPair {
7888                    key: "width".into(),
7889                    value: "300".into()
7890                },
7891                AzStringPair {
7892                    key: "height".into(),
7893                    value: "169".into()
7894                },
7895            ])),
7896            children: Vec::new().into(),
7897        };
7898
7899        let component_map = ComponentMap::default();
7900        let dom = xml_node_to_dom_fast(&img_node, &component_map, false, 0)
7901            .expect("xml_node_to_dom_fast for <img> should succeed");
7902
7903        match dom.root.get_node_type() {
7904            NodeType::Image(image_ref) => match image_ref.as_ref().get_data() {
7905                DecodedImage::NullImage {
7906                    tag, width, height, ..
7907                } => {
7908                    assert_eq!(
7909                        core::str::from_utf8(tag).unwrap(),
7910                        "cat.jpg",
7911                        "image tag must carry the src string"
7912                    );
7913                    assert_eq!(*width, 300, "width attribute should set intrinsic width");
7914                    assert_eq!(*height, 169, "height attribute should set intrinsic height");
7915                }
7916                other => panic!("expected NullImage carrying the src tag, got {:?}", other),
7917            },
7918            other => panic!("expected NodeType::Image for <img>, got {:?}", other),
7919        }
7920
7921        println!("Test passed: <img src=\"cat.jpg\"> -> NodeType::Image tagged \"cat.jpg\"");
7922    }
7923
7924    #[test]
7925    fn test_tag_to_node_type_img_is_image() {
7926        // The bare tag mapping should also yield an Image (placeholder, empty tag).
7927        match tag_to_node_type("img") {
7928            NodeType::Image(_) => {}
7929            other => panic!("tag_to_node_type(\"img\") should be Image, got {:?}", other),
7930        }
7931    }
7932
7933    /// Build a `<div>` nested `depth` levels deep, innermost first.
7934    fn nested_divs(depth: usize) -> XmlNode {
7935        let mut node = XmlNode {
7936            node_type: "div".into(),
7937            ..Default::default()
7938        };
7939        for _ in 0..depth {
7940            node = XmlNode {
7941                node_type: "div".into(),
7942                children: vec![XmlNodeChild::Element(node)].into(),
7943                ..Default::default()
7944            };
7945        }
7946        node
7947    }
7948
7949    /// AUDIT 2026-07-08: `extract_css_urls` used to slice the original string with
7950    /// a byte offset computed in a `to_lowercase()` temporary. On `'İ'` (whose
7951    /// lowercase is longer in bytes) that offset was misaligned. This must no
7952    /// longer panic and must still find the `@import` target.
7953    #[test]
7954    fn extract_css_urls_unicode_import_no_panic() {
7955        let mut res = Vec::new();
7956        Xml::extract_css_urls("İ@import 'x'", &mut res);
7957        assert_eq!(res.len(), 1, "should find the one @import target");
7958        assert_eq!(res[0].url.as_str(), "x");
7959    }
7960
7961    /// The `url(` and `@import` scans are case-insensitive after the audit fix.
7962    #[test]
7963    fn extract_css_urls_is_case_insensitive() {
7964        let mut res = Vec::new();
7965        Xml::extract_css_urls("body { background: URL(http://e.com/a.png); }", &mut res);
7966        assert_eq!(res.len(), 1);
7967        assert_eq!(res[0].url.as_str(), "http://e.com/a.png");
7968
7969        let mut res2 = Vec::new();
7970        Xml::extract_css_urls("@IMPORT \"theme.css\";", &mut res2);
7971        assert_eq!(res2.len(), 1);
7972        assert_eq!(res2[0].url.as_str(), "theme.css");
7973    }
7974
7975    /// AUDIT 2026-07-08: the resource scan recurses per nesting level; deep markup
7976    /// must not overflow the stack (deeper-than-cap subtrees are just not scanned).
7977    #[test]
7978    fn scan_external_resources_deep_nesting_ok() {
7979        let xml = Xml {
7980            root: vec![XmlNodeChild::Element(nested_divs(2000))].into(),
7981        };
7982        // Must simply return (no stack overflow); no resources in a plain tree.
7983        drop(xml.scan_external_resources());
7984    }
7985
7986    /// AUDIT 2026-07-08: the fast + tree DOM builders recurse per nesting level;
7987    /// deep markup must not overflow the stack (children beyond the cap are
7988    /// dropped, but the call returns `Ok`).
7989    #[test]
7990    fn xml_node_to_dom_fast_deep_nesting_ok() {
7991        let deep = nested_divs(2000);
7992        let component_map = ComponentMap::default();
7993        let dom = xml_node_to_dom_fast(&deep, &component_map, false, 0);
7994        assert!(dom.is_ok(), "deep DOM build must not overflow the stack");
7995
7996        let mut builder = CompactDomBuilder::new();
7997        let fast = xml_node_to_fast_dom(&deep, &component_map, false, &mut builder, 0);
7998        assert!(fast.is_ok(), "deep FastDom build must not overflow the stack");
7999    }
8000
8001    /// AUDIT 2026-07-08: `ComponentFieldType::parse` recurses through `Option<..>`
8002    /// / `Vec<..>` wrappers; an over-deep type string is rejected rather than
8003    /// overflowing the stack, while ordinary nesting still parses.
8004    #[test]
8005    fn component_field_type_parse_depth_capped() {
8006        let deep = format!("{}Bool{}", "Option<".repeat(4000), ">".repeat(4000));
8007        assert!(
8008            ComponentFieldType::parse(&deep).is_none(),
8009            "over-deep type string must be rejected, not overflow"
8010        );
8011
8012        let shallow = format!("{}Bool{}", "Option<".repeat(8), ">".repeat(8));
8013        assert!(
8014            ComponentFieldType::parse(&shallow).is_some(),
8015            "ordinary nesting must still parse"
8016        );
8017    }
8018
8019    /// AUDIT 2026-07-08: `prepare_string` now decodes the full common entity set
8020    /// plus numeric references, in a single pass so `&amp;` cannot double-decode.
8021    #[test]
8022    fn prepare_string_entity_decoding() {
8023        assert_eq!(prepare_string("a &amp; b"), "a & b");
8024        // `&amp;lt;` must yield the literal text "&lt;", not "<".
8025        assert_eq!(prepare_string("&amp;lt;"), "&lt;");
8026        assert_eq!(prepare_string("&quot;hi&quot;"), "\"hi\"");
8027        assert_eq!(prepare_string("&#65;&#66;"), "AB");
8028        assert_eq!(prepare_string("&#x41;"), "A");
8029        // Existing behavior preserved.
8030        assert_eq!(prepare_string("&lt;tag&gt;"), "<tag>");
8031    }
8032}
8033
8034#[cfg(test)]
8035#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
8036mod autotest_generated {
8037    use super::*;
8038    use crate::dom::{NodeData, NodeType};
8039    use azul_css::css::{CssNthChildPattern, CssNthChildSelector};
8040
8041    // ----------------------------------------------------------------- helpers
8042
8043    fn attrs(kv: &[(&str, &str)]) -> XmlAttributeMap {
8044        XmlAttributeMap::from(StringPairVec::from_vec(
8045            kv.iter()
8046                .map(|(k, v)| AzStringPair {
8047                    key: AzString::from(*k),
8048                    value: AzString::from(*v),
8049                })
8050                .collect::<Vec<_>>(),
8051        ))
8052    }
8053
8054    fn node(tag: &str, kv: &[(&str, &str)], children: Vec<XmlNodeChild>) -> XmlNode {
8055        XmlNode {
8056            node_type: tag.into(),
8057            attributes: attrs(kv),
8058            children: children.into(),
8059        }
8060    }
8061
8062    fn txt(s: &str) -> XmlNodeChild {
8063        XmlNodeChild::Text(AzString::from(s))
8064    }
8065
8066    fn elem(n: XmlNode) -> XmlNodeChild {
8067        XmlNodeChild::Element(n)
8068    }
8069
8070    /// `<html><head><style>{css}</style></head><body>{children}</body></html>`
8071    fn doc(css: &str, body_children: Vec<XmlNodeChild>) -> Vec<XmlNodeChild> {
8072        let style = node("style", &[], vec![txt(css)]);
8073        let head = node("head", &[], vec![elem(style)]);
8074        let body = node("body", &[], body_children);
8075        vec![elem(node("html", &[], vec![elem(head), elem(body)]))]
8076    }
8077
8078    fn no_args() -> ComponentArgumentVec {
8079        ComponentArgumentVec::from_const_slice(&[])
8080    }
8081
8082    fn dm(name: &str, fields: Vec<ComponentDataField>) -> ComponentDataModel {
8083        ComponentDataModel {
8084            name: AzString::from(name),
8085            description: AzString::from_const_str(""),
8086            fields: fields.into(),
8087        }
8088    }
8089
8090    fn user_def(css: &str, fields: Vec<ComponentDataField>) -> ComponentDef {
8091        ComponentDef {
8092            id: ComponentId::new("mylib", "widget"),
8093            display_name: AzString::from_const_str("Widget"),
8094            description: AzString::from_const_str(""),
8095            css: AzString::from(css),
8096            source: ComponentSource::UserDefined,
8097            data_model: dm("WidgetData", fields),
8098            render_fn: user_defined_render_fn,
8099            compile_fn: user_defined_compile_fn,
8100            render_fn_source: None.into(),
8101            compile_fn_source: None.into(),
8102        }
8103    }
8104
8105    /// A string that is long enough to smoke out O(n^2) / allocation blowups but
8106    /// still finishes fast in a debug-profile test run.
8107    const LONG: usize = 200_000;
8108
8109    // ================================================================
8110    // Xml::extract_url_value  (parser)
8111    // ================================================================
8112
8113    #[test]
8114    fn extract_url_value_empty_and_whitespace() {
8115        assert_eq!(Xml::extract_url_value(""), None);
8116        assert_eq!(Xml::extract_url_value("   "), None);
8117        assert_eq!(Xml::extract_url_value("\t\n\r "), None);
8118    }
8119
8120    #[test]
8121    fn extract_url_value_valid_minimal() {
8122        assert_eq!(
8123            Xml::extract_url_value("a.png)"),
8124            Some("a.png".to_string()),
8125            "unquoted url terminated by ')'"
8126        );
8127        assert_eq!(
8128            Xml::extract_url_value("\"a.png\")"),
8129            Some("a.png".to_string()),
8130            "double-quoted url"
8131        );
8132        assert_eq!(
8133            Xml::extract_url_value("'a.png')"),
8134            Some("a.png".to_string()),
8135            "single-quoted url"
8136        );
8137    }
8138
8139    #[test]
8140    fn extract_url_value_leading_trailing_junk_is_trimmed() {
8141        // Leading whitespace is trimmed by `trim_start`, inner padding by `trim`.
8142        assert_eq!(
8143            Xml::extract_url_value("   a.png   )tail"),
8144            Some("a.png".to_string())
8145        );
8146    }
8147
8148    #[test]
8149    fn extract_url_value_garbage_returns_none() {
8150        // Unterminated quote / no closing paren => None, never a panic.
8151        assert_eq!(Xml::extract_url_value("\"unterminated"), None);
8152        assert_eq!(Xml::extract_url_value("'unterminated"), None);
8153        assert_eq!(Xml::extract_url_value("no-closing-paren"), None);
8154        assert_eq!(Xml::extract_url_value("\u{0}\u{1}\u{7f}"), None);
8155    }
8156
8157    #[test]
8158    fn extract_url_value_boundary_numbers() {
8159        for s in [
8160            "0)",
8161            "-0)",
8162            "9223372036854775807)",
8163            "-9223372036854775808)",
8164            "NaN)",
8165            "inf)",
8166            "1e400)",
8167        ] {
8168            let got = Xml::extract_url_value(s);
8169            assert!(got.is_some(), "numeric-looking url {s:?} is still a url");
8170        }
8171        assert_eq!(Xml::extract_url_value("0)"), Some("0".to_string()));
8172    }
8173
8174    #[test]
8175    fn extract_url_value_unicode_no_panic() {
8176        // The ')' scan must land on a char boundary of the ORIGINAL string.
8177        assert_eq!(
8178            Xml::extract_url_value("\u{1F600}\u{0301})"),
8179            Some("\u{1F600}\u{0301}".to_string())
8180        );
8181        assert_eq!(Xml::extract_url_value("\"\u{130}\")"), Some("\u{130}".to_string()));
8182        assert_eq!(Xml::extract_url_value("\u{1F600}"), None);
8183    }
8184
8185    #[test]
8186    fn extract_url_value_extremely_long_terminates() {
8187        let s = "a".repeat(LONG);
8188        assert_eq!(Xml::extract_url_value(&s), None, "no ')' anywhere => None");
8189        let s2 = format!("{})", "b".repeat(LONG));
8190        assert_eq!(Xml::extract_url_value(&s2).map(|v| v.len()), Some(LONG));
8191    }
8192
8193    #[test]
8194    fn extract_url_value_nested_brackets_no_stack_overflow() {
8195        // Not recursive, but confirm deeply "nested" input is handled iteratively.
8196        let s = "(".repeat(10_000);
8197        assert_eq!(Xml::extract_url_value(&s), None);
8198        let s2 = format!("{}{}", "(".repeat(10_000), ")");
8199        assert_eq!(Xml::extract_url_value(&s2), Some("(".repeat(10_000)));
8200    }
8201
8202    // ================================================================
8203    // Xml::extract_quoted_string  (parser)
8204    // ================================================================
8205
8206    #[test]
8207    fn extract_quoted_string_empty_whitespace_garbage() {
8208        assert_eq!(Xml::extract_quoted_string(""), None);
8209        assert_eq!(Xml::extract_quoted_string("   "), None);
8210        assert_eq!(Xml::extract_quoted_string("\t\n"), None);
8211        assert_eq!(Xml::extract_quoted_string("bare"), None);
8212        // Leading whitespace is NOT trimmed here (unlike extract_url_value).
8213        assert_eq!(Xml::extract_quoted_string("  \"x\""), None);
8214    }
8215
8216    #[test]
8217    fn extract_quoted_string_valid_minimal_and_empty_quotes() {
8218        assert_eq!(Xml::extract_quoted_string("\"x\""), Some("x".to_string()));
8219        assert_eq!(Xml::extract_quoted_string("'x'"), Some("x".to_string()));
8220        // An empty quoted string is Some(""), not None.
8221        assert_eq!(Xml::extract_quoted_string("\"\""), Some(String::new()));
8222        assert_eq!(Xml::extract_quoted_string("''"), Some(String::new()));
8223    }
8224
8225    #[test]
8226    fn extract_quoted_string_unterminated_is_none() {
8227        assert_eq!(Xml::extract_quoted_string("\"abc"), None);
8228        assert_eq!(Xml::extract_quoted_string("'abc"), None);
8229        // Mismatched quotes do not pair up.
8230        assert_eq!(Xml::extract_quoted_string("\"abc'"), None);
8231    }
8232
8233    #[test]
8234    fn extract_quoted_string_boundary_numbers_and_unicode() {
8235        assert_eq!(Xml::extract_quoted_string("\"0\""), Some("0".to_string()));
8236        assert_eq!(Xml::extract_quoted_string("\"-0\""), Some("-0".to_string()));
8237        assert_eq!(Xml::extract_quoted_string("\"NaN\""), Some("NaN".to_string()));
8238        assert_eq!(
8239            Xml::extract_quoted_string("\"\u{1F600}\u{0301}\""),
8240            Some("\u{1F600}\u{0301}".to_string())
8241        );
8242    }
8243
8244    #[test]
8245    fn extract_quoted_string_extremely_long_terminates() {
8246        let unterminated = format!("\"{}", "x".repeat(LONG));
8247        assert_eq!(Xml::extract_quoted_string(&unterminated), None);
8248        let terminated = format!("\"{}\"", "x".repeat(LONG));
8249        assert_eq!(
8250            Xml::extract_quoted_string(&terminated).map(|s| s.len()),
8251            Some(LONG)
8252        );
8253    }
8254
8255    // ================================================================
8256    // Xml::parse_srcset  (parser)
8257    // ================================================================
8258
8259    #[test]
8260    fn parse_srcset_empty_and_whitespace_yield_no_urls() {
8261        assert!(Xml::parse_srcset("").is_empty());
8262        assert!(Xml::parse_srcset("   ").is_empty());
8263        assert!(Xml::parse_srcset("\t\n").is_empty());
8264        assert!(Xml::parse_srcset(",,,").is_empty(), "all-empty entries dropped");
8265    }
8266
8267    #[test]
8268    fn parse_srcset_valid_minimal() {
8269        assert_eq!(
8270            Xml::parse_srcset("a.png 1x, b.png 2x"),
8271            vec!["a.png".to_string(), "b.png".to_string()]
8272        );
8273        // No descriptor at all is still a valid single entry.
8274        assert_eq!(Xml::parse_srcset("a.png"), vec!["a.png".to_string()]);
8275    }
8276
8277    #[test]
8278    fn parse_srcset_garbage_and_boundary_numbers() {
8279        assert_eq!(Xml::parse_srcset("0, -0, NaN"), vec!["0", "-0", "NaN"]);
8280        // Garbage bytes still round out to "first whitespace-delimited token".
8281        assert_eq!(Xml::parse_srcset("\u{0}\u{7f} 1x"), vec!["\u{0}\u{7f}".to_string()]);
8282    }
8283
8284    #[test]
8285    fn parse_srcset_unicode_no_panic() {
8286        assert_eq!(
8287            Xml::parse_srcset("\u{1F600}.png 1x, \u{130}.png 2x"),
8288            vec!["\u{1F600}.png".to_string(), "\u{130}.png".to_string()]
8289        );
8290    }
8291
8292    #[test]
8293    fn parse_srcset_extremely_long_terminates() {
8294        let s = "a.png 1x,".repeat(20_000);
8295        assert_eq!(Xml::parse_srcset(&s).len(), 20_000);
8296        let one_huge = "a".repeat(LONG);
8297        assert_eq!(Xml::parse_srcset(&one_huge).len(), 1);
8298    }
8299
8300    // ================================================================
8301    // Xml::looks_like_resource / guess_kind_from_url / guess_mime_from_url
8302    // ================================================================
8303
8304    #[test]
8305    fn looks_like_resource_edges() {
8306        assert!(!Xml::looks_like_resource(""));
8307        assert!(!Xml::looks_like_resource("   "));
8308        assert!(!Xml::looks_like_resource("/about"));
8309        assert!(Xml::looks_like_resource("/a.PNG"), "case-insensitive");
8310        assert!(Xml::looks_like_resource("x.pdf"));
8311        // A query string defeats the extension check (documented consequence of
8312        // matching on `ends_with`).
8313        assert!(!Xml::looks_like_resource("x.png?v=1"));
8314        assert!(!Xml::looks_like_resource(&"a".repeat(LONG)));
8315    }
8316
8317    #[test]
8318    fn guess_kind_from_url_covers_every_bucket() {
8319        use ExternalResourceKind::*;
8320        assert_eq!(Xml::guess_kind_from_url(""), Unknown);
8321        assert_eq!(Xml::guess_kind_from_url("a.PNG"), Image);
8322        assert_eq!(Xml::guess_kind_from_url("a.woff2"), Font);
8323        assert_eq!(Xml::guess_kind_from_url("a.css"), Stylesheet);
8324        assert_eq!(Xml::guess_kind_from_url("a.mjs"), Script);
8325        assert_eq!(Xml::guess_kind_from_url("a.webm"), Video);
8326        assert_eq!(Xml::guess_kind_from_url("a.flac"), Audio);
8327        assert_eq!(Xml::guess_kind_from_url("a.ico"), Icon);
8328        // Query strings ARE stripped here (unlike looks_like_resource).
8329        assert_eq!(Xml::guess_kind_from_url("a.png?v=1"), Image);
8330        assert_eq!(Xml::guess_kind_from_url("\u{1F600}"), Unknown);
8331    }
8332
8333    #[test]
8334    fn guess_mime_from_url_empty_and_garbage() {
8335        assert_eq!(Xml::guess_mime_from_url("", ""), None);
8336        assert_eq!(Xml::guess_mime_from_url("   ", ""), None);
8337        assert_eq!(Xml::guess_mime_from_url("\u{0}\u{7f}", ""), None);
8338        assert_eq!(Xml::guess_mime_from_url("\u{1F600}", ""), None);
8339    }
8340
8341    #[test]
8342    fn guess_mime_from_url_valid_minimal_and_category_fallback() {
8343        let m = Xml::guess_mime_from_url("a.PNG", "").expect("png is a known extension");
8344        assert_eq!(m.inner.as_str(), "image/png");
8345        let m = Xml::guess_mime_from_url("a.png?v=1", "").expect("query string stripped");
8346        assert_eq!(m.inner.as_str(), "image/png");
8347        // Unknown extension + a category hint => the category wildcard.
8348        let m = Xml::guess_mime_from_url("/no-ext", "image").expect("category fallback");
8349        assert_eq!(m.inner.as_str(), "image/*");
8350        // Unknown category => None.
8351        assert_eq!(Xml::guess_mime_from_url("/no-ext", "bogus"), None);
8352    }
8353
8354    #[test]
8355    fn guess_mime_from_url_boundary_numbers_and_long() {
8356        assert_eq!(Xml::guess_mime_from_url("0", ""), None);
8357        assert_eq!(Xml::guess_mime_from_url("-0", ""), None);
8358        assert_eq!(Xml::guess_mime_from_url("NaN", ""), None);
8359        assert_eq!(Xml::guess_mime_from_url("inf", ""), None);
8360        let long = format!("{}.png", "a".repeat(LONG));
8361        assert_eq!(
8362            Xml::guess_mime_from_url(&long, "").map(|m| m.inner.as_str().to_string()),
8363            Some("image/png".to_string())
8364        );
8365    }
8366
8367    // ================================================================
8368    // Xml::extract_css_urls / scan_node / scan_external_resources
8369    // ================================================================
8370
8371    #[test]
8372    fn extract_css_urls_empty_and_garbage_no_panic() {
8373        let mut v = Vec::new();
8374        Xml::extract_css_urls("", &mut v);
8375        Xml::extract_css_urls("   ", &mut v);
8376        Xml::extract_css_urls("\u{0}\u{7f}\u{1F600}", &mut v);
8377        Xml::extract_css_urls("url(", &mut v);
8378        Xml::extract_css_urls("@import", &mut v);
8379        Xml::extract_css_urls("@import url(", &mut v);
8380        assert!(v.is_empty(), "no well-formed url in any of those inputs");
8381    }
8382
8383    #[test]
8384    fn extract_css_urls_valid_minimal() {
8385        let mut v = Vec::new();
8386        Xml::extract_css_urls("a { background: url('x.png'); }", &mut v);
8387        assert_eq!(v.len(), 1);
8388        assert_eq!(v[0].url.as_str(), "x.png");
8389        assert_eq!(v[0].kind, ExternalResourceKind::Image);
8390        assert_eq!(v[0].source_attribute.as_str(), "url()");
8391    }
8392
8393    #[test]
8394    fn extract_css_urls_import_is_tagged_as_stylesheet() {
8395        let mut v = Vec::new();
8396        Xml::extract_css_urls("@import url(theme.css);", &mut v);
8397        assert_eq!(v.len(), 1);
8398        assert_eq!(v[0].url.as_str(), "theme.css");
8399        assert_eq!(v[0].kind, ExternalResourceKind::Stylesheet);
8400        assert_eq!(v[0].source_attribute.as_str(), "@import");
8401    }
8402
8403    #[test]
8404    fn extract_css_urls_multibyte_before_url_no_panic() {
8405        // ASCII-only lowercasing keeps byte offsets 1:1 with the original.
8406        let mut v = Vec::new();
8407        Xml::extract_css_urls("\u{130}\u{1F600} URL(\"a.css\") \u{0301}", &mut v);
8408        assert_eq!(v.len(), 1);
8409        assert_eq!(v[0].url.as_str(), "a.css");
8410    }
8411
8412    #[test]
8413    fn extract_css_urls_extremely_long_terminates() {
8414        // Each iteration advances search_from past the "url(" it just matched, so
8415        // this must terminate (and not spin).
8416        let mut v = Vec::new();
8417        Xml::extract_css_urls(&"url(".repeat(2_000), &mut v);
8418        // No ')' anywhere => nothing extractable, but the scan still terminates.
8419        assert!(v.is_empty());
8420
8421        let mut v2 = Vec::new();
8422        Xml::extract_css_urls(&"url(a.png)".repeat(2_000), &mut v2);
8423        assert_eq!(v2.len(), 2_000);
8424    }
8425
8426    #[test]
8427    fn scan_node_on_empty_and_extreme_nodes_no_panic() {
8428        let mut v = Vec::new();
8429        Xml::scan_node(&XmlNode::default(), &mut v);
8430        Xml::scan_node(&node("", &[], vec![]), &mut v);
8431        Xml::scan_node(&node(&"a".repeat(10_000), &[("style", "url(x.png)")], vec![]), &mut v);
8432        assert_eq!(v.len(), 1, "only the inline style url()");
8433        assert_eq!(v[0].url.as_str(), "x.png");
8434    }
8435
8436    #[test]
8437    fn scan_node_img_srcset_and_background() {
8438        let mut v = Vec::new();
8439        Xml::scan_node(
8440            &node(
8441                "IMG",
8442                &[("src", "a.png"), ("srcset", "b.png 1x, c.png 2x"), ("background", "d.gif")],
8443                vec![],
8444            ),
8445            &mut v,
8446        );
8447        let urls: Vec<&str> = v.iter().map(|r| r.url.as_str()).collect();
8448        assert_eq!(urls, vec!["a.png", "b.png", "c.png", "d.gif"]);
8449        assert!(v.iter().all(|r| r.kind == ExternalResourceKind::Image));
8450    }
8451
8452    #[test]
8453    fn scan_external_resources_on_empty_document() {
8454        let xml = Xml {
8455            root: Vec::new().into(),
8456        };
8457        assert_eq!(xml.scan_external_resources().as_ref().len(), 0);
8458    }
8459
8460    #[test]
8461    fn scan_external_resources_finds_every_element_kind() {
8462        let xml = Xml {
8463            root: vec![
8464                elem(node("img", &[("src", "i.png")], vec![])),
8465                elem(node("link", &[("href", "s.css"), ("rel", "stylesheet")], vec![])),
8466                elem(node("script", &[("src", "s.js")], vec![])),
8467                elem(node("video", &[("src", "v.mp4"), ("poster", "p.jpg")], vec![])),
8468                elem(node("audio", &[("src", "a.mp3")], vec![])),
8469                elem(node("a", &[("href", "f.pdf")], vec![])),
8470                elem(node("a", &[("href", "/page")], vec![])),
8471            ]
8472            .into(),
8473        };
8474        let res = xml.scan_external_resources();
8475        let mut urls: Vec<&str> = res.as_ref().iter().map(|r| r.url.as_str()).collect();
8476        urls.sort_unstable();
8477        assert_eq!(
8478            urls,
8479            vec!["a.mp3", "f.pdf", "i.png", "p.jpg", "s.css", "s.js", "v.mp4"],
8480            "`/page` is not a resource and must be skipped"
8481        );
8482    }
8483
8484    // ================================================================
8485    // MimeTypeHint  (constructor)
8486    // ================================================================
8487
8488    #[test]
8489    fn mime_type_hint_new_no_panic_and_fields_match_args() {
8490        for s in ["", "   ", "text/css", "\u{1F600}", "\u{0}"] {
8491            assert_eq!(MimeTypeHint::new(s).inner.as_str(), s, "new() stores verbatim");
8492        }
8493        let long = "x".repeat(LONG);
8494        assert_eq!(MimeTypeHint::new(&long).inner.as_str().len(), LONG);
8495    }
8496
8497    #[test]
8498    fn mime_type_hint_from_extension_edges() {
8499        assert_eq!(
8500            MimeTypeHint::from_extension("").inner.as_str(),
8501            "application/octet-stream"
8502        );
8503        assert_eq!(
8504            MimeTypeHint::from_extension("PnG").inner.as_str(),
8505            "image/png",
8506            "extension match is case-insensitive"
8507        );
8508        assert_eq!(MimeTypeHint::from_extension("jpeg").inner.as_str(), "image/jpeg");
8509        assert_eq!(MimeTypeHint::from_extension("woff2").inner.as_str(), "font/woff2");
8510        assert_eq!(
8511            MimeTypeHint::from_extension("\u{1F600}").inner.as_str(),
8512            "application/octet-stream"
8513        );
8514        assert_eq!(
8515            MimeTypeHint::from_extension(&"z".repeat(LONG)).inner.as_str(),
8516            "application/octet-stream"
8517        );
8518    }
8519
8520    // ================================================================
8521    // ComponentId  (constructor / getter)
8522    // ================================================================
8523
8524    #[test]
8525    fn component_id_builtin_and_new_invariants() {
8526        let b = ComponentId::builtin("div");
8527        assert_eq!(b.collection.as_str(), "builtin");
8528        assert_eq!(b.name.as_str(), "div");
8529
8530        let c = ComponentId::new("", "");
8531        assert_eq!(c.collection.as_str(), "");
8532        assert_eq!(c.name.as_str(), "");
8533
8534        let u = ComponentId::new("\u{1F600}", "\u{130}");
8535        assert_eq!(u.collection.as_str(), "\u{1F600}");
8536        assert_eq!(u.name.as_str(), "\u{130}");
8537    }
8538
8539    #[test]
8540    fn component_id_qualified_name_roundtrips_through_the_map_lookup() {
8541        assert_eq!(ComponentId::builtin("div").qualified_name(), "builtin:div");
8542        assert_eq!(ComponentId::new("", "").qualified_name(), ":");
8543        // A name that itself contains ':' makes the qualified name ambiguous —
8544        // pin the (lossy) behavior so a change is noticed.
8545        assert_eq!(ComponentId::new("a", "b:c").qualified_name(), "a:b:c");
8546    }
8547
8548    // ================================================================
8549    // ComponentFieldTypeBox / ComponentFieldValueBox  (constructor / getter)
8550    // ================================================================
8551
8552    #[test]
8553    fn component_field_type_box_new_as_ref_and_clone() {
8554        let b = ComponentFieldTypeBox::new(ComponentFieldType::Bool);
8555        assert!(!b.ptr.is_null());
8556        assert_eq!(*b.as_ref(), ComponentFieldType::Bool);
8557
8558        let c = b.clone();
8559        assert_eq!(*c.as_ref(), ComponentFieldType::Bool);
8560        assert_ne!(b.ptr, c.ptr, "clone must deep-copy, not alias");
8561        assert_eq!(b, c, "PartialEq compares pointees");
8562        drop(c);
8563        assert_eq!(*b.as_ref(), ComponentFieldType::Bool, "original survives");
8564    }
8565
8566    #[test]
8567    fn component_field_type_box_nested_deeply_drops_cleanly() {
8568        let mut t = ComponentFieldType::Bool;
8569        for _ in 0..64 {
8570            t = ComponentFieldType::OptionType(ComponentFieldTypeBox::new(t));
8571        }
8572        assert_eq!(t.format(), format!("{}Bool{}", "Option<".repeat(64), ">".repeat(64)));
8573        drop(t);
8574    }
8575
8576    #[test]
8577    fn component_field_value_box_new_as_ref_and_clone() {
8578        let v = ComponentFieldValueBox::new(ComponentFieldValue::I32(i32::MIN));
8579        assert!(!v.ptr.is_null());
8580        assert_eq!(*v.as_ref(), ComponentFieldValue::I32(i32::MIN));
8581        let c = v.clone();
8582        assert_ne!(v.ptr, c.ptr);
8583        assert_eq!(v, c);
8584    }
8585
8586    // ================================================================
8587    // ComponentFieldType::parse / parse_depth / format  (round-trip)
8588    // ================================================================
8589
8590    #[test]
8591    fn component_field_type_parse_empty_whitespace_garbage() {
8592        assert_eq!(ComponentFieldType::parse(""), None);
8593        assert_eq!(ComponentFieldType::parse("   "), None);
8594        assert_eq!(ComponentFieldType::parse("\t\n"), None);
8595        assert_eq!(ComponentFieldType::parse("lowercase"), None);
8596        assert_eq!(ComponentFieldType::parse("\u{0}\u{7f}"), None);
8597        assert_eq!(ComponentFieldType::parse("Option<>"), None, "empty inner rejected");
8598        assert_eq!(ComponentFieldType::parse("Vec<>"), None);
8599        assert_eq!(ComponentFieldType::parse("Option<lowercase>"), None);
8600    }
8601
8602    #[test]
8603    fn component_field_type_parse_valid_minimal_and_trimming() {
8604        assert_eq!(ComponentFieldType::parse("String"), Some(ComponentFieldType::String));
8605        assert_eq!(
8606            ComponentFieldType::parse("  String  "),
8607            Some(ComponentFieldType::String),
8608            "leading/trailing whitespace is trimmed"
8609        );
8610        assert_eq!(ComponentFieldType::parse("bool"), Some(ComponentFieldType::Bool));
8611        assert_eq!(ComponentFieldType::parse("usize"), Some(ComponentFieldType::Usize));
8612        assert_eq!(
8613            ComponentFieldType::parse("StructRef(Foo)"),
8614            Some(ComponentFieldType::StructRef(AzString::from("Foo")))
8615        );
8616        assert_eq!(
8617            ComponentFieldType::parse("EnumRef(Foo)"),
8618            Some(ComponentFieldType::EnumRef(AzString::from("Foo")))
8619        );
8620        assert_eq!(
8621            ComponentFieldType::parse("RefAny"),
8622            Some(ComponentFieldType::RefAny(AzString::from("")))
8623        );
8624    }
8625
8626    #[test]
8627    fn component_field_type_parse_leading_trailing_junk_is_rejected_or_absorbed() {
8628        // Trailing junk after a known keyword falls through to the
8629        // "starts uppercase => StructRef" catch-all rather than being rejected.
8630        assert_eq!(
8631            ComponentFieldType::parse("String;garbage"),
8632            Some(ComponentFieldType::StructRef(AzString::from("String;garbage")))
8633        );
8634        // Lowercase junk has no uppercase first char => rejected.
8635        assert_eq!(ComponentFieldType::parse("string;garbage"), None);
8636    }
8637
8638    #[test]
8639    fn component_field_type_parse_boundary_numbers() {
8640        for s in ["0", "-0", "9223372036854775807", "-9223372036854775808", "1e400", "inf"] {
8641            assert_eq!(
8642                ComponentFieldType::parse(s),
8643                None,
8644                "numeric literal {s:?} is not a type name"
8645            );
8646        }
8647        // ...but anything starting with an uppercase letter hits the StructRef
8648        // catch-all, so "NaN" parses as a struct reference rather than failing.
8649        assert_eq!(
8650            ComponentFieldType::parse("NaN"),
8651            Some(ComponentFieldType::StructRef(AzString::from("NaN")))
8652        );
8653    }
8654
8655    #[test]
8656    fn component_field_type_parse_unicode_no_panic() {
8657        assert_eq!(ComponentFieldType::parse("\u{1F600}"), None, "emoji is not uppercase");
8658        // A real uppercase non-ASCII letter hits the StructRef catch-all.
8659        assert_eq!(
8660            ComponentFieldType::parse("\u{0391}bc"),
8661            Some(ComponentFieldType::StructRef(AzString::from("\u{0391}bc")))
8662        );
8663    }
8664
8665    #[test]
8666    fn component_field_type_parse_depth_boundary_is_exact() {
8667        // MAX_TYPE_PARSE_DEPTH wrappers parse; one more is rejected.
8668        let ok = format!(
8669            "{}Bool{}",
8670            "Option<".repeat(MAX_TYPE_PARSE_DEPTH),
8671            ">".repeat(MAX_TYPE_PARSE_DEPTH)
8672        );
8673        assert!(
8674            ComponentFieldType::parse(&ok).is_some(),
8675            "exactly MAX_TYPE_PARSE_DEPTH wrappers must still parse"
8676        );
8677
8678        let too_deep = format!(
8679            "{}Bool{}",
8680            "Option<".repeat(MAX_TYPE_PARSE_DEPTH + 1),
8681            ">".repeat(MAX_TYPE_PARSE_DEPTH + 1)
8682        );
8683        assert_eq!(
8684            ComponentFieldType::parse(&too_deep),
8685            None,
8686            "one wrapper past the cap must be rejected, not overflow"
8687        );
8688    }
8689
8690    #[test]
8691    fn component_field_type_parse_depth_direct_call_honors_start_depth() {
8692        assert_eq!(
8693            ComponentFieldType::parse_depth("Bool", MAX_TYPE_PARSE_DEPTH),
8694            Some(ComponentFieldType::Bool),
8695            "depth == cap is still allowed"
8696        );
8697        assert_eq!(
8698            ComponentFieldType::parse_depth("Bool", MAX_TYPE_PARSE_DEPTH + 1),
8699            None
8700        );
8701        assert_eq!(
8702            ComponentFieldType::parse_depth("Bool", usize::MAX),
8703            None,
8704            "usize::MAX start depth must not overflow, just refuse"
8705        );
8706    }
8707
8708    #[test]
8709    fn component_field_type_parse_nested_recursion_does_not_stack_overflow() {
8710        let bomb = format!("{}Bool{}", "Vec<".repeat(50_000), ">".repeat(50_000));
8711        assert_eq!(ComponentFieldType::parse(&bomb), None);
8712    }
8713
8714    #[test]
8715    fn component_field_type_parse_extremely_long_terminates() {
8716        // A single 200k-char uppercase token becomes a StructRef of that name.
8717        let long = format!("A{}", "b".repeat(LONG));
8718        assert_eq!(
8719            ComponentFieldType::parse(&long),
8720            Some(ComponentFieldType::StructRef(AzString::from(long.as_str())))
8721        );
8722    }
8723
8724    #[test]
8725    fn component_field_type_round_trip_representative() {
8726        let representative = vec![
8727            ComponentFieldType::String,
8728            ComponentFieldType::Bool,
8729            ComponentFieldType::I32,
8730            ComponentFieldType::I64,
8731            ComponentFieldType::U32,
8732            ComponentFieldType::U64,
8733            ComponentFieldType::Usize,
8734            ComponentFieldType::F32,
8735            ComponentFieldType::F64,
8736            ComponentFieldType::ColorU,
8737            ComponentFieldType::CssProperty,
8738            ComponentFieldType::ImageRef,
8739            ComponentFieldType::FontRef,
8740            ComponentFieldType::StyledDom,
8741            ComponentFieldType::StructRef(AzString::from("Foo")),
8742            ComponentFieldType::OptionType(ComponentFieldTypeBox::new(ComponentFieldType::Bool)),
8743            ComponentFieldType::VecType(ComponentFieldTypeBox::new(ComponentFieldType::I32)),
8744            ComponentFieldType::RefAny(AzString::from("")),
8745            ComponentFieldType::RefAny(AzString::from("Hint")),
8746            ComponentFieldType::Callback(ComponentCallbackSignature {
8747                return_type: AzString::from("Update"),
8748                args: Vec::new().into(),
8749            }),
8750        ];
8751        for x in representative {
8752            let s = x.format();
8753            assert_eq!(
8754                ComponentFieldType::parse(&s),
8755                Some(x.clone()),
8756                "parse(format({x:?})) must round-trip"
8757            );
8758        }
8759    }
8760
8761    #[test]
8762    fn component_field_type_round_trip_edge_values() {
8763        // Empty / unicode-bearing payloads.
8764        for x in [
8765            ComponentFieldType::StructRef(AzString::from("\u{0391}\u{1F600}")),
8766            ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
8767                ComponentFieldType::VecType(ComponentFieldTypeBox::new(
8768                    ComponentFieldType::StructRef(AzString::from("Foo")),
8769                )),
8770            )),
8771        ] {
8772            assert_eq!(ComponentFieldType::parse(&x.format()), Some(x.clone()));
8773        }
8774    }
8775
8776    #[test]
8777    fn component_field_type_format_is_an_idempotent_normalization() {
8778        // `EnumRef` and `StructRef` share the same canonical spelling, so parse()
8779        // collapses EnumRef -> StructRef. The normalization is still STABLE:
8780        // format(parse(format(x))) == format(x).
8781        let e = ComponentFieldType::EnumRef(AzString::from("Role"));
8782        let once = e.format();
8783        assert_eq!(once, "Role");
8784        let reparsed = ComponentFieldType::parse(&once).expect("parses");
8785        assert_eq!(
8786            reparsed,
8787            ComponentFieldType::StructRef(AzString::from("Role")),
8788            "EnumRef is lossy through format() — it comes back as StructRef"
8789        );
8790        assert_eq!(reparsed.format(), once, "but the normalization is idempotent");
8791    }
8792
8793    #[test]
8794    fn component_field_type_display_matches_format() {
8795        let t = ComponentFieldType::OptionType(ComponentFieldTypeBox::new(ComponentFieldType::F64));
8796        assert_eq!(format!("{t}"), t.format());
8797        assert_eq!(format!("{t}"), "Option<F64>");
8798    }
8799
8800    #[test]
8801    fn component_field_type_format_no_panic_on_empty_payloads() {
8802        let t = ComponentFieldType::Callback(ComponentCallbackSignature {
8803            return_type: AzString::from(""),
8804            args: Vec::new().into(),
8805        });
8806        assert_eq!(t.format(), "Callback()");
8807        // Callback() with an empty signature round-trips.
8808        assert_eq!(ComponentFieldType::parse("Callback()"), Some(t));
8809    }
8810
8811    // ================================================================
8812    // ComponentFieldNamedValueVec::get_field / get_string  (parser-ish lookup)
8813    // ================================================================
8814
8815    fn named(name: &str, v: ComponentFieldValue) -> ComponentFieldNamedValue {
8816        ComponentFieldNamedValue {
8817            name: AzString::from(name),
8818            value: v,
8819        }
8820    }
8821
8822    fn named_vec() -> ComponentFieldNamedValueVec {
8823        vec![
8824            named("a", ComponentFieldValue::String(AzString::from("x"))),
8825            named("b", ComponentFieldValue::Bool(true)),
8826            named("", ComponentFieldValue::U64(u64::MAX)),
8827            named("\u{1F600}", ComponentFieldValue::String(AzString::from("emoji"))),
8828        ]
8829        .into()
8830    }
8831
8832    #[test]
8833    fn named_value_vec_get_field_valid_minimal() {
8834        let v = named_vec();
8835        assert_eq!(
8836            v.get_field("a"),
8837            Some(&ComponentFieldValue::String(AzString::from("x")))
8838        );
8839        assert_eq!(v.get_field("b"), Some(&ComponentFieldValue::Bool(true)));
8840    }
8841
8842    #[test]
8843    fn named_value_vec_get_field_empty_whitespace_garbage_unicode() {
8844        let v = named_vec();
8845        // An empty NAME is a legal key here — it matches the field literally named "".
8846        assert_eq!(v.get_field(""), Some(&ComponentFieldValue::U64(u64::MAX)));
8847        assert_eq!(v.get_field("   "), None);
8848        assert_eq!(v.get_field("\t\n"), None);
8849        assert_eq!(v.get_field("\u{0}\u{7f}"), None);
8850        assert!(v.get_field("\u{1F600}").is_some());
8851        assert_eq!(v.get_field(" a "), None, "no trimming: lookup is exact");
8852        assert_eq!(v.get_field("a;garbage"), None);
8853    }
8854
8855    #[test]
8856    fn named_value_vec_get_field_on_empty_vec_and_long_key() {
8857        let empty = ComponentFieldNamedValueVec::from_const_slice(&[]);
8858        assert_eq!(empty.get_field("a"), None);
8859        assert_eq!(empty.get_string("a"), None);
8860        assert_eq!(named_vec().get_field(&"z".repeat(LONG)), None);
8861    }
8862
8863    #[test]
8864    fn named_value_vec_get_string_only_matches_string_variant() {
8865        let v = named_vec();
8866        assert_eq!(v.get_string("a").map(AzString::as_str), Some("x"));
8867        assert_eq!(v.get_string("b"), None, "Bool is not a String");
8868        assert_eq!(v.get_string(""), None, "U64 is not a String");
8869        assert_eq!(v.get_string("missing"), None);
8870    }
8871
8872    #[test]
8873    fn named_value_vec_boundary_numeric_keys() {
8874        let v: ComponentFieldNamedValueVec = vec![
8875            named("0", ComponentFieldValue::I32(0)),
8876            named("-0", ComponentFieldValue::I32(i32::MIN)),
8877            named("9223372036854775807", ComponentFieldValue::I64(i64::MAX)),
8878            named("NaN", ComponentFieldValue::F32(f32::NAN)),
8879        ]
8880        .into();
8881        assert_eq!(v.get_field("0"), Some(&ComponentFieldValue::I32(0)));
8882        assert_eq!(v.get_field("-0"), Some(&ComponentFieldValue::I32(i32::MIN)));
8883        assert_eq!(
8884            v.get_field("9223372036854775807"),
8885            Some(&ComponentFieldValue::I64(i64::MAX))
8886        );
8887        // NaN != NaN, so only check the variant, not equality.
8888        assert!(matches!(v.get_field("NaN"), Some(ComponentFieldValue::F32(f)) if f.is_nan()));
8889    }
8890
8891    // ================================================================
8892    // ComponentDataModel::get_field / get_default_string / with_default
8893    // ================================================================
8894
8895    fn model_with_text() -> ComponentDataModel {
8896        dm(
8897            "M",
8898            vec![
8899                data_field(
8900                    "text",
8901                    ComponentFieldType::String,
8902                    Some(ComponentDefaultValue::String(AzString::from("hi"))),
8903                    "",
8904                ),
8905                data_field("count", ComponentFieldType::U32, Some(ComponentDefaultValue::U32(3)), ""),
8906                data_field("required_one", ComponentFieldType::String, None, ""),
8907            ],
8908        )
8909    }
8910
8911    #[test]
8912    fn data_model_get_field_valid_minimal_and_missing() {
8913        let m = model_with_text();
8914        assert!(m.get_field("text").is_some());
8915        assert!(m.get_field("count").is_some());
8916        assert!(m.get_field("missing").is_none());
8917        assert!(m.get_field("").is_none());
8918        assert!(m.get_field("   ").is_none());
8919        assert!(m.get_field(" text ").is_none(), "exact match, no trimming");
8920        assert!(m.get_field("\u{1F600}").is_none());
8921        assert!(m.get_field(&"z".repeat(LONG)).is_none());
8922    }
8923
8924    #[test]
8925    fn data_model_get_field_on_empty_model() {
8926        let m = dm("Empty", Vec::new());
8927        assert!(m.get_field("anything").is_none());
8928        assert!(m.get_default_string("anything").is_none());
8929    }
8930
8931    #[test]
8932    fn data_model_get_default_string_only_for_string_defaults() {
8933        let m = model_with_text();
8934        assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("hi"));
8935        assert_eq!(m.get_default_string("count"), None, "U32 default is not a String");
8936        assert_eq!(m.get_default_string("required_one"), None, "no default at all");
8937        assert_eq!(m.get_default_string("missing"), None);
8938    }
8939
8940    #[test]
8941    fn data_model_required_flag_follows_default_presence() {
8942        let m = model_with_text();
8943        assert!(!m.get_field("text").unwrap().required);
8944        assert!(
8945            m.get_field("required_one").unwrap().required,
8946            "a field with no default must be marked required"
8947        );
8948    }
8949
8950    #[test]
8951    fn data_model_with_default_overrides_and_preserves_len() {
8952        let m = model_with_text();
8953        let before = m.fields.as_ref().len();
8954        let m = m.with_default("text", ComponentDefaultValue::String(AzString::from("bye")));
8955        assert_eq!(m.fields.as_ref().len(), before, "len is preserved");
8956        assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("bye"));
8957    }
8958
8959    #[test]
8960    fn data_model_with_default_on_missing_field_is_a_no_op() {
8961        let m = model_with_text();
8962        let m = m.with_default("nope", ComponentDefaultValue::Bool(true));
8963        assert_eq!(m.fields.as_ref().len(), 3);
8964        assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("hi"));
8965        assert!(m.get_field("nope").is_none(), "no field is inserted");
8966    }
8967
8968    #[test]
8969    fn data_model_with_default_extreme_names_and_values_no_panic() {
8970        let m = model_with_text()
8971            .with_default("", ComponentDefaultValue::None)
8972            .with_default(&"z".repeat(10_000), ComponentDefaultValue::F64(f64::NAN))
8973            .with_default("count", ComponentDefaultValue::Usize(usize::MAX))
8974            .with_default("text", ComponentDefaultValue::I64(i64::MIN));
8975        assert_eq!(m.fields.as_ref().len(), 3);
8976        assert!(matches!(
8977            m.get_field("count").unwrap().default_value,
8978            OptionComponentDefaultValue::Some(ComponentDefaultValue::Usize(usize::MAX))
8979        ));
8980        assert_eq!(
8981            m.get_default_string("text"),
8982            None,
8983            "text is now an I64 default, no longer a String"
8984        );
8985    }
8986
8987    #[test]
8988    fn data_model_with_default_fills_only_the_first_match() {
8989        // Duplicate field names: `with_default` breaks after the first hit.
8990        let m = dm(
8991            "Dup",
8992            vec![
8993                data_field("x", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from("1"))), ""),
8994                data_field("x", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from("2"))), ""),
8995            ],
8996        )
8997        .with_default("x", ComponentDefaultValue::String(AzString::from("3")));
8998        let vals: Vec<&str> = m
8999            .fields
9000            .as_ref()
9001            .iter()
9002            .filter_map(|f| match &f.default_value {
9003                OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s.as_str()),
9004                _ => None,
9005            })
9006            .collect();
9007        assert_eq!(vals, vec!["3", "2"], "only the first duplicate is overridden");
9008    }
9009
9010    // ================================================================
9011    // ComponentSource / ComponentMap  (constructor / getter / lookup)
9012    // ================================================================
9013
9014    #[test]
9015    fn component_source_create_is_user_defined() {
9016        assert_eq!(ComponentSource::create(), ComponentSource::UserDefined);
9017        assert_eq!(ComponentSource::default(), ComponentSource::UserDefined);
9018    }
9019
9020    #[test]
9021    fn component_map_create_is_empty_and_all_lookups_return_none() {
9022        let m = ComponentMap::create();
9023        assert_eq!(m.libraries.as_ref().len(), 0);
9024        assert!(m.get("builtin", "div").is_none());
9025        assert!(m.get_unqualified("div").is_none());
9026        assert!(m.get_by_qualified_name("builtin:div").is_none());
9027        assert!(m.all_components().is_empty());
9028        assert!(m.get_exportable_libraries().is_empty());
9029    }
9030
9031    #[test]
9032    fn component_map_with_builtin_invariants() {
9033        let m = ComponentMap::with_builtin();
9034        assert_eq!(m.libraries.as_ref().len(), 1);
9035        let lib = &m.libraries.as_ref()[0];
9036        assert_eq!(lib.name.as_str(), "builtin");
9037        assert!(!lib.exportable, "builtins must never be exportable");
9038        assert!(!lib.modifiable, "builtins must never be modifiable");
9039        assert_eq!(
9040            m.all_components().len(),
9041            lib.components.as_ref().len(),
9042            "all_components must see every registered component"
9043        );
9044        assert!(
9045            m.get_exportable_libraries().is_empty(),
9046            "the builtin library is not exportable"
9047        );
9048    }
9049
9050    #[test]
9051    fn component_map_get_valid_minimal() {
9052        let m = ComponentMap::with_builtin();
9053        assert!(m.get("builtin", "div").is_some());
9054        assert!(m.get("builtin", "if").is_some());
9055        assert!(m.get("builtin", "for").is_some());
9056        assert!(m.get("builtin", "map").is_some());
9057        assert_eq!(
9058            m.get("builtin", "div").unwrap().id.qualified_name(),
9059            "builtin:div"
9060        );
9061    }
9062
9063    #[test]
9064    fn component_map_get_empty_whitespace_garbage_unicode() {
9065        let m = ComponentMap::with_builtin();
9066        assert!(m.get("", "").is_none());
9067        assert!(m.get("builtin", "").is_none());
9068        assert!(m.get("", "div").is_none());
9069        assert!(m.get("builtin", "   ").is_none());
9070        assert!(m.get("builtin", " div ").is_none(), "no trimming");
9071        assert!(m.get("builtin", "DIV").is_none(), "lookup is case-sensitive");
9072        assert!(m.get("builtin", "\u{1F600}").is_none());
9073        assert!(m.get("builtin", "\u{0}\u{7f}").is_none());
9074        assert!(m.get("builtin", "div;garbage").is_none());
9075    }
9076
9077    #[test]
9078    fn component_map_get_extremely_long_name_terminates() {
9079        let m = ComponentMap::with_builtin();
9080        assert!(m.get(&"a".repeat(LONG), &"b".repeat(LONG)).is_none());
9081        assert!(m.get_unqualified(&"b".repeat(LONG)).is_none());
9082        assert!(m.get_by_qualified_name(&"c".repeat(LONG)).is_none());
9083    }
9084
9085    #[test]
9086    fn component_map_get_unqualified_only_searches_builtin() {
9087        let lib = ComponentLibrary {
9088            name: AzString::from("mylib"),
9089            version: AzString::from("1.0.0"),
9090            description: AzString::from(""),
9091            components: vec![user_def("", Vec::new())].into(),
9092            exportable: true,
9093            modifiable: true,
9094            data_models: Vec::new().into(),
9095            enum_models: Vec::new().into(),
9096        };
9097        let libs: ComponentLibraryVec = vec![lib].into();
9098        let m = ComponentMap::from_libraries(&libs);
9099
9100        assert!(m.get("mylib", "widget").is_some());
9101        assert!(
9102            m.get_unqualified("widget").is_none(),
9103            "unqualified lookup must NOT reach non-builtin libraries"
9104        );
9105        assert!(m.get_by_qualified_name("mylib:widget").is_some());
9106        assert_eq!(m.get_exportable_libraries().len(), 1);
9107        assert_eq!(m.all_components().len(), 1);
9108    }
9109
9110    #[test]
9111    fn component_map_get_by_qualified_name_boundary_forms() {
9112        let m = ComponentMap::with_builtin();
9113        // No colon => falls back to the builtin library.
9114        assert!(m.get_by_qualified_name("div").is_some());
9115        // Exactly one colon.
9116        assert!(m.get_by_qualified_name("builtin:div").is_some());
9117        // Splits on the FIRST colon, so the remainder (incl. colons) is the name.
9118        assert!(m.get_by_qualified_name("builtin:div:extra").is_none());
9119        assert!(m.get_by_qualified_name(":").is_none());
9120        assert!(m.get_by_qualified_name(":div").is_none());
9121        assert!(m.get_by_qualified_name("builtin:").is_none());
9122        assert!(m.get_by_qualified_name("").is_none());
9123        assert!(m.get_by_qualified_name("   ").is_none());
9124    }
9125
9126    #[test]
9127    fn component_map_from_libraries_clones_without_losing_entries() {
9128        let src = ComponentMap::with_builtin();
9129        let copy = ComponentMap::from_libraries(&src.libraries);
9130        assert_eq!(copy.all_components().len(), src.all_components().len());
9131        assert!(copy.get_unqualified("div").is_some());
9132    }
9133
9134    #[test]
9135    fn register_builtin_components_is_stable_across_calls() {
9136        let a = register_builtin_components();
9137        let b = register_builtin_components();
9138        assert_eq!(a.components.as_ref().len(), b.components.as_ref().len());
9139        assert!(a.components.as_ref().len() > 50);
9140        assert_eq!(a.name.as_str(), "builtin");
9141        assert_eq!(a.version.as_str(), "1.0.0");
9142        // Every component must be namespaced into "builtin".
9143        assert!(a
9144            .components
9145            .as_ref()
9146            .iter()
9147            .all(|c| c.id.collection.as_str() == "builtin"));
9148        assert!(a
9149            .components
9150            .as_ref()
9151            .iter()
9152            .all(|c| c.source == ComponentSource::Builtin));
9153    }
9154
9155    // ================================================================
9156    // XmlNodeChild / XmlNode  (getter / predicate / constructor)
9157    // ================================================================
9158
9159    #[test]
9160    fn xml_node_child_as_text_and_as_element_are_mutually_exclusive() {
9161        let t = txt("hello");
9162        assert_eq!(t.as_text(), Some("hello"));
9163        assert!(t.as_element().is_none());
9164
9165        let e = elem(node("div", &[], vec![]));
9166        assert!(e.as_text().is_none());
9167        assert_eq!(e.as_element().map(|n| n.node_type.as_str()), Some("div"));
9168    }
9169
9170    #[test]
9171    fn xml_node_child_as_text_edge_values() {
9172        assert_eq!(txt("").as_text(), Some(""), "an empty text node is still text");
9173        assert_eq!(txt("   ").as_text(), Some("   "), "no trimming in the getter");
9174        assert_eq!(txt("\u{1F600}\u{0301}").as_text(), Some("\u{1F600}\u{0301}"));
9175        assert_eq!(txt("\u{0}").as_text(), Some("\u{0}"));
9176    }
9177
9178    #[test]
9179    fn xml_node_child_as_element_mut_allows_mutation_and_rejects_text() {
9180        let mut e = elem(node("div", &[], vec![]));
9181        e.as_element_mut().expect("is an element").node_type = "span".into();
9182        assert_eq!(e.as_element().map(|n| n.node_type.as_str()), Some("span"));
9183
9184        let mut t = txt("x");
9185        assert!(t.as_element_mut().is_none(), "text nodes have no element");
9186    }
9187
9188    #[test]
9189    fn xml_node_create_and_with_children_invariants() {
9190        let n = XmlNode::create("div");
9191        assert_eq!(n.node_type.as_str(), "div");
9192        assert_eq!(n.children.as_ref().len(), 0);
9193        assert_eq!(n.attributes.as_ref().len(), 0);
9194
9195        let n = n.with_children(vec![txt("a"), elem(XmlNode::create("b"))]);
9196        assert_eq!(n.children.as_ref().len(), 2);
9197        assert_eq!(n.node_type.as_str(), "div", "tag survives with_children");
9198
9199        // with_children REPLACES, it does not append.
9200        let n = n.with_children(Vec::new());
9201        assert_eq!(n.children.as_ref().len(), 0);
9202    }
9203
9204    #[test]
9205    fn xml_node_create_extreme_tag_names_no_panic() {
9206        assert_eq!(XmlNode::create("").node_type.as_str(), "");
9207        assert_eq!(XmlNode::create("\u{1F600}").node_type.as_str(), "\u{1F600}");
9208        assert_eq!(
9209            XmlNode::create(&*"a".repeat(10_000)).node_type.as_str().len(),
9210            10_000
9211        );
9212    }
9213
9214    #[test]
9215    fn xml_node_get_text_content_concatenates_only_direct_text() {
9216        let n = node(
9217            "p",
9218            &[],
9219            vec![
9220                txt("a "),
9221                elem(node("span", &[], vec![txt("IGNORED")])),
9222                txt("b"),
9223            ],
9224        );
9225        assert_eq!(
9226            n.get_text_content(),
9227            "a b",
9228            "only DIRECT text children, nested element text is not included"
9229        );
9230        assert_eq!(XmlNode::default().get_text_content(), "");
9231        assert_eq!(node("p", &[], vec![txt(""), txt("")]).get_text_content(), "");
9232    }
9233
9234    #[test]
9235    fn xml_node_has_only_text_children_true_false_and_empty() {
9236        assert!(
9237            XmlNode::default().has_only_text_children(),
9238            "vacuously true for a childless node — callers must pair this with a text check"
9239        );
9240        assert!(node("p", &[], vec![txt("a"), txt("b")]).has_only_text_children());
9241        assert!(!node("p", &[], vec![txt("a"), elem(XmlNode::create("b"))]).has_only_text_children());
9242        assert!(!node("p", &[], vec![elem(XmlNode::create("b"))]).has_only_text_children());
9243    }
9244
9245    // ================================================================
9246    // get_html_node / get_body_node / find_node_by_type / find_attribute
9247    // ================================================================
9248
9249    #[test]
9250    fn get_html_node_empty_input_is_no_html_node() {
9251        assert_eq!(get_html_node(&[]), Err(DomXmlParseError::NoHtmlNode));
9252        assert_eq!(get_html_node(&[txt("just text")]), Err(DomXmlParseError::NoHtmlNode));
9253        assert_eq!(
9254            get_html_node(&[elem(XmlNode::create("div"))]),
9255            Err(DomXmlParseError::NoHtmlNode)
9256        );
9257    }
9258
9259    #[test]
9260    fn get_html_node_valid_minimal_and_case_insensitive() {
9261        let roots = vec![elem(XmlNode::create("HTML"))];
9262        assert!(get_html_node(&roots).is_ok(), "tag casing is normalized");
9263    }
9264
9265    #[test]
9266    fn get_html_node_rejects_multiple_roots() {
9267        let roots = vec![elem(XmlNode::create("html")), elem(XmlNode::create("html"))];
9268        assert_eq!(
9269            get_html_node(&roots),
9270            Err(DomXmlParseError::MultipleHtmlRootNodes)
9271        );
9272    }
9273
9274    #[test]
9275    fn get_body_node_empty_and_missing() {
9276        assert_eq!(get_body_node(&[]), Err(DomXmlParseError::NoBodyInHtml));
9277        assert_eq!(
9278            get_body_node(&[elem(XmlNode::create("head"))]),
9279            Err(DomXmlParseError::NoBodyInHtml)
9280        );
9281    }
9282
9283    #[test]
9284    fn get_body_node_direct_and_nested() {
9285        let direct = vec![elem(XmlNode::create("body"))];
9286        assert!(get_body_node(&direct).is_ok());
9287
9288        // Malformed markup: <body> buried inside <head>.
9289        let nested = vec![elem(node(
9290            "head",
9291            &[],
9292            vec![elem(node("div", &[], vec![elem(XmlNode::create("BODY"))]))],
9293        ))];
9294        assert!(
9295            get_body_node(&nested).is_ok(),
9296            "the recursive fallback finds a nested body"
9297        );
9298    }
9299
9300    /// Build a `<div>` chain `depth` levels deep with `inner` at the bottom.
9301    fn wrap_divs(depth: usize, inner: XmlNode) -> XmlNode {
9302        let mut n = inner;
9303        for _ in 0..depth {
9304            n = node("div", &[], vec![elem(n)]);
9305        }
9306        n
9307    }
9308
9309    #[test]
9310    fn get_body_node_deep_nesting_is_depth_capped_not_stack_overflowing() {
9311        // Body sits just inside the cap => found.
9312        let ok = vec![elem(wrap_divs(
9313            MAX_XML_NESTING_DEPTH - 2,
9314            XmlNode::create("body"),
9315        ))];
9316        assert!(get_body_node(&ok).is_ok(), "body within the depth cap is found");
9317
9318        // Body far below the cap => reported missing, but MUST NOT overflow.
9319        let too_deep = vec![elem(wrap_divs(2_000, XmlNode::create("body")))];
9320        assert_eq!(
9321            get_body_node(&too_deep),
9322            Err(DomXmlParseError::NoBodyInHtml),
9323            "past MAX_XML_NESTING_DEPTH the search gives up instead of crashing"
9324        );
9325    }
9326
9327    #[test]
9328    fn find_node_by_type_empty_garbage_unicode() {
9329        assert!(find_node_by_type(&[], "div").is_none());
9330        assert!(find_node_by_type(&[txt("x")], "div").is_none());
9331
9332        let roots = vec![elem(XmlNode::create("div"))];
9333        assert!(find_node_by_type(&roots, "").is_none());
9334        assert!(find_node_by_type(&roots, "   ").is_none());
9335        assert!(find_node_by_type(&roots, "\u{1F600}").is_none());
9336        assert!(find_node_by_type(&roots, &"z".repeat(LONG)).is_none());
9337        // The needle is compared against the NORMALIZED tag, so it must already
9338        // be in normalized (lowercase / snake) form.
9339        assert!(find_node_by_type(&roots, "DIV").is_none());
9340    }
9341
9342    #[test]
9343    fn find_node_by_type_valid_minimal_and_recursive() {
9344        let roots = vec![elem(node(
9345            "html",
9346            &[],
9347            vec![elem(node("head", &[], vec![elem(XmlNode::create("STYLE"))]))],
9348        ))];
9349        assert!(find_node_by_type(&roots, "html").is_some());
9350        assert!(
9351            find_node_by_type(&roots, "style").is_some(),
9352            "search recurses into the whole tree"
9353        );
9354        assert!(find_node_by_type(&roots, "body").is_none());
9355    }
9356
9357    #[test]
9358    fn find_node_by_type_prefers_the_shallowest_match() {
9359        let roots = vec![
9360            elem(node("div", &[], vec![elem(XmlNode::create("span"))])),
9361            elem(node("span", &[("id", "shallow")], vec![])),
9362        ];
9363        let found = find_node_by_type(&roots, "span").expect("found");
9364        assert_eq!(
9365            found.attributes.get_key("id").map(AzString::as_str),
9366            Some("shallow"),
9367            "direct children are scanned before recursing"
9368        );
9369    }
9370
9371    #[test]
9372    fn find_attribute_valid_minimal_and_missing() {
9373        let n = node("a", &[("href", "x"), ("id", "y")], vec![]);
9374        assert_eq!(find_attribute(&n, "href").map(AzString::as_str), Some("x"));
9375        assert_eq!(find_attribute(&n, "id").map(AzString::as_str), Some("y"));
9376        assert!(find_attribute(&n, "missing").is_none());
9377        assert!(find_attribute(&n, "").is_none());
9378        assert!(find_attribute(&n, "   ").is_none());
9379        assert!(find_attribute(&XmlNode::default(), "href").is_none());
9380        assert!(find_attribute(&n, &"z".repeat(LONG)).is_none());
9381    }
9382
9383    #[test]
9384    fn find_attribute_compares_against_the_normalized_key() {
9385        // `normalize_casing` turns `aria-label` into `aria_label`, so callers must
9386        // pass the NORMALIZED spelling — the raw HTML spelling does not match.
9387        let n = node("button", &[("aria-label", "Save")], vec![]);
9388        assert_eq!(
9389            find_attribute(&n, "aria_label").map(AzString::as_str),
9390            Some("Save")
9391        );
9392        assert!(
9393            find_attribute(&n, "aria-label").is_none(),
9394            "the hyphenated spelling never matches (keys are normalized first)"
9395        );
9396    }
9397
9398    #[test]
9399    fn find_attribute_unicode_keys_no_panic() {
9400        let n = node("div", &[("\u{130}", "v"), ("\u{1F600}", "w")], vec![]);
9401        assert!(find_attribute(&n, "\u{1F600}").is_some(), "emoji keys pass through");
9402        // 'İ' lowercases to 2 chars, so the normalized key is not 'İ'.
9403        assert!(find_attribute(&n, "\u{130}").is_none());
9404    }
9405
9406    // ================================================================
9407    // normalize_casing
9408    // ================================================================
9409
9410    #[test]
9411    fn normalize_casing_documented_forms() {
9412        assert_eq!(normalize_casing("abcDef"), "abc_def");
9413        assert_eq!(normalize_casing("AbcDef"), "abc_def");
9414        assert_eq!(normalize_casing("abc-def"), "abc_def");
9415        assert_eq!(normalize_casing("abc_def"), "abc_def");
9416    }
9417
9418    #[test]
9419    fn normalize_casing_edge_inputs() {
9420        assert_eq!(normalize_casing(""), "");
9421        assert_eq!(normalize_casing("---"), "", "separators alone produce no words");
9422        assert_eq!(normalize_casing("___"), "");
9423        assert_eq!(normalize_casing("A"), "a");
9424        assert_eq!(
9425            normalize_casing("ABC"),
9426            "a_b_c",
9427            "every uppercase char starts a new word"
9428        );
9429        assert_eq!(normalize_casing("h1"), "h1");
9430        assert_eq!(normalize_casing("   "), "   ", "whitespace is not a separator");
9431    }
9432
9433    #[test]
9434    fn normalize_casing_unicode_and_long_no_panic() {
9435        // 'İ' (U+0130) lowercases to TWO chars — the fn must not slice bytes.
9436        assert_eq!(normalize_casing("\u{130}"), "i\u{307}");
9437        assert_eq!(normalize_casing("\u{1F600}"), "\u{1F600}");
9438        assert_eq!(normalize_casing(&"a".repeat(50_000)).len(), 50_000);
9439        // 50k uppercase chars => 50k single-char words joined by '_'.
9440        assert_eq!(normalize_casing(&"A".repeat(50_000)).len(), 50_000 * 2 - 1);
9441    }
9442
9443    // ================================================================
9444    // get_item / get_item_internal
9445    // ================================================================
9446
9447    #[test]
9448    fn get_item_empty_hierarchy_returns_the_root() {
9449        let mut root = node("div", &[("id", "root")], vec![]);
9450        let got = get_item(&[], &mut root).expect("empty hierarchy => root");
9451        assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("root"));
9452    }
9453
9454    #[test]
9455    fn get_item_walks_nested_elements() {
9456        let mut root = node(
9457            "a",
9458            &[],
9459            vec![elem(node("b", &[], vec![elem(node("c", &[("id", "deep")], vec![]))]))],
9460        );
9461        let got = get_item(&[0, 0], &mut root).expect("a > b > c");
9462        assert_eq!(got.node_type.as_str(), "c");
9463        assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("deep"));
9464    }
9465
9466    #[test]
9467    fn get_item_out_of_bounds_and_text_nodes_return_none() {
9468        let mut root = node("a", &[], vec![txt("hello"), elem(XmlNode::create("b"))]);
9469        assert!(get_item(&[5], &mut root).is_none(), "out of bounds => None");
9470        assert!(
9471            get_item(&[usize::MAX], &mut root).is_none(),
9472            "usize::MAX index must not panic"
9473        );
9474        assert!(
9475            get_item(&[0], &mut root).is_none(),
9476            "index 0 is a TEXT node — not traversable"
9477        );
9478        assert!(get_item(&[1], &mut root).is_some());
9479        assert!(
9480            get_item(&[1, 0], &mut root).is_none(),
9481            "descending past a leaf => None"
9482        );
9483    }
9484
9485    #[test]
9486    fn get_item_deep_hierarchy_terminates() {
9487        // 400 levels: deep, but bounded by the (short) hierarchy vec, not by markup.
9488        let mut root = wrap_divs(400, node("div", &[("id", "bottom")], vec![]));
9489        let path = vec![0usize; 400];
9490        let got = get_item(&path, &mut root).expect("reaches the bottom");
9491        assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("bottom"));
9492    }
9493
9494    // ================================================================
9495    // decode_numeric_entity  (parser)
9496    // ================================================================
9497
9498    #[test]
9499    fn decode_numeric_entity_valid_minimal() {
9500        assert_eq!(decode_numeric_entity("#65"), Some('A'));
9501        assert_eq!(decode_numeric_entity("#x41"), Some('A'));
9502        assert_eq!(decode_numeric_entity("#X41"), Some('A'), "uppercase X accepted");
9503        assert_eq!(decode_numeric_entity("#x1F600"), Some('\u{1F600}'));
9504    }
9505
9506    #[test]
9507    fn decode_numeric_entity_empty_whitespace_garbage() {
9508        assert_eq!(decode_numeric_entity(""), None);
9509        assert_eq!(decode_numeric_entity("   "), None);
9510        assert_eq!(decode_numeric_entity("\t\n"), None);
9511        assert_eq!(decode_numeric_entity("amp"), None, "named entity is not numeric");
9512        assert_eq!(decode_numeric_entity("#"), None);
9513        assert_eq!(decode_numeric_entity("#x"), None);
9514        assert_eq!(decode_numeric_entity("#zz"), None);
9515        assert_eq!(decode_numeric_entity("# 65"), None);
9516        assert_eq!(decode_numeric_entity("#65junk"), None);
9517        assert_eq!(decode_numeric_entity("\u{1F600}"), None);
9518    }
9519
9520    #[test]
9521    fn decode_numeric_entity_boundary_code_points() {
9522        assert_eq!(decode_numeric_entity("#0"), Some('\u{0}'), "NUL is a valid char");
9523        assert_eq!(decode_numeric_entity("#x10FFFF"), Some('\u{10FFFF}'), "max scalar");
9524        assert_eq!(
9525            decode_numeric_entity("#x110000"),
9526            None,
9527            "one past the max scalar value"
9528        );
9529        assert_eq!(
9530            decode_numeric_entity("#xD800"),
9531            None,
9532            "a lone surrogate is not a char"
9533        );
9534        assert_eq!(
9535            decode_numeric_entity("#4294967295"),
9536            None,
9537            "u32::MAX is not a scalar value"
9538        );
9539        assert_eq!(
9540            decode_numeric_entity("#4294967296"),
9541            None,
9542            "one past u32::MAX must not wrap — it must fail to parse"
9543        );
9544        assert_eq!(decode_numeric_entity("#-1"), None, "negative is rejected by u32");
9545        assert_eq!(
9546            decode_numeric_entity("#xFFFFFFFFFFFF"),
9547            None,
9548            "hex overflow is rejected, not truncated"
9549        );
9550    }
9551
9552    #[test]
9553    fn decode_numeric_entity_extremely_long_terminates() {
9554        let s = format!("#{}", "9".repeat(LONG));
9555        assert_eq!(s.len(), LONG + 1);
9556        assert_eq!(decode_numeric_entity(&s), None, "overflows u32 => None");
9557    }
9558
9559    // ================================================================
9560    // decode_entities / prepare_string
9561    // ================================================================
9562
9563    #[test]
9564    fn decode_entities_leaves_unrecognized_sequences_verbatim() {
9565        assert_eq!(decode_entities(""), "");
9566        assert_eq!(decode_entities("&"), "&", "a bare '&' at EOF must not panic");
9567        assert_eq!(decode_entities("&;"), "&;", "empty entity body is not decoded");
9568        assert_eq!(decode_entities("&bogus;"), "&bogus;");
9569        assert_eq!(decode_entities("&nbsp;"), "&nbsp;", "&nbsp; is deliberately kept");
9570        // A ';' further away than MAX_ENTITY_BODY is not treated as an entity end.
9571        assert_eq!(
9572            decode_entities("&averyveryverylongbody;"),
9573            "&averyveryverylongbody;"
9574        );
9575    }
9576
9577    #[test]
9578    fn decode_entities_single_pass_prevents_double_decoding() {
9579        assert_eq!(decode_entities("&amp;lt;"), "&lt;", "&amp; must not re-open an entity");
9580        assert_eq!(decode_entities("&lt;&gt;&amp;&quot;&apos;"), "<>&\"'");
9581    }
9582
9583    #[test]
9584    fn decode_entities_unicode_and_long_input_terminate() {
9585        assert_eq!(decode_entities("\u{1F600}\u{0301}\u{130}"), "\u{1F600}\u{0301}\u{130}");
9586        // NOTE: each '&' triggers a `find(';')` over the whole remaining suffix, so
9587        // this is quadratic in the number of '&'. Keep the input modest.
9588        let amps = "&".repeat(20_000);
9589        assert_eq!(decode_entities(&amps).len(), 20_000);
9590    }
9591
9592    #[test]
9593    fn prepare_string_empty_and_whitespace() {
9594        assert_eq!(prepare_string(""), "");
9595        assert_eq!(prepare_string("   "), "");
9596        assert_eq!(prepare_string("\t\n\r  \n"), "");
9597    }
9598
9599    #[test]
9600    fn prepare_string_nbsp_becomes_a_space_that_survives_trim() {
9601        assert_eq!(prepare_string("&nbsp;"), " ");
9602        assert_eq!(prepare_string("a&nbsp;b"), "a b");
9603    }
9604
9605    #[test]
9606    fn prepare_string_collapses_a_blank_line_into_a_single_return() {
9607        assert_eq!(prepare_string("a\n\nb"), "a\nb");
9608        assert_eq!(prepare_string("a\n\n\n\nb"), "a\nb", "runs of blanks collapse");
9609    }
9610
9611    #[test]
9612    fn prepare_string_unicode_and_long_input_no_panic() {
9613        assert_eq!(prepare_string("\u{1F600}"), "\u{1F600}");
9614        assert_eq!(prepare_string("  \u{130}\u{0301}  "), "\u{130}\u{0301}");
9615        assert_eq!(prepare_string(&"x".repeat(LONG)).len(), LONG);
9616    }
9617
9618    /// BUG (reported): a soft-wrapped multi-line text node loses the word break
9619    /// on the FINAL line, because `prepare_string` skips the joining space when
9620    /// `line_idx == line_len - 1`. HTML must collapse the newline into a space.
9621    #[test]
9622    fn prepare_string_joins_wrapped_lines_with_a_space() {
9623        assert_eq!(
9624            prepare_string("Hello\nworld"),
9625            "Hello world",
9626            "a single newline between words must collapse to a space, not vanish"
9627        );
9628        assert_eq!(prepare_string("a\nb\nc"), "a b c");
9629    }
9630
9631    // ================================================================
9632    // parse_bool  (parser)
9633    // ================================================================
9634
9635    #[test]
9636    fn parse_bool_valid_minimal_and_everything_else_is_none() {
9637        assert_eq!(parse_bool("true"), Some(true));
9638        assert_eq!(parse_bool("false"), Some(false));
9639
9640        for s in [
9641            "", "   ", "\t\n", " true", "true ", "TRUE", "True", "FALSE", "1", "0", "-0", "yes",
9642            "no", "NaN", "inf", "9223372036854775807", "\u{1F600}", "true;garbage",
9643        ] {
9644            assert_eq!(parse_bool(s), None, "{s:?} must not parse as a bool");
9645        }
9646        assert_eq!(parse_bool(&"a".repeat(LONG)), None);
9647    }
9648
9649    // ================================================================
9650    // split_dynamic_string / format_args_dynamic
9651    // ================================================================
9652
9653    fn args(kv: &[(&str, &str)]) -> ComponentArgumentVec {
9654        kv.iter()
9655            .map(|(n, t)| ComponentArgument {
9656                name: AzString::from(*n),
9657                arg_type: AzString::from(*t),
9658            })
9659            .collect::<Vec<_>>()
9660            .into()
9661    }
9662
9663    #[test]
9664    fn split_dynamic_string_empty_and_plain() {
9665        assert!(split_dynamic_string("").is_empty());
9666        assert_eq!(
9667            split_dynamic_string("abc"),
9668            vec![DynamicItem::Str("abc".to_string())]
9669        );
9670    }
9671
9672    #[test]
9673    fn split_dynamic_string_format_spec_is_split_on_the_first_colon() {
9674        assert_eq!(
9675            split_dynamic_string("{a:?}"),
9676            vec![DynamicItem::Var {
9677                name: "a".to_string(),
9678                format_spec: Some("?".to_string()),
9679            }]
9680        );
9681        assert_eq!(
9682            split_dynamic_string("{a:x:y}"),
9683            vec![DynamicItem::Var {
9684                name: "a".to_string(),
9685                format_spec: Some("x:y".to_string()),
9686            }],
9687            "only the FIRST colon separates name from spec"
9688        );
9689    }
9690
9691    #[test]
9692    fn split_dynamic_string_unterminated_var_stays_literal() {
9693        // No closing brace => the scan runs to EOF and the text stays a literal.
9694        assert_eq!(
9695            split_dynamic_string("{unterminated"),
9696            vec![DynamicItem::Str("{unterminated".to_string())]
9697        );
9698        // A whitespace inside the braces aborts the variable scan.
9699        assert_eq!(
9700            split_dynamic_string("{a b}"),
9701            vec![DynamicItem::Str("{a b}".to_string())]
9702        );
9703    }
9704
9705    #[test]
9706    fn split_dynamic_string_unicode_and_long_input_terminate() {
9707        assert_eq!(
9708            split_dynamic_string("\u{1F600}{v}\u{130}"),
9709            vec![
9710                DynamicItem::Str("\u{1F600}".to_string()),
9711                DynamicItem::Var {
9712                    name: "v".to_string(),
9713                    format_spec: None
9714                },
9715                DynamicItem::Str("\u{130}".to_string()),
9716            ]
9717        );
9718        // The failed-variable scan advances the cursor by the amount it scanned,
9719        // so this stays linear rather than quadratic.
9720        let bomb = "{a".repeat(50_000);
9721        let out = split_dynamic_string(&bomb);
9722        assert!(out.len() <= 2, "a never-closed variable collapses, got {}", out.len());
9723
9724        let braces = "{".repeat(100_000);
9725        assert!(split_dynamic_string(&braces).len() <= 1);
9726    }
9727
9728    #[test]
9729    fn format_args_dynamic_documented_example() {
9730        let vars = args(&[("a", "value1"), ("b", "value2")]);
9731        assert_eq!(
9732            format_args_dynamic("hello {a}, {b}{{ {c} }}", &vars),
9733            "hello value1, value2{ {c} }"
9734        );
9735    }
9736
9737    #[test]
9738    fn format_args_dynamic_unknown_var_is_preserved_verbatim() {
9739        let empty = no_args();
9740        assert_eq!(format_args_dynamic("{c}", &empty), "{c}");
9741        assert_eq!(
9742            format_args_dynamic("{c:?}", &empty),
9743            "{c:?}",
9744            "the format spec is re-attached when the var is unresolved"
9745        );
9746        // Escaped braces round-trip to themselves.
9747        assert_eq!(format_args_dynamic("{{}}", &empty), "{{}}");
9748    }
9749
9750    #[test]
9751    fn format_args_dynamic_variable_names_are_normalized() {
9752        let vars = args(&[("my_var", "V")]);
9753        assert_eq!(format_args_dynamic("{myVar}", &vars), "V", "camelCase is normalized");
9754        assert_eq!(format_args_dynamic("{ my-var }", &vars), "{ my-var }",
9755            "whitespace inside the braces aborts the variable scan entirely");
9756        assert_eq!(format_args_dynamic("{my-var}", &vars), "V");
9757    }
9758
9759    #[test]
9760    fn format_args_dynamic_edge_values_no_panic() {
9761        let empty = no_args();
9762        assert_eq!(format_args_dynamic("", &empty), "");
9763        assert_eq!(format_args_dynamic("   ", &empty), "   ");
9764        assert_eq!(format_args_dynamic("\u{1F600}", &empty), "\u{1F600}");
9765        assert_eq!(format_args_dynamic(&"x".repeat(50_000), &empty).len(), 50_000);
9766    }
9767
9768    #[test]
9769    fn combine_and_replace_dynamic_items_on_empty_input() {
9770        assert_eq!(combine_and_replace_dynamic_items(&[], &no_args()), "");
9771    }
9772
9773    // ================================================================
9774    // compile_and_format_dynamic_items / format_args_for_rust_code
9775    // ================================================================
9776
9777    #[test]
9778    fn format_args_for_rust_code_empty_and_single_items() {
9779        assert_eq!(format_args_for_rust_code(""), "AzString::from_const_str(\"\")");
9780        assert_eq!(format_args_for_rust_code("hi"), "AzString::from_const_str(\"hi\")");
9781        assert_eq!(format_args_for_rust_code("{a}"), "a", "a lone var becomes a bare ident");
9782        assert_eq!(
9783            format_args_for_rust_code("{a:?}"),
9784            "format!(\"{:?}\", a).into()"
9785        );
9786    }
9787
9788    #[test]
9789    fn format_args_for_rust_code_multi_item_builds_a_format_call() {
9790        assert_eq!(
9791            format_args_for_rust_code("x={a} y={b}"),
9792            "format!(\"x={a} y={b}\", a, b).into()"
9793        );
9794    }
9795
9796    #[test]
9797    fn format_args_for_rust_code_escapes_quotes_in_literals() {
9798        let out = format_args_for_rust_code("say \"hi\" {a}");
9799        assert!(
9800            out.contains("say \\\"hi\\\""),
9801            "double quotes must be escaped for the emitted literal, got {out}"
9802        );
9803    }
9804
9805    #[test]
9806    fn compile_and_format_dynamic_items_edge_values() {
9807        assert_eq!(
9808            compile_and_format_dynamic_items(&[]),
9809            "AzString::from_const_str(\"\")"
9810        );
9811        assert_eq!(
9812            compile_and_format_dynamic_items(&[DynamicItem::Str(String::new())]),
9813            "AzString::from_const_str(\"\")"
9814        );
9815        assert_eq!(
9816            compile_and_format_dynamic_items(&[DynamicItem::Var {
9817                name: "  spaced  ".to_string(),
9818                format_spec: None,
9819            }]),
9820            "spaced",
9821            "the var name is trimmed + normalized"
9822        );
9823    }
9824
9825    // ================================================================
9826    // cap_first / camel_to_snake / esc_lit / c_creator_suffix
9827    // ================================================================
9828
9829    #[test]
9830    fn cap_first_edge_inputs() {
9831        assert_eq!(cap_first(""), "", "empty input must not panic");
9832        assert_eq!(cap_first("h1"), "H1");
9833        assert_eq!(cap_first("button"), "Button");
9834        assert_eq!(cap_first("A"), "A", "already-uppercase is idempotent");
9835        assert_eq!(cap_first("\u{1F600}x"), "\u{1F600}x", "emoji has no uppercase form");
9836        // 'ß' uppercases to TWO chars — the fn must not assume 1:1.
9837        assert_eq!(cap_first("\u{df}x"), "SSx");
9838        assert_eq!(cap_first(&"a".repeat(10_000)).len(), 10_000);
9839    }
9840
9841    #[test]
9842    fn camel_to_snake_documented_forms() {
9843        assert_eq!(camel_to_snake("ButtonNoA11y"), "button_no_a11y");
9844        assert_eq!(camel_to_snake("PWithText"), "p_with_text");
9845        assert_eq!(camel_to_snake("ANoA11y"), "a_no_a11y");
9846        assert_eq!(camel_to_snake("H1WithText"), "h1_with_text");
9847        assert_eq!(camel_to_snake("Div"), "div");
9848    }
9849
9850    #[test]
9851    fn camel_to_snake_edge_inputs() {
9852        assert_eq!(camel_to_snake(""), "");
9853        assert_eq!(camel_to_snake("A"), "a");
9854        assert_eq!(camel_to_snake("AB"), "ab", "an all-caps run is not split");
9855        assert_eq!(camel_to_snake("ABc"), "a_bc", "a caps run splits before the last cap");
9856        assert_eq!(camel_to_snake("\u{1F600}"), "\u{1F600}");
9857        assert_eq!(camel_to_snake(&"a".repeat(10_000)).len(), 10_000);
9858    }
9859
9860    #[test]
9861    fn esc_lit_escapes_backslash_before_quote() {
9862        assert_eq!(esc_lit(""), "");
9863        assert_eq!(esc_lit("plain"), "plain");
9864        assert_eq!(esc_lit("a\"b"), "a\\\"b");
9865        assert_eq!(esc_lit("a\\b"), "a\\\\b");
9866        // The backslash pass must run FIRST so an escaped quote is not double-escaped.
9867        assert_eq!(esc_lit("\\\""), "\\\\\\\"");
9868        assert_eq!(esc_lit("\u{1F600}"), "\u{1F600}");
9869    }
9870
9871    #[test]
9872    fn c_creator_suffix_edge_inputs() {
9873        assert_eq!(c_creator_suffix(""), "Div", "empty debug name falls back to Div");
9874        assert_eq!(c_creator_suffix("Div"), "Div");
9875        assert_eq!(c_creator_suffix("H1"), "H1");
9876        assert_eq!(c_creator_suffix("BlockQuote"), "Blockquote");
9877        assert_eq!(c_creator_suffix("FigCaption"), "Figcaption");
9878        assert_eq!(c_creator_suffix("\u{1F600}"), "\u{1F600}");
9879    }
9880
9881    // ================================================================
9882    // safe_container_tag
9883    // ================================================================
9884
9885    #[test]
9886    fn safe_container_tag_falls_back_to_div_for_arg_taking_widgets() {
9887        assert_eq!(safe_container_tag(""), "Div");
9888        assert_eq!(safe_container_tag("Div"), "Div");
9889        assert_eq!(safe_container_tag("Span"), "Span");
9890        assert_eq!(safe_container_tag("H1"), "H1");
9891        // Interactive / arg-taking elements deliberately degrade to a container.
9892        assert_eq!(safe_container_tag("Button"), "Div");
9893        assert_eq!(safe_container_tag("Input"), "Div");
9894        assert_eq!(safe_container_tag("A"), "Div");
9895        assert_eq!(safe_container_tag("\u{1F600}"), "Div");
9896        assert_eq!(safe_container_tag(&"z".repeat(10_000)), "Div");
9897    }
9898
9899    /// BUG (reported): `SAFE_CONTAINER_TAGS` is documented as holding the
9900    /// `NodeType`/`NodeTypeTag` **debug names**, and `safe_container_tag` compares
9901    /// against `format!("{:?}", tag_to_node_type(tag))`. But six entries are spelled
9902    /// with a different inner capitalization than the actual variant, so the
9903    /// comparison never matches and `<blockquote>`/`<figcaption>`/`<thead>`/`<tbody>`
9904    /// /`<tfoot>`/`<colgroup>` silently compile down to a plain `div`.
9905    #[test]
9906    fn safe_container_tag_matches_the_real_nodetype_debug_names() {
9907        for tag in [
9908            "blockquote",
9909            "figcaption",
9910            "thead",
9911            "tbody",
9912            "tfoot",
9913            "colgroup",
9914        ] {
9915            let dbg = format!("{:?}", tag_to_node_type(tag));
9916            assert_ne!(
9917                safe_container_tag(&dbg),
9918                "Div",
9919                "<{tag}> (NodeType debug name {dbg:?}) is a pure container and must keep \
9920                 its own creator instead of degrading to a div"
9921            );
9922        }
9923    }
9924
9925    // ================================================================
9926    // fmt_f32_lit  (numeric)
9927    // ================================================================
9928
9929    #[test]
9930    fn fmt_f32_lit_zero_and_negative() {
9931        assert_eq!(fmt_f32_lit(0.0), "0.0", "an integral value gains a decimal point");
9932        assert_eq!(fmt_f32_lit(-0.0), "-0.0");
9933        assert_eq!(fmt_f32_lit(-1.0), "-1.0");
9934        assert_eq!(fmt_f32_lit(1.5), "1.5");
9935        assert_eq!(fmt_f32_lit(-1.5), "-1.5");
9936    }
9937
9938    #[test]
9939    fn fmt_f32_lit_min_max_stay_parseable_float_literals() {
9940        for f in [f32::MAX, f32::MIN, f32::MIN_POSITIVE, f32::EPSILON] {
9941            let s = fmt_f32_lit(f);
9942            assert_eq!(
9943                s.parse::<f32>(),
9944                Ok(f),
9945                "{f:e} must round-trip through its emitted literal ({s})"
9946            );
9947        }
9948    }
9949
9950    #[test]
9951    fn fmt_f32_lit_nan_inf_produce_a_defined_result_and_do_not_panic() {
9952        // NOTE: these are NOT valid Rust/C float literals — a page with
9953        // `<progress value="NaN">` emits `create_progress_no_a11y(NaN, 1.0)`.
9954        // Pinned so a fix (e.g. clamping to 0.0) is a visible change.
9955        assert_eq!(fmt_f32_lit(f32::NAN), "NaN");
9956        assert_eq!(fmt_f32_lit(f32::INFINITY), "inf");
9957        assert_eq!(fmt_f32_lit(f32::NEG_INFINITY), "-inf");
9958    }
9959
9960    // ================================================================
9961    // node_direct_text / node_aria_label / node_attr_or / node_attr_f32
9962    // first_caption_text
9963    // ================================================================
9964
9965    #[test]
9966    fn node_direct_text_trims_and_skips_elements() {
9967        assert_eq!(node_direct_text(&XmlNode::default()), "");
9968        assert_eq!(node_direct_text(&node("p", &[], vec![txt("  Go  ")])), "Go");
9969        assert_eq!(
9970            node_direct_text(&node(
9971                "p",
9972                &[],
9973                vec![txt("a"), elem(node("b", &[], vec![txt("IGNORED")])), txt("b")]
9974            )),
9975            "a b",
9976            "direct text children are joined with a single space"
9977        );
9978        assert_eq!(
9979            node_direct_text(&node("p", &[], vec![txt("   "), txt("\t\n")])),
9980            "",
9981            "whitespace-only children are dropped"
9982        );
9983    }
9984
9985    #[test]
9986    fn node_aria_label_ignores_empty_and_whitespace() {
9987        assert_eq!(node_aria_label(&XmlNode::default()), None);
9988        assert_eq!(node_aria_label(&node("b", &[("aria-label", "")], vec![])), None);
9989        assert_eq!(node_aria_label(&node("b", &[("aria-label", "   ")], vec![])), None);
9990        assert_eq!(
9991            node_aria_label(&node("b", &[("aria-label", "  Save  ")], vec![])),
9992            Some("Save".to_string())
9993        );
9994    }
9995
9996    #[test]
9997    fn node_attr_or_returns_the_default_when_absent() {
9998        let n = node("a", &[("href", "/x"), ("empty", "")], vec![]);
9999        assert_eq!(node_attr_or(&n, "href", "FALLBACK"), "/x");
10000        assert_eq!(node_attr_or(&n, "missing", "FALLBACK"), "FALLBACK");
10001        assert_eq!(
10002            node_attr_or(&n, "empty", "FALLBACK"),
10003            "",
10004            "a present-but-empty attribute wins over the default"
10005        );
10006        assert_eq!(node_attr_or(&XmlNode::default(), "x", ""), "");
10007    }
10008
10009    #[test]
10010    fn node_attr_f32_zero_negative_and_defaults() {
10011        let n = node(
10012            "meter",
10013            &[("zero", "0"), ("negzero", "-0"), ("neg", "-2.5"), ("pad", "  1.5  ")],
10014            vec![],
10015        );
10016        assert_eq!(node_attr_f32(&n, "zero", 9.0), 0.0);
10017        assert!(node_attr_f32(&n, "negzero", 9.0).is_sign_negative());
10018        assert_eq!(node_attr_f32(&n, "neg", 9.0), -2.5);
10019        assert_eq!(node_attr_f32(&n, "pad", 9.0), 1.5, "the value is trimmed first");
10020        assert_eq!(node_attr_f32(&n, "missing", 9.0), 9.0);
10021    }
10022
10023    #[test]
10024    fn node_attr_f32_unparsable_falls_back_and_min_max_saturate() {
10025        let n = node(
10026            "meter",
10027            &[
10028                ("junk", "abc"),
10029                ("empty", ""),
10030                ("huge", "1e400"),
10031                ("tiny", "-1e400"),
10032                ("big", "340282350000000000000000000000000000000"),
10033            ],
10034            vec![],
10035        );
10036        assert_eq!(node_attr_f32(&n, "junk", 7.0), 7.0);
10037        assert_eq!(node_attr_f32(&n, "empty", 7.0), 7.0);
10038        assert!(
10039            node_attr_f32(&n, "huge", 7.0).is_infinite(),
10040            "an out-of-range literal saturates to inf, it does not panic"
10041        );
10042        assert_eq!(node_attr_f32(&n, "tiny", 7.0), f32::NEG_INFINITY);
10043        assert_eq!(node_attr_f32(&n, "big", 7.0), f32::MAX);
10044    }
10045
10046    #[test]
10047    fn node_attr_f32_accepts_nan_and_inf_spellings() {
10048        // Rust's f32 FromStr accepts "NaN"/"inf", so hostile markup can inject a
10049        // non-finite value straight into codegen (see fmt_f32_lit above).
10050        let n = node("progress", &[("value", "NaN"), ("max", "inf")], vec![]);
10051        assert!(node_attr_f32(&n, "value", 0.0).is_nan());
10052        assert_eq!(node_attr_f32(&n, "max", 1.0), f32::INFINITY);
10053    }
10054
10055    #[test]
10056    fn node_attr_f32_nan_default_is_returned_verbatim() {
10057        assert!(node_attr_f32(&XmlNode::default(), "x", f32::NAN).is_nan());
10058        assert_eq!(node_attr_f32(&XmlNode::default(), "x", f32::INFINITY), f32::INFINITY);
10059    }
10060
10061    #[test]
10062    fn first_caption_text_edges() {
10063        assert_eq!(first_caption_text(&XmlNode::default()), None);
10064        assert_eq!(
10065            first_caption_text(&node("table", &[], vec![elem(node("caption", &[], vec![]))])),
10066            None,
10067            "an empty caption yields None"
10068        );
10069        assert_eq!(
10070            first_caption_text(&node(
10071                "table",
10072                &[],
10073                vec![elem(node("CAPTION", &[], vec![txt("  Hi  ")]))]
10074            )),
10075            Some("Hi".to_string()),
10076            "the tag match is ASCII-case-insensitive and the text is trimmed"
10077        );
10078    }
10079
10080    // ================================================================
10081    // analyze_node_ctor / CtorArg / NodeCtor
10082    // ================================================================
10083
10084    #[test]
10085    fn analyze_node_ctor_plain_for_unknown_and_empty_tags() {
10086        assert!(matches!(analyze_node_ctor("div", &XmlNode::default()), NodeCtor::Plain));
10087        assert!(matches!(analyze_node_ctor("", &XmlNode::default()), NodeCtor::Plain));
10088        assert!(matches!(
10089            analyze_node_ctor("\u{1F600}", &XmlNode::default()),
10090            NodeCtor::Plain
10091        ));
10092        let plain = analyze_node_ctor("div", &XmlNode::default());
10093        assert_eq!(plain.render_rust(), None);
10094        assert_eq!(plain.render_c(), None);
10095        assert_eq!(plain.render_fluent(&CompileTarget::Cpp), None);
10096        assert!(!plain.consumes_text());
10097        assert!(!plain.skip_caption());
10098    }
10099
10100    #[test]
10101    fn analyze_node_ctor_with_text_tier_requires_actual_text() {
10102        // Empty <p> stays a plain container (has_only_text_children() is vacuously
10103        // true for a childless node, so `has_text` is the real gate).
10104        assert!(matches!(analyze_node_ctor("p", &XmlNode::default()), NodeCtor::Plain));
10105        // <p> with an element child is not "pure text" either.
10106        let mixed = node("p", &[], vec![txt("a"), elem(XmlNode::create("span"))]);
10107        assert!(matches!(analyze_node_ctor("p", &mixed), NodeCtor::Plain));
10108
10109        let pure = node("p", &[], vec![txt("  Hello  ")]);
10110        let ctor = analyze_node_ctor("p", &pure);
10111        assert!(ctor.consumes_text(), "the text is folded into the constructor");
10112        assert_eq!(
10113            ctor.render_rust().as_deref(),
10114            Some("Dom::create_p_with_text(AzString::from(\"Hello\"))")
10115        );
10116        assert_eq!(
10117            ctor.render_c().as_deref(),
10118            Some("AzDom_createPWithText(AZ_STR(\"Hello\"))")
10119        );
10120        assert_eq!(
10121            ctor.render_fluent(&CompileTarget::Python).as_deref(),
10122            Some("azul.Dom.create_p_with_text(\"Hello\")")
10123        );
10124    }
10125
10126    #[test]
10127    fn analyze_node_ctor_button_with_and_without_aria() {
10128        let plain_btn = node("button", &[], vec![txt("Go")]);
10129        assert_eq!(
10130            analyze_node_ctor("button", &plain_btn).render_rust().as_deref(),
10131            Some("Dom::create_button_no_a11y(AzString::from(\"Go\"))")
10132        );
10133
10134        let aria_btn = node("button", &[("aria-label", "Save")], vec![txt("Go")]);
10135        assert_eq!(
10136            analyze_node_ctor("button", &aria_btn).render_rust().as_deref(),
10137            Some(
10138                "Dom::create_button(AzString::from(\"Go\"), \
10139                 SmallAriaInfo::label(AzString::from(\"Save\")))"
10140            )
10141        );
10142    }
10143
10144    #[test]
10145    fn analyze_node_ctor_escapes_quotes_and_backslashes_in_text() {
10146        let btn = node("button", &[], vec![txt("say \"hi\"\\now")]);
10147        let rust = analyze_node_ctor("button", &btn).render_rust().expect("semantic");
10148        assert!(
10149            rust.contains("say \\\"hi\\\"\\\\now"),
10150            "quotes and backslashes must be escaped for the literal, got {rust}"
10151        );
10152    }
10153
10154    #[test]
10155    fn analyze_node_ctor_anchor_uses_option_string_when_it_has_no_text() {
10156        let bare = node("a", &[], vec![]);
10157        assert_eq!(
10158            analyze_node_ctor("a", &bare).render_rust().as_deref(),
10159            Some("Dom::create_a_no_a11y(AzString::from(\"\"), OptionString::None)"),
10160            "a missing href defaults to an empty string, missing text to OptionString::None"
10161        );
10162
10163        let full = node("a", &[("href", "/x")], vec![txt("Home")]);
10164        assert_eq!(
10165            analyze_node_ctor("a", &full).render_rust().as_deref(),
10166            Some(
10167                "Dom::create_a_no_a11y(AzString::from(\"/x\"), \
10168                 OptionString::Some(AzString::from(\"Home\")))"
10169            )
10170        );
10171        assert_eq!(
10172            analyze_node_ctor("a", &full).render_c().as_deref(),
10173            Some("AzDom_createANoA11y(AZ_STR(\"/x\"), AzOptionString_some(AZ_STR(\"Home\")))")
10174        );
10175    }
10176
10177    #[test]
10178    fn analyze_node_ctor_table_aria_form_skips_the_literal_caption() {
10179        let t = node(
10180            "table",
10181            &[("aria-label", "Prices")],
10182            vec![elem(node("caption", &[], vec![txt("Q1")]))],
10183        );
10184        let ctor = analyze_node_ctor("table", &t);
10185        assert!(ctor.skip_caption(), "the aria form injects its own caption child");
10186        assert_eq!(
10187            ctor.render_rust().as_deref(),
10188            Some(
10189                "Dom::create_table(AzString::from(\"Q1\"), \
10190                 SmallAriaInfo::label(AzString::from(\"Prices\")))"
10191            )
10192        );
10193
10194        let plain = analyze_node_ctor("table", &node("table", &[], vec![]));
10195        assert!(!plain.skip_caption());
10196        assert_eq!(plain.render_rust().as_deref(), Some("Dom::create_table_no_a11y()"));
10197    }
10198
10199    #[test]
10200    fn analyze_node_ctor_scalar_widgets_use_defaults_and_emit_float_literals() {
10201        let p = node("progress", &[], vec![]);
10202        assert_eq!(
10203            analyze_node_ctor("progress", &p).render_rust().as_deref(),
10204            Some("Dom::create_progress_no_a11y(0.0, 1.0)"),
10205            "missing value/max fall back to 0.0 / 1.0 as float literals"
10206        );
10207
10208        let m = node("meter", &[("value", "5"), ("min", "-1"), ("max", "10")], vec![]);
10209        assert_eq!(
10210            analyze_node_ctor("meter", &m).render_c().as_deref(),
10211            Some("AzDom_createMeterNoA11y(5.0f, -1.0f, 10.0f)")
10212        );
10213    }
10214
10215    /// Non-finite attribute values flow straight into the emitted literal. Pinned
10216    /// so that a fix (clamping / rejecting them) shows up as a change.
10217    #[test]
10218    fn analyze_node_ctor_non_finite_attributes_emit_non_finite_literals() {
10219        let p = node("progress", &[("value", "NaN"), ("max", "inf")], vec![]);
10220        let rust = analyze_node_ctor("progress", &p).render_rust().expect("semantic");
10221        assert_eq!(rust, "Dom::create_progress_no_a11y(NaN, inf)");
10222    }
10223
10224    #[test]
10225    fn ctor_arg_render_targets_are_distinct() {
10226        let s = CtorArg::Str("a\"b".to_string());
10227        assert_eq!(s.render_rust(), "AzString::from(\"a\\\"b\")");
10228        assert_eq!(s.render_c(), "AZ_STR(\"a\\\"b\")");
10229        assert_eq!(s.render_cpp(), "String(\"a\\\"b\")");
10230        assert_eq!(s.render_python(), "\"a\\\"b\"");
10231
10232        assert_eq!(CtorArg::OptNone.render_rust(), "OptionString::None");
10233        assert_eq!(CtorArg::OptNone.render_c(), "AzOptionString_none()");
10234        assert_eq!(CtorArg::OptNone.render_cpp(), "OptionString::none()");
10235        assert_eq!(CtorArg::OptNone.render_python(), "azul.OptionString.none()");
10236
10237        assert_eq!(CtorArg::Float(0.0).render_rust(), "0.0");
10238        assert_eq!(CtorArg::Float(0.0).render_c(), "0.0f");
10239        assert_eq!(CtorArg::Float(f32::NAN).render_c(), "NaNf");
10240    }
10241
10242    #[test]
10243    fn node_ctor_render_fluent_returns_none_for_non_fluent_targets() {
10244        let ctor = analyze_node_ctor("p", &node("p", &[], vec![txt("x")]));
10245        assert!(ctor.render_fluent(&CompileTarget::Rust).is_none());
10246        assert!(ctor.render_fluent(&CompileTarget::C).is_none());
10247        assert!(ctor.render_fluent(&CompileTarget::Cpp).is_some());
10248        assert!(ctor.render_fluent(&CompileTarget::Python).is_some());
10249    }
10250
10251    // ================================================================
10252    // format_component_args / compile_component / compile_components
10253    // ================================================================
10254
10255    #[test]
10256    fn format_component_args_empty_and_ordering() {
10257        assert_eq!(format_component_args(&no_args()), "");
10258        // Args are sorted DESCENDING by their rendered "name: type" string.
10259        assert_eq!(
10260            format_component_args(&args(&[("a", "u32"), ("b", "String")])),
10261            "b: String, a: u32"
10262        );
10263    }
10264
10265    #[test]
10266    fn format_component_args_edge_values_no_panic() {
10267        let a = args(&[("", ""), ("\u{1F600}", "\u{130}")]);
10268        let out = format_component_args(&a);
10269        assert!(out.contains(": "), "still emits `name: type` pairs, got {out:?}");
10270    }
10271
10272    #[test]
10273    fn compile_component_emits_a_render_fn() {
10274        let ca = ComponentArguments {
10275            args: args(&[("count", "u32")]),
10276            accepts_text: false,
10277        };
10278        let out = compile_component("MyWidget", &ca, "Dom::create_div()");
10279        assert!(out.contains("pub fn render(count: u32) -> Dom {"), "got:\n{out}");
10280        assert!(out.contains("#[inline]"), "a one-line body is inlined");
10281    }
10282
10283    #[test]
10284    fn compile_component_accepts_text_prepends_the_text_param() {
10285        let ca = ComponentArguments {
10286            args: args(&[("count", "u32")]),
10287            accepts_text: true,
10288        };
10289        let out = compile_component("my-widget", &ca, "Dom::create_div()");
10290        assert!(
10291            out.contains("pub fn render(text: AzString, count: u32) -> Dom {"),
10292            "got:\n{out}"
10293        );
10294
10295        let ca_no_args = ComponentArguments {
10296            args: no_args(),
10297            accepts_text: true,
10298        };
10299        let out = compile_component("w", &ca_no_args, "Dom::create_div()");
10300        assert!(
10301            out.contains("pub fn render(text: AzString) -> Dom {"),
10302            "no trailing comma when there are no extra args, got:\n{out}"
10303        );
10304    }
10305
10306    #[test]
10307    fn compile_component_empty_name_and_body_no_panic() {
10308        let ca = ComponentArguments::default();
10309        let out = compile_component("", &ca, "");
10310        assert!(out.contains("pub fn render() -> Dom {"), "got:\n{out}");
10311    }
10312
10313    #[test]
10314    fn compile_components_of_an_empty_list_is_empty() {
10315        assert_eq!(compile_components(Vec::new()), "");
10316    }
10317
10318    // ================================================================
10319    // parse_svg_float / parse_svg_points  (parser / numeric)
10320    // ================================================================
10321
10322    #[test]
10323    fn parse_svg_float_none_empty_whitespace_garbage() {
10324        assert_eq!(parse_svg_float(None), None);
10325        let empty = AzString::from("");
10326        assert_eq!(parse_svg_float(Some(&empty)), None);
10327        let ws = AzString::from("   \t\n");
10328        assert_eq!(parse_svg_float(Some(&ws)), None);
10329        let junk = AzString::from("10px");
10330        assert_eq!(parse_svg_float(Some(&junk)), None, "units are not stripped");
10331        let uni = AzString::from("\u{1F600}");
10332        assert_eq!(parse_svg_float(Some(&uni)), None);
10333    }
10334
10335    #[test]
10336    fn parse_svg_float_valid_and_boundary_numbers() {
10337        let padded = AzString::from("  1.5  ");
10338        assert_eq!(parse_svg_float(Some(&padded)), Some(1.5), "value is trimmed");
10339        let zero = AzString::from("0");
10340        assert_eq!(parse_svg_float(Some(&zero)), Some(0.0));
10341        let negzero = AzString::from("-0");
10342        assert!(parse_svg_float(Some(&negzero)).unwrap().is_sign_negative());
10343        let huge = AzString::from("1e400");
10344        assert!(
10345            parse_svg_float(Some(&huge)).unwrap().is_infinite(),
10346            "overflow saturates to inf rather than erroring"
10347        );
10348        let nan = AzString::from("NaN");
10349        assert!(parse_svg_float(Some(&nan)).unwrap().is_nan());
10350        let inf = AzString::from("-inf");
10351        assert_eq!(parse_svg_float(Some(&inf)), Some(f32::NEG_INFINITY));
10352    }
10353
10354    #[test]
10355    fn parse_svg_points_rejects_degenerate_input() {
10356        assert!(parse_svg_points("", false).is_none());
10357        assert!(parse_svg_points("   ", false).is_none());
10358        assert!(parse_svg_points("garbage", false).is_none());
10359        assert!(parse_svg_points("1 2", false).is_none(), "a single point is not a line");
10360        assert!(
10361            parse_svg_points("1 2 3", false).is_none(),
10362            "an odd coordinate count is rejected"
10363        );
10364        assert!(parse_svg_points("\u{1F600}", false).is_none());
10365    }
10366
10367    #[test]
10368    fn parse_svg_points_valid_minimal_and_close() {
10369        let open = parse_svg_points("0,0 10,0", false).expect("two points => one line");
10370        assert_eq!(open.rings.as_ref().len(), 1);
10371        assert_eq!(open.rings.as_ref()[0].items.as_ref().len(), 1);
10372
10373        // `close` adds a segment back to the first point when it differs.
10374        let closed = parse_svg_points("0,0 10,0 10,10", true).expect("triangle");
10375        assert_eq!(
10376            closed.rings.as_ref()[0].items.as_ref().len(),
10377            3,
10378            "2 segments + 1 closing segment"
10379        );
10380
10381        // Already-closed rings do not get a duplicate closing segment.
10382        let already = parse_svg_points("0,0 10,0 0,0", true).expect("closed ring");
10383        assert_eq!(already.rings.as_ref()[0].items.as_ref().len(), 2);
10384    }
10385
10386    #[test]
10387    fn parse_svg_points_skips_unparsable_tokens_and_handles_boundaries() {
10388        // Unparsable coordinates are silently dropped, which can shift the pairing.
10389        let p = parse_svg_points("0,0 junk 10,0", false).expect("junk token dropped");
10390        assert_eq!(p.rings.as_ref()[0].items.as_ref().len(), 1);
10391
10392        let nan = parse_svg_points("NaN,0 1,1", false).expect("NaN is a parseable f32");
10393        assert_eq!(nan.rings.as_ref()[0].items.as_ref().len(), 1);
10394    }
10395
10396    #[test]
10397    fn parse_svg_points_extremely_long_terminates() {
10398        let pts = "1,2 ".repeat(20_000);
10399        let p = parse_svg_points(&pts, false).expect("20k points");
10400        assert_eq!(p.rings.as_ref()[0].items.as_ref().len(), 19_999);
10401    }
10402
10403    // ================================================================
10404    // CompactDomBuilder  (constructor / numeric)
10405    // ================================================================
10406
10407    #[test]
10408    fn compact_dom_builder_new_and_with_capacity_start_empty() {
10409        for b in [
10410            CompactDomBuilder::new(),
10411            CompactDomBuilder::with_capacity(0),
10412            CompactDomBuilder::with_capacity(1),
10413            CompactDomBuilder::with_capacity(4096),
10414        ] {
10415            let fd = b.finish();
10416            assert_eq!(fd.node_hierarchy.as_ref().len(), 0);
10417            assert_eq!(fd.node_data.as_ref().len(), 0);
10418            assert_eq!(fd.css.as_ref().len(), 0);
10419        }
10420        assert_eq!(
10421            CompactDomBuilder::default().finish().node_data.as_ref().len(),
10422            0
10423        );
10424    }
10425
10426    #[test]
10427    fn compact_dom_builder_close_node_on_an_empty_stack_is_a_no_op() {
10428        let mut b = CompactDomBuilder::new();
10429        b.close_node();
10430        b.close_node();
10431        assert_eq!(b.finish().node_hierarchy.as_ref().len(), 0, "no panic, no nodes");
10432    }
10433
10434    #[test]
10435    fn compact_dom_builder_keeps_hierarchy_and_data_parallel() {
10436        let mut b = CompactDomBuilder::new();
10437        b.open_node(NodeData::create_node(NodeType::Html));
10438        b.add_leaf(NodeData::create_text("a"));
10439        b.add_leaf(NodeData::create_text("b"));
10440        b.close_node();
10441        let fd = b.finish();
10442        assert_eq!(fd.node_data.as_ref().len(), 3);
10443        assert_eq!(
10444            fd.node_hierarchy.as_ref().len(),
10445            fd.node_data.as_ref().len(),
10446            "the two arenas must stay the same length"
10447        );
10448    }
10449
10450    #[test]
10451    fn compact_dom_builder_unclosed_node_still_finishes() {
10452        let mut b = CompactDomBuilder::new();
10453        b.open_node(NodeData::create_node(NodeType::Div));
10454        // Deliberately NOT closed.
10455        let fd = b.finish();
10456        assert_eq!(fd.node_hierarchy.as_ref().len(), 1);
10457        assert_eq!(
10458            fd.node_hierarchy.as_ref()[0].last_child, 0,
10459            "last_child stays unset when close_node() is never called"
10460        );
10461    }
10462
10463    #[test]
10464    fn compact_dom_builder_add_css_accepts_zero_and_usize_max_node_ids() {
10465        let mut b = CompactDomBuilder::new();
10466        b.add_css(0, Css::empty());
10467        b.add_css(usize::MAX, Css::empty());
10468        let fd = b.finish();
10469        assert_eq!(fd.css.as_ref().len(), 2);
10470        assert_eq!(fd.css.as_ref()[0].node_id, 0);
10471        assert_eq!(
10472            fd.css.as_ref()[1].node_id,
10473            usize::MAX,
10474            "an out-of-range node id is stored verbatim (no bounds check, no panic)"
10475        );
10476    }
10477
10478    // ================================================================
10479    // xml_node_to_dom_fast / xml_node_to_fast_dom  (numeric: depth)
10480    // ================================================================
10481
10482    #[test]
10483    fn xml_node_to_dom_fast_depth_zero_builds_children() {
10484        let map = ComponentMap::default();
10485        let n = node("div", &[], vec![txt("hi"), elem(XmlNode::create("span"))]);
10486        let dom = xml_node_to_dom_fast(&n, &map, false, 0).expect("ok");
10487        assert_eq!(dom.children.as_ref().len(), 2);
10488    }
10489
10490    #[test]
10491    fn xml_node_to_dom_fast_at_and_past_the_depth_cap_truncates_instead_of_panicking() {
10492        let map = ComponentMap::default();
10493        let n = node("div", &[], vec![txt("hi")]);
10494
10495        let at_cap = xml_node_to_dom_fast(&n, &map, false, MAX_XML_NESTING_DEPTH).expect("ok");
10496        assert!(
10497            at_cap.children.as_ref().is_empty(),
10498            "at the cap the node is emitted without children"
10499        );
10500
10501        let saturated = xml_node_to_dom_fast(&n, &map, false, usize::MAX)
10502            .expect("usize::MAX depth must not overflow when computing depth + 1");
10503        assert!(saturated.children.as_ref().is_empty());
10504
10505        let below = xml_node_to_dom_fast(&n, &map, false, MAX_XML_NESTING_DEPTH - 1).expect("ok");
10506        assert_eq!(below.children.as_ref().len(), 1, "one below the cap still recurses");
10507    }
10508
10509    #[test]
10510    fn xml_node_to_fast_dom_at_the_depth_cap_still_emits_the_node() {
10511        let map = ComponentMap::default();
10512        let n = node("div", &[], vec![txt("hi")]);
10513
10514        let mut b = CompactDomBuilder::new();
10515        xml_node_to_fast_dom(&n, &map, false, &mut b, usize::MAX).expect("no overflow");
10516        let fd = b.finish();
10517        assert_eq!(
10518            fd.node_data.as_ref().len(),
10519            1,
10520            "the node itself is still opened+closed, only its children are dropped"
10521        );
10522
10523        let mut b2 = CompactDomBuilder::new();
10524        xml_node_to_fast_dom(&n, &map, false, &mut b2, 0).expect("ok");
10525        assert_eq!(b2.finish().node_data.as_ref().len(), 2, "node + text child");
10526    }
10527
10528    #[test]
10529    fn apply_xml_node_attributes_extreme_tabindex_does_not_panic() {
10530        let map = ComponentMap::default();
10531        for v in [
10532            "0",
10533            "-1",
10534            "2147483647",
10535            "9223372036854775807",
10536            "-9223372036854775808",
10537            "99999999999999999999999999999999",
10538            "abc",
10539            "",
10540            "\u{1F600}",
10541        ] {
10542            let n = node("div", &[("tabindex", v), ("focusable", "true")], vec![]);
10543            assert!(
10544                xml_node_to_dom_fast(&n, &map, false, 0).is_ok(),
10545                "tabindex={v:?} must not panic"
10546            );
10547        }
10548    }
10549
10550    #[test]
10551    fn apply_xml_node_attributes_img_width_height_garbage_falls_back_to_zero() {
10552        let map = ComponentMap::default();
10553        let n = node(
10554            "img",
10555            &[("src", "a.png"), ("width", "-5"), ("height", "not-a-number")],
10556            vec![],
10557        );
10558        let dom = xml_node_to_dom_fast(&n, &map, false, 0).expect("ok");
10559        match dom.root.get_node_type() {
10560            NodeType::Image(_) => {}
10561            other => panic!("expected an Image node, got {other:?}"),
10562        }
10563    }
10564
10565    // ================================================================
10566    // set_stringified_attributes  (numeric: tabs / tabindex)
10567    // ================================================================
10568
10569    #[test]
10570    fn set_stringified_attributes_zero_tabs_and_empty_attrs() {
10571        let mut s = String::new();
10572        set_stringified_attributes(&mut s, &attrs(&[]), &no_args(), 0);
10573        assert_eq!(s, "", "nothing to emit for an attribute-less node");
10574    }
10575
10576    #[test]
10577    fn set_stringified_attributes_splits_ids_and_classes_on_whitespace() {
10578        let mut s = String::new();
10579        set_stringified_attributes(
10580            &mut s,
10581            &attrs(&[("id", "a  b"), ("class", "c\td")]),
10582            &no_args(),
10583            0,
10584        );
10585        assert!(s.contains(".with_id(\"a\")"), "got {s:?}");
10586        assert!(s.contains(".with_id(\"b\")"));
10587        assert!(s.contains(".with_class(\"c\")"));
10588        assert!(s.contains(".with_class(\"d\")"));
10589    }
10590
10591    #[test]
10592    fn set_stringified_attributes_tabindex_boundaries() {
10593        let cases: &[(&str, &str)] = &[
10594            ("0", "TabIndex::Auto"),
10595            ("5", "TabIndex::OverrideInParent(5)"),
10596            ("-1", "TabIndex::NoKeyboardFocus"),
10597        ];
10598        for (val, expected) in cases {
10599            let mut s = String::new();
10600            set_stringified_attributes(&mut s, &attrs(&[("tabindex", val)]), &no_args(), 0);
10601            assert!(s.contains(expected), "tabindex={val:?} => {s:?}");
10602        }
10603
10604        // Unparsable / overflowing values emit nothing rather than panicking.
10605        for val in ["abc", "", "99999999999999999999999999999999", "1.5"] {
10606            let mut s = String::new();
10607            set_stringified_attributes(&mut s, &attrs(&[("tabindex", val)]), &no_args(), 0);
10608            assert!(
10609                !s.contains("TabIndex"),
10610                "tabindex={val:?} must be ignored, got {s:?}"
10611            );
10612        }
10613    }
10614
10615    #[test]
10616    fn set_stringified_attributes_focusable_only_accepts_exact_true_false() {
10617        let mut s = String::new();
10618        set_stringified_attributes(&mut s, &attrs(&[("focusable", "true")]), &no_args(), 0);
10619        assert!(s.contains("TabIndex::Auto"));
10620
10621        let mut s = String::new();
10622        set_stringified_attributes(&mut s, &attrs(&[("focusable", "false")]), &no_args(), 0);
10623        assert!(s.contains("TabIndex::NoKeyboardFocus"));
10624
10625        let mut s = String::new();
10626        set_stringified_attributes(&mut s, &attrs(&[("focusable", "TRUE")]), &no_args(), 0);
10627        assert!(s.is_empty(), "casing other than `true`/`false` is ignored, got {s:?}");
10628    }
10629
10630    #[test]
10631    fn set_stringified_attributes_large_tab_depth_does_not_overflow() {
10632        // `tabs` becomes `"    ".repeat(tabs)`; a large-but-sane nesting depth must
10633        // stay linear and allocate without panicking.
10634        let mut s = String::new();
10635        set_stringified_attributes(&mut s, &attrs(&[("id", "x")]), &no_args(), 1_000);
10636        assert!(s.contains(".with_id(\"x\")"));
10637        assert!(s.len() > 4_000, "the 1000-level indent is actually emitted");
10638    }
10639
10640    // ================================================================
10641    // group_matches / CssMatcher  (numeric: indices)
10642    // ================================================================
10643
10644    fn refs(v: &[CssPathSelector]) -> Vec<&CssPathSelector> {
10645        v.iter().collect()
10646    }
10647
10648    #[test]
10649    fn group_matches_global_matches_at_any_index() {
10650        let a = vec![CssPathSelector::Global];
10651        assert!(group_matches(&refs(&a), &[], 0, 0));
10652        assert!(
10653            group_matches(&refs(&a), &[], usize::MAX, usize::MAX),
10654            "usize::MAX indices must not overflow"
10655        );
10656    }
10657
10658    #[test]
10659    fn group_matches_type_class_id() {
10660        let div = vec![CssPathSelector::Type(NodeTypeTag::Div)];
10661        let p = vec![CssPathSelector::Type(NodeTypeTag::P)];
10662        assert!(group_matches(&refs(&div), &refs(&div), 0, 1));
10663        assert!(!group_matches(&refs(&div), &refs(&p), 0, 1));
10664        assert!(!group_matches(&refs(&div), &[], 0, 1), "an empty haystack never matches");
10665
10666        let cls = vec![CssPathSelector::Class(AzString::from("x"))];
10667        assert!(group_matches(&refs(&cls), &refs(&cls), 0, 1));
10668        let id = vec![CssPathSelector::Id(AzString::from("x"))];
10669        assert!(!group_matches(&refs(&id), &refs(&cls), 0, 1), "an id is not a class");
10670    }
10671
10672    #[test]
10673    fn group_matches_first_and_last_pseudo_at_boundaries() {
10674        let first = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::First)];
10675        assert!(group_matches(&refs(&first), &[], 0, 10));
10676        assert!(!group_matches(&refs(&first), &[], 1, 10));
10677
10678        let last = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::Last)];
10679        assert!(group_matches(&refs(&last), &[], 9, 10));
10680        assert!(!group_matches(&refs(&last), &[], 8, 10));
10681        assert!(
10682            group_matches(&refs(&last), &[], 0, 0),
10683            "parent_children == 0 saturates to 0, so index 0 counts as last"
10684        );
10685    }
10686
10687    #[test]
10688    fn group_matches_nth_child_even_odd_and_number() {
10689        let even = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10690            CssNthChildSelector::Even,
10691        ))];
10692        assert!(group_matches(&refs(&even), &[], 0, 0));
10693        assert!(!group_matches(&refs(&even), &[], 1, 0));
10694        assert!(
10695            !group_matches(&refs(&even), &[], usize::MAX, 0),
10696            "usize::MAX is odd"
10697        );
10698
10699        let odd = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10700            CssNthChildSelector::Odd,
10701        ))];
10702        assert!(group_matches(&refs(&odd), &[], 1, 0));
10703        assert!(!group_matches(&refs(&odd), &[], 2, 0));
10704
10705        let n = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10706            CssNthChildSelector::Number(u32::MAX),
10707        ))];
10708        assert!(group_matches(&refs(&n), &[], u32::MAX as usize, 0));
10709        assert!(!group_matches(&refs(&n), &[], 0, 0));
10710    }
10711
10712    #[test]
10713    fn group_matches_nth_child_pattern_zero_repeat_does_not_divide_by_zero() {
10714        let zero = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10715            CssNthChildSelector::Pattern(CssNthChildPattern {
10716                pattern_repeat: 0,
10717                offset: 0,
10718            }),
10719        ))];
10720        // `is_multiple_of(0)` is `self == 0` — no division by zero.
10721        assert!(group_matches(&refs(&zero), &[], 0, 0));
10722        assert!(!group_matches(&refs(&zero), &[], 5, 0));
10723
10724        let offset_past = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10725            CssNthChildSelector::Pattern(CssNthChildPattern {
10726                pattern_repeat: 2,
10727                offset: u32::MAX,
10728            }),
10729        ))];
10730        assert!(
10731            group_matches(&refs(&offset_past), &[], 0, 0),
10732            "index - offset saturates to 0 rather than underflowing"
10733        );
10734    }
10735
10736    #[test]
10737    fn group_matches_structural_combinators_never_match() {
10738        for sel in [
10739            CssPathSelector::Children,
10740            CssPathSelector::DirectChildren,
10741            CssPathSelector::AdjacentSibling,
10742            CssPathSelector::GeneralSibling,
10743        ] {
10744            let a = vec![sel.clone()];
10745            assert!(
10746                !group_matches(&refs(&a), &refs(&a), 0, 1),
10747                "{sel:?} is a combinator, not a matchable group member"
10748            );
10749        }
10750    }
10751
10752    #[test]
10753    fn css_matcher_empty_path_never_matches() {
10754        let m = CssMatcher {
10755            path: Vec::new(),
10756            indices_in_parent: vec![0],
10757            children_length: vec![0],
10758        };
10759        let path = CssPath {
10760            selectors: vec![CssPathSelector::Type(NodeTypeTag::Body)].into(),
10761        };
10762        assert!(!m.matches(&path), "an empty matcher path can never match");
10763
10764        let m2 = CssMatcher {
10765            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10766            indices_in_parent: vec![0],
10767            children_length: vec![0],
10768        };
10769        let empty_path = CssPath {
10770            selectors: Vec::new().into(),
10771        };
10772        assert!(!m2.matches(&empty_path), "an empty CSS path can never match");
10773    }
10774
10775    #[test]
10776    fn css_matcher_get_hash_is_deterministic_and_path_sensitive() {
10777        let a = CssMatcher {
10778            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10779            indices_in_parent: vec![0],
10780            children_length: vec![0],
10781        };
10782        let b = CssMatcher {
10783            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10784            indices_in_parent: vec![9],
10785            children_length: vec![9],
10786        };
10787        let c = CssMatcher {
10788            path: vec![CssPathSelector::Type(NodeTypeTag::Div)],
10789            indices_in_parent: vec![0],
10790            children_length: vec![0],
10791        };
10792        assert_eq!(a.get_hash(), a.get_hash(), "stable across calls");
10793        assert_eq!(
10794            a.get_hash(),
10795            b.get_hash(),
10796            "the hash covers only `path`, not the sibling indices"
10797        );
10798        assert_ne!(a.get_hash(), c.get_hash());
10799
10800        let empty = CssMatcher {
10801            path: Vec::new(),
10802            indices_in_parent: Vec::new(),
10803            children_length: Vec::new(),
10804        };
10805        let _ = empty.get_hash(); // must not panic
10806    }
10807
10808    #[test]
10809    fn css_matcher_mismatched_bookkeeping_vec_lengths_bail_out() {
10810        // `indices_in_parent` / `children_length` must be as long as the group list.
10811        let m = CssMatcher {
10812            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10813            indices_in_parent: Vec::new(),
10814            children_length: Vec::new(),
10815        };
10816        let path = CssPath {
10817            selectors: vec![CssPathSelector::Type(NodeTypeTag::Body)].into(),
10818        };
10819        assert!(
10820            !m.matches(&path),
10821            "a desynced matcher must return false, not index out of bounds"
10822        );
10823    }
10824
10825    #[test]
10826    fn get_css_blocks_and_inline_string_on_empty_css() {
10827        let m = CssMatcher {
10828            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10829            indices_in_parent: vec![0],
10830            children_length: vec![0],
10831        };
10832        assert!(get_css_blocks(&Css::empty(), &m).is_empty());
10833        assert_eq!(css_blocks_to_inline_string(&[]), "");
10834    }
10835
10836    // ================================================================
10837    // str_to_dom / str_to_dom_unstyled / parse_page_style_and_body / body_matcher
10838    // ================================================================
10839
10840    #[test]
10841    fn str_to_dom_rejects_documents_without_html_or_body() {
10842        let map = ComponentMap::with_builtin();
10843        assert_eq!(
10844            str_to_dom(&[], &map, None).unwrap_err(),
10845            DomXmlParseError::NoHtmlNode
10846        );
10847        let html_only = vec![elem(XmlNode::create("html"))];
10848        assert_eq!(
10849            str_to_dom(&html_only, &map, None).unwrap_err(),
10850            DomXmlParseError::NoBodyInHtml
10851        );
10852        assert!(str_to_dom_unstyled(&[], &map).is_err());
10853    }
10854
10855    #[test]
10856    fn str_to_dom_valid_minimal() {
10857        let map = ComponentMap::with_builtin();
10858        let d = doc("body { color: red; }", vec![elem(node("div", &[("id", "x")], vec![]))]);
10859        assert!(str_to_dom(&d, &map, None).is_ok());
10860        assert!(str_to_dom_unstyled(&d, &map).is_ok());
10861    }
10862
10863    #[test]
10864    fn str_to_dom_max_width_edge_values_do_not_panic() {
10865        let map = ComponentMap::with_builtin();
10866        let d = doc("", vec![elem(XmlNode::create("div"))]);
10867        for w in [
10868            Some(0.0f32),
10869            Some(-0.0),
10870            Some(-1.0),
10871            Some(f32::MAX),
10872            Some(f32::MIN),
10873            Some(f32::NAN),
10874            Some(f32::INFINITY),
10875            Some(f32::NEG_INFINITY),
10876            None,
10877        ] {
10878            assert!(
10879                str_to_dom(&d, &map, w).is_ok(),
10880                "max_width={w:?} is formatted straight into a CSS string and must not panic"
10881            );
10882        }
10883    }
10884
10885    #[test]
10886    fn str_to_dom_deeply_nested_body_is_depth_capped_not_stack_overflowing() {
10887        let map = ComponentMap::with_builtin();
10888        let deep = wrap_divs(2_000, node("div", &[("id", "bottom")], vec![]));
10889        let d = doc("", vec![elem(deep)]);
10890        assert!(
10891            str_to_dom(&d, &map, None).is_ok(),
10892            "children past MAX_XML_NESTING_DEPTH are dropped, not crashed on"
10893        );
10894    }
10895
10896    #[test]
10897    fn parse_page_style_and_body_and_body_matcher() {
10898        let d = doc("body { color: red; }", vec![elem(XmlNode::create("div"))]);
10899        let (css, body) = parse_page_style_and_body(&d).expect("well-formed page");
10900        assert_eq!(body.node_type.as_str(), "body");
10901        assert!(!css.rules.as_ref().is_empty(), "the <style> block is parsed");
10902
10903        let m = body_matcher(body);
10904        assert!(m.path.is_empty(), "the matcher starts with an empty path");
10905        assert_eq!(m.indices_in_parent, vec![0]);
10906        assert_eq!(m.children_length, vec![body.children.as_ref().len()]);
10907    }
10908
10909    #[test]
10910    fn parse_page_style_and_body_with_no_style_block() {
10911        let head = node("head", &[], vec![]);
10912        let body = node("body", &[], vec![]);
10913        let d = vec![elem(node("html", &[], vec![elem(head), elem(body)]))];
10914        let (css, body) = parse_page_style_and_body(&d).expect("ok");
10915        assert!(css.rules.as_ref().is_empty());
10916        assert_eq!(body.children.as_ref().len(), 0);
10917    }
10918
10919    // ================================================================
10920    // str_to_rust_code / str_to_c_code / str_to_cpp_code / str_to_python_code
10921    // ================================================================
10922
10923    #[test]
10924    fn str_to_rust_code_empty_input_is_an_error_not_a_panic() {
10925        let map = ComponentMap::with_builtin();
10926        assert!(matches!(
10927            str_to_rust_code(&[], "", &map),
10928            Err(CompileError::Xml(DomXmlParseError::NoHtmlNode))
10929        ));
10930        assert!(str_to_c_code(&[], &map).is_err());
10931        assert!(str_to_cpp_code(&[], &map).is_err());
10932        assert!(str_to_python_code(&[], &map).is_err());
10933    }
10934
10935    #[test]
10936    fn str_to_rust_code_whitespace_and_text_only_roots_are_errors() {
10937        let map = ComponentMap::with_builtin();
10938        for roots in [vec![txt("   ")], vec![txt("\t\n")], vec![txt("garbage")]] {
10939            assert!(
10940                str_to_rust_code(&roots, "", &map).is_err(),
10941                "a document with no <html> element must be rejected"
10942            );
10943        }
10944    }
10945
10946    #[test]
10947    fn str_to_rust_code_valid_minimal() {
10948        let map = ComponentMap::with_builtin();
10949        let d = doc("", vec![elem(node("p", &[], vec![txt("Hi")]))]);
10950        let src = str_to_rust_code(&d, "// imports", &map).expect("compiles");
10951        assert!(src.contains("Dom::create_body()"), "got:\n{src}");
10952        assert!(src.contains("Dom::create_p_with_text(AzString::from(\"Hi\"))"));
10953        assert!(src.contains("// imports"), "the imports blob is spliced in");
10954        assert!(src.contains("fn main()"));
10955    }
10956
10957    #[test]
10958    fn str_to_c_cpp_python_code_valid_minimal() {
10959        let map = ComponentMap::with_builtin();
10960        let d = doc("", vec![elem(node("p", &[], vec![txt("Hi")]))]);
10961
10962        let c = str_to_c_code(&d, &map).expect("compiles");
10963        assert!(c.contains("#include \"azul.h\""), "got:\n{c}");
10964        assert!(c.contains("AzDom n0 = AzDom_createBody();"));
10965        assert!(c.contains("AzDom_createPWithText(AZ_STR(\"Hi\"))"));
10966
10967        let cpp = str_to_cpp_code(&d, &map).expect("compiles");
10968        assert!(cpp.contains("#include \"azul20.hpp\""), "got:\n{cpp}");
10969        assert!(cpp.contains("Dom::create_p_with_text(String(\"Hi\"))"));
10970
10971        let py = str_to_python_code(&d, &map).expect("compiles");
10972        assert!(py.contains("import azul"), "got:\n{py}");
10973        assert!(py.contains("azul.Dom.create_p_with_text(\"Hi\")"));
10974    }
10975
10976    #[test]
10977    fn compile_targets_escape_quotes_in_text_content() {
10978        let map = ComponentMap::with_builtin();
10979        let d = doc("", vec![elem(node("div", &[], vec![txt("say \"hi\"")]))]);
10980
10981        let rust = str_to_rust_code(&d, "", &map).expect("compiles");
10982        assert!(rust.contains("say \\\"hi\\\""), "got:\n{rust}");
10983        let c = str_to_c_code(&d, &map).expect("compiles");
10984        assert!(c.contains("say \\\"hi\\\""), "got:\n{c}");
10985    }
10986
10987    #[test]
10988    fn compile_body_node_to_rust_code_on_an_empty_body() {
10989        let map = ComponentMap::with_builtin();
10990        let body = node("body", &[], vec![]);
10991        let mut extra = VecContents::default();
10992        let mut blocks = BTreeMap::new();
10993        let out = compile_body_node_to_rust_code(
10994            &body,
10995            &map,
10996            &mut extra,
10997            &mut blocks,
10998            &Css::empty(),
10999            body_matcher(&body),
11000        )
11001        .expect("ok");
11002        assert_eq!(out, "Dom::create_body()", "no children => no .with_children()");
11003    }
11004
11005    #[test]
11006    fn compile_body_node_to_rust_code_skips_whitespace_only_text_children() {
11007        let map = ComponentMap::with_builtin();
11008        let body = node("body", &[], vec![txt("   \n\t ")]);
11009        let mut extra = VecContents::default();
11010        let mut blocks = BTreeMap::new();
11011        let out = compile_body_node_to_rust_code(
11012            &body,
11013            &map,
11014            &mut extra,
11015            &mut blocks,
11016            &Css::empty(),
11017            body_matcher(&body),
11018        )
11019        .expect("ok");
11020        assert!(
11021            !out.contains("create_text"),
11022            "a whitespace-only text child emits nothing, got:\n{out}"
11023        );
11024    }
11025
11026    // ================================================================
11027    // builtin_render_fn / builtin_compile_fn  (numeric: indent)
11028    // ================================================================
11029
11030    #[test]
11031    fn builtin_render_fn_for_a_text_and_a_textless_element() {
11032        let map = ComponentMap::with_builtin();
11033        let div = map.get_unqualified("div").expect("builtin div");
11034        assert!(matches!(
11035            builtin_render_fn(div, &div.data_model, &map),
11036            ResultStyledDomRenderDomError::Ok(_)
11037        ));
11038
11039        let p = map.get_unqualified("p").expect("builtin p");
11040        assert!(matches!(
11041            builtin_render_fn(p, &p.data_model, &map),
11042            ResultStyledDomRenderDomError::Ok(_)
11043        ));
11044    }
11045
11046    #[test]
11047    fn builtin_compile_fn_ignores_indent_so_usize_max_is_safe() {
11048        let map = ComponentMap::with_builtin();
11049        let div = map.get_unqualified("div").expect("builtin div");
11050        for indent in [0usize, 1, 1024, usize::MAX] {
11051            match builtin_compile_fn(div, &CompileTarget::Rust, &div.data_model, indent) {
11052                ResultStringCompileError::Ok(s) => assert_eq!(
11053                    s.as_str(),
11054                    "Dom::create_node(NodeType::Div)",
11055                    "indent is unused by builtin_compile_fn (indent={indent})"
11056                ),
11057                ResultStringCompileError::Err(e) => panic!("unexpected error: {e:?}"),
11058            }
11059        }
11060    }
11061
11062    #[test]
11063    fn builtin_compile_fn_emits_text_and_escapes_it() {
11064        let map = ComponentMap::with_builtin();
11065        let p = map.get_unqualified("p").expect("builtin p");
11066        let data = p
11067            .data_model
11068            .clone()
11069            .with_default("text", ComponentDefaultValue::String(AzString::from("a\"b\\c")));
11070
11071        match builtin_compile_fn(p, &CompileTarget::Rust, &data, 0) {
11072            ResultStringCompileError::Ok(s) => {
11073                assert!(s.as_str().contains("a\\\"b\\\\c"), "got {}", s.as_str());
11074            }
11075            ResultStringCompileError::Err(e) => panic!("unexpected error: {e:?}"),
11076        }
11077    }
11078
11079    #[test]
11080    fn builtin_compile_fn_covers_every_target() {
11081        let map = ComponentMap::with_builtin();
11082        let div = map.get_unqualified("div").expect("builtin div");
11083        for target in [
11084            CompileTarget::Rust,
11085            CompileTarget::C,
11086            CompileTarget::Cpp,
11087            CompileTarget::Python,
11088        ] {
11089            match builtin_compile_fn(div, &target, &div.data_model, 0) {
11090                ResultStringCompileError::Ok(s) => {
11091                    assert!(!s.as_str().is_empty(), "{target:?} emitted nothing");
11092                }
11093                ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11094            }
11095        }
11096    }
11097
11098    // ================================================================
11099    // user_defined_render_fn / user_defined_compile_fn
11100    // ================================================================
11101
11102    fn every_default_kind() -> Vec<ComponentDataField> {
11103        use ComponentDefaultValue as D;
11104        vec![
11105            data_field("s", ComponentFieldType::String, Some(D::String(AzString::from("txt"))), ""),
11106            data_field("b", ComponentFieldType::Bool, Some(D::Bool(true)), ""),
11107            data_field("i32", ComponentFieldType::I32, Some(D::I32(i32::MIN)), ""),
11108            data_field("i64", ComponentFieldType::I64, Some(D::I64(i64::MIN)), ""),
11109            data_field("u32", ComponentFieldType::U32, Some(D::U32(u32::MAX)), ""),
11110            data_field("u64", ComponentFieldType::U64, Some(D::U64(u64::MAX)), ""),
11111            data_field("us", ComponentFieldType::Usize, Some(D::Usize(usize::MAX)), ""),
11112            data_field("f32", ComponentFieldType::F32, Some(D::F32(f32::NAN)), ""),
11113            data_field("f64", ComponentFieldType::F64, Some(D::F64(f64::INFINITY)), ""),
11114            data_field(
11115                "c",
11116                ComponentFieldType::ColorU,
11117                Some(D::ColorU(ColorU { r: 0, g: 0, b: 0, a: 0 })),
11118                "",
11119            ),
11120            data_field("cb", ComponentFieldType::StyledDom, Some(D::CallbackFnPointer(AzString::from("on_click"))), ""),
11121            data_field("j", ComponentFieldType::String, Some(D::Json(AzString::from("{}"))), ""),
11122            data_field("none", ComponentFieldType::String, Some(D::None), ""),
11123            data_field("missing", ComponentFieldType::String, None, ""),
11124        ]
11125    }
11126
11127    #[test]
11128    fn user_defined_render_fn_handles_every_default_value_kind() {
11129        let map = ComponentMap::with_builtin();
11130        let def = user_def("", every_default_kind());
11131        assert!(matches!(
11132            user_defined_render_fn(&def, &def.data_model, &map),
11133            ResultStyledDomRenderDomError::Ok(_)
11134        ));
11135    }
11136
11137    #[test]
11138    fn user_defined_render_fn_on_an_empty_model_and_with_css() {
11139        let map = ComponentMap::with_builtin();
11140        let empty = user_def("", Vec::new());
11141        assert!(matches!(
11142            user_defined_render_fn(&empty, &empty.data_model, &map),
11143            ResultStyledDomRenderDomError::Ok(_)
11144        ));
11145
11146        let styled = user_def(".widget { color: red; }", Vec::new());
11147        assert!(matches!(
11148            user_defined_render_fn(&styled, &styled.data_model, &map),
11149            ResultStyledDomRenderDomError::Ok(_)
11150        ));
11151    }
11152
11153    #[test]
11154    fn user_defined_render_fn_unknown_sub_component_renders_a_placeholder() {
11155        let map = ComponentMap::create(); // empty: no library can resolve the instance
11156        let def = user_def(
11157            "",
11158            vec![data_field(
11159                "child",
11160                ComponentFieldType::StyledDom,
11161                Some(ComponentDefaultValue::ComponentInstance(ComponentInstanceDefault {
11162                    library: AzString::from("nope"),
11163                    component: AzString::from("missing"),
11164                    field_overrides: Vec::new().into(),
11165                })),
11166                "",
11167            )],
11168        );
11169        assert!(
11170            matches!(
11171                user_defined_render_fn(&def, &def.data_model, &map),
11172                ResultStyledDomRenderDomError::Ok(_)
11173            ),
11174            "an unresolvable sub-component must render a placeholder, not error out"
11175        );
11176    }
11177
11178    #[test]
11179    fn user_defined_compile_fn_indent_zero_and_every_target() {
11180        let def = user_def("", every_default_kind());
11181        for target in [
11182            CompileTarget::Rust,
11183            CompileTarget::C,
11184            CompileTarget::Cpp,
11185            CompileTarget::Python,
11186        ] {
11187            match user_defined_compile_fn(&def, &target, &def.data_model, 0) {
11188                ResultStringCompileError::Ok(s) => {
11189                    assert!(!s.as_str().is_empty(), "{target:?} emitted nothing");
11190                }
11191                ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11192            }
11193        }
11194    }
11195
11196    #[test]
11197    fn user_defined_compile_fn_indent_scales_the_leading_whitespace() {
11198        // NOTE: `indent` is used as `" ".repeat(indent * 4)`, so it is NOT safe at
11199        // usize::MAX (the multiply overflows). Exercise the realistic range.
11200        let def = user_def("", Vec::new());
11201        let mut prev = 0usize;
11202        for indent in [0usize, 1, 2, 8] {
11203            match user_defined_compile_fn(&def, &CompileTarget::Rust, &def.data_model, indent) {
11204                ResultStringCompileError::Ok(s) => {
11205                    let len = s.as_str().len();
11206                    assert!(len > prev, "indent={indent} must widen the output");
11207                    prev = len;
11208                }
11209                ResultStringCompileError::Err(e) => panic!("indent={indent}: {e:?}"),
11210            }
11211        }
11212    }
11213
11214    #[test]
11215    fn user_defined_compile_fn_escapes_string_defaults() {
11216        let def = user_def(
11217            "",
11218            vec![data_field(
11219                "s",
11220                ComponentFieldType::String,
11221                Some(ComponentDefaultValue::String(AzString::from("a\"b\\c"))),
11222                "",
11223            )],
11224        );
11225        match user_defined_compile_fn(&def, &CompileTarget::Rust, &def.data_model, 0) {
11226            ResultStringCompileError::Ok(s) => {
11227                assert!(s.as_str().contains("a\\\"b\\\\c"), "got:\n{}", s.as_str());
11228            }
11229            ResultStringCompileError::Err(e) => panic!("{e:?}"),
11230        }
11231    }
11232
11233    #[test]
11234    fn push_scalar_field_appends_one_div_per_call() {
11235        let mut children: Vec<Dom> = Vec::new();
11236        push_scalar_field(&mut children, "n", &i64::MIN);
11237        push_scalar_field(&mut children, "", &f32::NAN);
11238        push_scalar_field(&mut children, "\u{1F600}", &usize::MAX);
11239        assert_eq!(children.len(), 3);
11240    }
11241
11242    // ================================================================
11243    // Structural builtins: if / for / map
11244    // ================================================================
11245
11246    #[test]
11247    fn builtin_if_for_map_component_defs_are_well_formed() {
11248        for (def, model, field) in [
11249            (builtin_if_component(), "IfData", "condition"),
11250            (builtin_for_component(), "ForData", "count"),
11251            (builtin_map_component(), "MapData", "data_json"),
11252        ] {
11253            assert_eq!(def.id.collection.as_str(), "builtin");
11254            assert_eq!(def.data_model.name.as_str(), model);
11255            assert!(
11256                def.data_model.get_field(field).is_some(),
11257                "{model} must expose `{field}`"
11258            );
11259        }
11260    }
11261
11262    #[test]
11263    fn builtin_if_render_fn_defaults_to_the_else_branch() {
11264        let map = ComponentMap::create();
11265        let def = builtin_if_component();
11266        // Missing / wrongly-typed condition => false, no panic.
11267        let empty = dm("IfData", Vec::new());
11268        assert!(matches!(
11269            builtin_if_render_fn(&def, &empty, &map),
11270            ResultStyledDomRenderDomError::Ok(_)
11271        ));
11272
11273        let truthy = def
11274            .data_model
11275            .clone()
11276            .with_default("condition", ComponentDefaultValue::Bool(true));
11277        assert!(matches!(
11278            builtin_if_render_fn(&def, &truthy, &map),
11279            ResultStyledDomRenderDomError::Ok(_)
11280        ));
11281    }
11282
11283    #[test]
11284    fn builtin_for_render_fn_handles_zero_and_a_wrongly_typed_count() {
11285        let map = ComponentMap::create();
11286        let def = builtin_for_component();
11287
11288        let zero = def
11289            .data_model
11290            .clone()
11291            .with_default("count", ComponentDefaultValue::U32(0));
11292        assert!(matches!(
11293            builtin_for_render_fn(&def, &zero, &map),
11294            ResultStyledDomRenderDomError::Ok(_)
11295        ));
11296
11297        // A non-U32 default falls back to the documented default of 3.
11298        let wrong_type = def
11299            .data_model
11300            .clone()
11301            .with_default("count", ComponentDefaultValue::String(AzString::from("9")));
11302        assert!(matches!(
11303            builtin_for_render_fn(&def, &wrong_type, &map),
11304            ResultStyledDomRenderDomError::Ok(_)
11305        ));
11306    }
11307
11308    #[test]
11309    fn builtin_map_render_fn_defaults_to_an_empty_json_array() {
11310        let map = ComponentMap::create();
11311        let def = builtin_map_component();
11312        assert!(matches!(
11313            builtin_map_render_fn(&def, &dm("MapData", Vec::new()), &map),
11314            ResultStyledDomRenderDomError::Ok(_)
11315        ));
11316        let garbage = def
11317            .data_model
11318            .clone()
11319            .with_default("data_json", ComponentDefaultValue::String(AzString::from("{{{")));
11320        assert!(
11321            matches!(
11322                builtin_map_render_fn(&def, &garbage, &map),
11323                ResultStyledDomRenderDomError::Ok(_)
11324            ),
11325            "malformed JSON must not panic — it is only echoed into a label"
11326        );
11327    }
11328
11329    #[test]
11330    fn structural_builtin_compile_fns_ignore_indent_entirely() {
11331        let cases: [(ComponentDef, ComponentCompileFn); 3] = [
11332            (builtin_if_component(), builtin_if_compile_fn),
11333            (builtin_for_component(), builtin_for_compile_fn),
11334            (builtin_map_component(), builtin_map_compile_fn),
11335        ];
11336        for (def, f) in cases {
11337            for target in [
11338                CompileTarget::Rust,
11339                CompileTarget::C,
11340                CompileTarget::Cpp,
11341                CompileTarget::Python,
11342            ] {
11343                for indent in [0usize, usize::MAX] {
11344                    match f(&def, &target, &def.data_model, indent) {
11345                        ResultStringCompileError::Ok(s) => {
11346                            assert!(!s.as_str().is_empty(), "{target:?}/{indent} emitted nothing");
11347                        }
11348                        ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11349                    }
11350                }
11351            }
11352        }
11353    }
11354
11355    // ================================================================
11356    // data_field / builtin_data_model / builtin_component_def
11357    // ================================================================
11358
11359    #[test]
11360    fn data_field_required_is_the_inverse_of_having_a_default() {
11361        let with = data_field(
11362            "x",
11363            ComponentFieldType::String,
11364            Some(ComponentDefaultValue::String(AzString::from("v"))),
11365            "d",
11366        );
11367        assert!(!with.required);
11368        assert_eq!(with.description.as_str(), "d");
11369
11370        let without = data_field("x", ComponentFieldType::String, None, "");
11371        assert!(without.required);
11372        assert!(matches!(
11373            without.default_value,
11374            OptionComponentDefaultValue::None
11375        ));
11376    }
11377
11378    #[test]
11379    fn builtin_data_model_unknown_tag_is_empty() {
11380        assert!(builtin_data_model("").is_empty());
11381        assert!(builtin_data_model("div").is_empty());
11382        assert!(builtin_data_model("\u{1F600}").is_empty());
11383        assert!(builtin_data_model(&"z".repeat(10_000)).is_empty());
11384    }
11385
11386    #[test]
11387    fn builtin_data_model_known_tags_expose_their_attributes() {
11388        let a = builtin_data_model("a");
11389        assert!(
11390            a.iter().any(|f| f.name.as_str() == "href"),
11391            "<a> must expose href"
11392        );
11393        // `src` on <img> is required (it has no default value).
11394        let img = builtin_data_model("img");
11395        let src = img
11396            .iter()
11397            .find(|f| f.name.as_str() == "src")
11398            .expect("img has src");
11399        assert!(src.required, "<img src> must be a required field");
11400        // `img` and `image` share the same model.
11401        assert_eq!(builtin_data_model("image").len(), img.len());
11402    }
11403
11404    #[test]
11405    fn builtin_component_def_default_text_controls_the_text_field() {
11406        let with_text = builtin_component_def("p", "Paragraph", Some("Hi"), "");
11407        assert_eq!(
11408            with_text.data_model.get_default_string("text").map(AzString::as_str),
11409            Some("Hi")
11410        );
11411        assert_eq!(with_text.data_model.name.as_str(), "ParagraphData");
11412        assert_eq!(with_text.id.qualified_name(), "builtin:p");
11413
11414        let no_text = builtin_component_def("div", "Div", None, "");
11415        assert!(
11416            no_text.data_model.get_field("text").is_none(),
11417            "a `None` default_text means the element has no text field at all"
11418        );
11419
11420        // An empty-string default still creates the field.
11421        let empty_text = builtin_component_def("span", "Span", Some(""), "");
11422        assert!(empty_text.data_model.get_field("text").is_some());
11423        assert_eq!(
11424            empty_text.data_model.get_default_string("text").map(AzString::as_str),
11425            Some("")
11426        );
11427    }
11428
11429    // ================================================================
11430    // xml_attrs_to_data_model
11431    // ================================================================
11432
11433    #[test]
11434    fn xml_attrs_to_data_model_overrides_defaults_from_attributes() {
11435        let base = builtin_component_def("a", "Link", Some("Link text"), "").data_model;
11436        let model = xml_attrs_to_data_model(&base, &attrs(&[("href", "/x")]), None);
11437        assert_eq!(
11438            model.get_default_string("href").map(AzString::as_str),
11439            Some("/x")
11440        );
11441        assert_eq!(
11442            model.get_default_string("text").map(AzString::as_str),
11443            Some("Link text"),
11444            "un-supplied fields keep their defaults"
11445        );
11446        assert_eq!(
11447            model.fields.as_ref().len(),
11448            base.fields.as_ref().len(),
11449            "no field is added or dropped"
11450        );
11451    }
11452
11453    #[test]
11454    fn xml_attrs_to_data_model_text_content_is_prepared_and_empty_text_is_ignored() {
11455        let base = builtin_component_def("a", "Link", Some("Link text"), "").data_model;
11456
11457        let with_text = xml_attrs_to_data_model(&base, &attrs(&[]), Some("  Hello &amp; bye  "));
11458        assert_eq!(
11459            with_text.get_default_string("text").map(AzString::as_str),
11460            Some("Hello & bye"),
11461            "text content is trimmed and entity-decoded"
11462        );
11463
11464        let blank = xml_attrs_to_data_model(&base, &attrs(&[]), Some("   \n\t "));
11465        assert_eq!(
11466            blank.get_default_string("text").map(AzString::as_str),
11467            Some("Link text"),
11468            "whitespace-only text content leaves the default intact"
11469        );
11470    }
11471
11472    #[test]
11473    fn xml_attrs_to_data_model_ignores_unknown_attributes() {
11474        let base = builtin_component_def("a", "Link", Some(""), "").data_model;
11475        let before = base.fields.as_ref().len();
11476        let model = xml_attrs_to_data_model(
11477            &base,
11478            &attrs(&[("data-nonsense", "1"), ("", ""), ("\u{1F600}", "x")]),
11479            None,
11480        );
11481        assert_eq!(
11482            model.fields.as_ref().len(),
11483            before,
11484            "unknown attributes must not create fields"
11485        );
11486    }
11487
11488    // ================================================================
11489    // DomXml
11490    // ================================================================
11491
11492    #[test]
11493    fn dom_xml_into_styled_dom_matches_the_from_impl() {
11494        let via_method: StyledDom = DomXml::default().into_styled_dom();
11495        let via_from: StyledDom = DomXml::default().into();
11496        assert_eq!(
11497            via_method, via_from,
11498            "into_styled_dom() must be exactly the From<DomXml> impl"
11499        );
11500    }
11501
11502    // ================================================================
11503    // Display impls  (serializer)
11504    // ================================================================
11505
11506    fn pos() -> XmlTextPos {
11507        XmlTextPos {
11508            row: u32::MAX,
11509            col: 0,
11510        }
11511    }
11512
11513    #[test]
11514    fn xml_text_pos_display_is_non_empty_for_edge_values() {
11515        assert_eq!(
11516            format!("{}", XmlTextPos { row: 0, col: 0 }),
11517            "line 0:0",
11518            "a zero position is still rendered"
11519        );
11520        assert_eq!(
11521            format!(
11522                "{}",
11523                XmlTextPos {
11524                    row: u32::MAX,
11525                    col: u32::MAX
11526                }
11527            ),
11528            "line 4294967295:4294967295"
11529        );
11530    }
11531
11532    #[test]
11533    fn xml_stream_error_display_covers_every_variant() {
11534        let variants = vec![
11535            XmlStreamError::UnexpectedEndOfStream,
11536            XmlStreamError::InvalidName,
11537            XmlStreamError::NonXmlChar(NonXmlCharError {
11538                ch: u32::MAX,
11539                pos: pos(),
11540            }),
11541            XmlStreamError::InvalidChar(InvalidCharError {
11542                expected: u8::MAX,
11543                got: 0,
11544                pos: pos(),
11545            }),
11546            XmlStreamError::InvalidCharMultiple(InvalidCharMultipleError {
11547                expected: 0,
11548                got: Vec::<u8>::new().into(),
11549                pos: pos(),
11550            }),
11551            XmlStreamError::InvalidQuote(InvalidQuoteError { got: 0, pos: pos() }),
11552            XmlStreamError::InvalidSpace(InvalidSpaceError { got: 0, pos: pos() }),
11553            XmlStreamError::InvalidString(InvalidStringError {
11554                got: AzString::from(""),
11555                pos: pos(),
11556            }),
11557            XmlStreamError::InvalidReference,
11558            XmlStreamError::InvalidExternalID,
11559            XmlStreamError::InvalidCommentData,
11560            XmlStreamError::InvalidCommentEnd,
11561            XmlStreamError::InvalidCharacterData,
11562        ];
11563        for v in &variants {
11564            let s = format!("{v}");
11565            assert!(!s.is_empty(), "{v:?} must render a non-empty message");
11566        }
11567        // `char::from_u32(u32::MAX)` is None — the formatter must not unwrap it.
11568        assert!(format!("{}", variants[2]).contains("None"));
11569    }
11570
11571    #[test]
11572    fn xml_parse_error_display_covers_every_variant() {
11573        let te = XmlTextError {
11574            stream_error: XmlStreamError::InvalidName,
11575            pos: pos(),
11576        };
11577        let variants = vec![
11578            XmlParseError::InvalidDeclaration(te.clone()),
11579            XmlParseError::InvalidComment(te.clone()),
11580            XmlParseError::InvalidPI(te.clone()),
11581            XmlParseError::InvalidDoctype(te.clone()),
11582            XmlParseError::InvalidEntity(te.clone()),
11583            XmlParseError::InvalidElement(te.clone()),
11584            XmlParseError::InvalidAttribute(te.clone()),
11585            XmlParseError::InvalidCdata(te.clone()),
11586            XmlParseError::InvalidCharData(te),
11587            XmlParseError::UnknownToken(pos()),
11588        ];
11589        for v in &variants {
11590            assert!(!format!("{v}").is_empty(), "{v:?} must render");
11591        }
11592    }
11593
11594    #[test]
11595    fn xml_error_display_covers_the_non_css_variants() {
11596        let variants = vec![
11597            XmlError::NoParserAvailable,
11598            XmlError::InvalidXmlPrefixUri(pos()),
11599            XmlError::UnexpectedXmlUri(pos()),
11600            XmlError::UnexpectedXmlnsUri(pos()),
11601            XmlError::InvalidElementNamePrefix(pos()),
11602            XmlError::DuplicatedNamespace(DuplicatedNamespaceError {
11603                ns: AzString::from(""),
11604                pos: pos(),
11605            }),
11606            XmlError::UnknownNamespace(UnknownNamespaceError {
11607                ns: AzString::from("\u{1F600}"),
11608                pos: pos(),
11609            }),
11610            XmlError::UnexpectedCloseTag(UnexpectedCloseTagError {
11611                expected: AzString::from("a"),
11612                actual: AzString::from("b"),
11613                pos: pos(),
11614            }),
11615            XmlError::UnexpectedEntityCloseTag(pos()),
11616            XmlError::UnknownEntityReference(UnknownEntityReferenceError {
11617                entity: AzString::from("x"),
11618                pos: pos(),
11619            }),
11620            XmlError::MalformedEntityReference(pos()),
11621            XmlError::EntityReferenceLoop(pos()),
11622            XmlError::InvalidAttributeValue(pos()),
11623            XmlError::DuplicatedAttribute(DuplicatedAttributeError {
11624                attribute: AzString::from("id"),
11625                pos: pos(),
11626            }),
11627            XmlError::NoRootNode,
11628            XmlError::SizeLimit,
11629            XmlError::DtdDetected,
11630            XmlError::MalformedHierarchy(MalformedHierarchyError {
11631                expected: AzString::from("app"),
11632                got: AzString::from("p"),
11633            }),
11634            XmlError::ParserError(XmlParseError::UnknownToken(pos())),
11635            XmlError::UnclosedRootNode,
11636            XmlError::UnexpectedDeclaration(pos()),
11637            XmlError::NodesLimitReached,
11638            XmlError::AttributesLimitReached,
11639            XmlError::NamespacesLimitReached,
11640            XmlError::InvalidName(pos()),
11641            XmlError::NonXmlChar(pos()),
11642            XmlError::InvalidChar(pos()),
11643            XmlError::InvalidChar2(pos()),
11644            XmlError::InvalidString(pos()),
11645            XmlError::InvalidExternalID(pos()),
11646            XmlError::InvalidComment(pos()),
11647            XmlError::InvalidCharacterData(pos()),
11648            XmlError::UnknownToken(pos()),
11649            XmlError::UnexpectedEndOfStream,
11650        ];
11651        for v in &variants {
11652            assert!(!format!("{v}").is_empty(), "{v:?} must render");
11653        }
11654    }
11655
11656    #[test]
11657    fn component_and_render_and_compile_error_display() {
11658        let unknown = ComponentError::UnknownComponent(AzString::from("\u{1F600}"));
11659        assert!(format!("{unknown}").contains("Unknown component"));
11660
11661        let useless = ComponentError::UselessFunctionArgument(UselessFunctionArgumentError {
11662            component_name: AzString::from("c"),
11663            argument_name: AzString::from("a"),
11664            valid_args: Vec::new().into(),
11665        });
11666        assert!(!format!("{useless}").is_empty());
11667
11668        let render: RenderDomError = unknown.clone().into();
11669        assert!(!format!("{render}").is_empty());
11670
11671        let compile: CompileError = render.clone().into();
11672        assert!(!format!("{compile}").is_empty());
11673
11674        let dom_xml: DomXmlParseError = render.into();
11675        assert!(!format!("{dom_xml}").is_empty());
11676        let compile2: CompileError = dom_xml.into();
11677        assert!(!format!("{compile2}").is_empty());
11678    }
11679
11680    #[test]
11681    fn dom_xml_parse_error_display_covers_the_non_css_variants() {
11682        let variants = vec![
11683            DomXmlParseError::NoHtmlNode,
11684            DomXmlParseError::MultipleHtmlRootNodes,
11685            DomXmlParseError::NoBodyInHtml,
11686            DomXmlParseError::MultipleBodyNodes,
11687            DomXmlParseError::Xml(XmlError::NoRootNode),
11688            DomXmlParseError::MalformedHierarchy(MalformedHierarchyError {
11689                expected: AzString::from("app"),
11690                got: AzString::from("p"),
11691            }),
11692            DomXmlParseError::RenderDom(RenderDomError::Component(
11693                ComponentError::UnknownComponent(AzString::from("x")),
11694            )),
11695            DomXmlParseError::Component(ComponentParseError::NotAComponent),
11696        ];
11697        for v in &variants {
11698            assert!(!format!("{v}").is_empty(), "{v:?} must render");
11699        }
11700    }
11701
11702    #[test]
11703    fn component_parse_error_display_covers_the_non_css_variants() {
11704        let variants = vec![
11705            ComponentParseError::NotAComponent,
11706            ComponentParseError::UnnamedComponent,
11707            ComponentParseError::MissingName(usize::MAX),
11708            ComponentParseError::MissingType(MissingTypeError {
11709                arg_pos: 0,
11710                arg_name: AzString::from(""),
11711            }),
11712            ComponentParseError::WhiteSpaceInComponentName(WhiteSpaceInComponentNameError {
11713                arg_pos: usize::MAX,
11714                arg_name: AzString::from("a b"),
11715            }),
11716            ComponentParseError::WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError {
11717                arg_pos: 0,
11718                arg_name: AzString::from("a"),
11719                arg_type: AzString::from("b c"),
11720            }),
11721        ];
11722        for v in &variants {
11723            assert!(!format!("{v}").is_empty(), "{v:?} must render");
11724        }
11725    }
11726
11727    // ================================================================
11728    // serde-json gated: ComponentDataModel::to_json / from_json
11729    // ================================================================
11730
11731    #[cfg(feature = "serde-json")]
11732    #[test]
11733    fn data_model_to_json_round_trips() {
11734        let m = model_with_text();
11735        let json = m.to_json().expect("serializes");
11736        let back = ComponentDataModel::from_json(&json).expect("deserializes");
11737        assert_eq!(back.name.as_str(), m.name.as_str());
11738        assert_eq!(back.fields.as_ref().len(), m.fields.as_ref().len());
11739        assert_eq!(back.get_default_string("text").map(AzString::as_str), Some("hi"));
11740    }
11741
11742    #[cfg(feature = "serde-json")]
11743    #[test]
11744    fn data_model_from_json_rejects_garbage_without_panicking() {
11745        for s in [
11746            "",
11747            "   ",
11748            "\t\n",
11749            "not json",
11750            "{",
11751            "[]",
11752            "null",
11753            "0",
11754            "-0",
11755            "9223372036854775807",
11756            "NaN",
11757            "\u{1F600}",
11758        ] {
11759            assert!(
11760                ComponentDataModel::from_json(s).is_err(),
11761                "{s:?} is not a data model"
11762            );
11763        }
11764    }
11765
11766    #[cfg(feature = "serde-json")]
11767    #[test]
11768    fn data_model_from_json_deeply_nested_input_does_not_stack_overflow() {
11769        let bomb = format!("{}{}", "[".repeat(10_000), "]".repeat(10_000));
11770        assert!(
11771            ComponentDataModel::from_json(&bomb).is_err(),
11772            "serde_json must reject the nesting bomb, not crash"
11773        );
11774    }
11775
11776    #[cfg(feature = "serde-json")]
11777    #[test]
11778    fn data_model_to_json_on_an_empty_model() {
11779        let m = dm("Empty", Vec::new());
11780        let json = m.to_json().expect("serializes");
11781        assert!(json.contains("\"fields\""), "got {json}");
11782        assert!(ComponentDataModel::from_json(&json).is_ok());
11783    }
11784}