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    codegen::format::VecContents,
31    css::{
32        Css, CssDeclaration, CssPath, CssPathPseudoSelector, CssPathSelector, CssRuleBlock,
33        NodeTypeTag,
34    },
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]
244    pub fn new(s: &str) -> Self {
245        Self {
246            inner: AzString::from(s),
247        }
248    }
249
250    #[must_use]
251    pub fn from_extension(ext: &str) -> Self {
252        let mime = match ext.to_lowercase().as_str() {
253            // Images
254            "png" => "image/png",
255            "jpg" | "jpeg" => "image/jpeg",
256            "gif" => "image/gif",
257            "webp" => "image/webp",
258            "svg" => "image/svg+xml",
259            "ico" => "image/x-icon",
260            "bmp" => "image/bmp",
261            "avif" => "image/avif",
262            // Fonts
263            "ttf" => "font/ttf",
264            "otf" => "font/otf",
265            "woff" => "font/woff",
266            "woff2" => "font/woff2",
267            "eot" => "application/vnd.ms-fontobject",
268            // Stylesheets
269            "css" => "text/css",
270            // Scripts
271            "js" | "mjs" => "application/javascript",
272            // Video
273            "mp4" => "video/mp4",
274            "webm" => "video/webm",
275            "ogg" => "video/ogg",
276            // Audio
277            "mp3" => "audio/mpeg",
278            "wav" => "audio/wav",
279            "flac" => "audio/flac",
280            // Default
281            _ => "application/octet-stream",
282        };
283        Self {
284            inner: AzString::from(mime),
285        }
286    }
287}
288
289impl_option!(
290    MimeTypeHint,
291    OptionMimeTypeHint,
292    copy = false,
293    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
294);
295
296/// An external resource URL found in an XML/HTML document
297#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
298#[repr(C)]
299pub struct ExternalResource {
300    /// The URL as found in the document (may be relative or absolute)
301    pub url: AzString,
302    /// Classification of the resource type
303    pub kind: ExternalResourceKind,
304    /// MIME type hint (from type attribute, file extension, or heuristics)
305    pub mime_type: OptionMimeTypeHint,
306    /// The HTML element that referenced this resource (e.g., "img", "link", "script")
307    pub source_element: AzString,
308    /// The attribute that contained the URL (e.g., "src", "href")
309    pub source_attribute: AzString,
310}
311
312impl_option!(
313    ExternalResource,
314    OptionExternalResource,
315    copy = false,
316    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
317);
318
319impl_vec!(
320    ExternalResource,
321    ExternalResourceVec,
322    ExternalResourceVecDestructor,
323    ExternalResourceVecDestructorType,
324    ExternalResourceVecSlice,
325    OptionExternalResource
326);
327impl_vec_mut!(ExternalResource, ExternalResourceVec);
328impl_vec_debug!(ExternalResource, ExternalResourceVec);
329impl_vec_partialeq!(ExternalResource, ExternalResourceVec);
330impl_vec_eq!(ExternalResource, ExternalResourceVec);
331impl_vec_partialord!(ExternalResource, ExternalResourceVec);
332impl_vec_ord!(ExternalResource, ExternalResourceVec);
333impl_vec_hash!(ExternalResource, ExternalResourceVec);
334impl_vec_clone!(
335    ExternalResource,
336    ExternalResourceVec,
337    ExternalResourceVecDestructor
338);
339
340/// AUDIT 2026-07-08: maximum XML/HTML nesting depth handled by the recursive
341/// DOM-build (`xml_node_to_dom_fast`, `xml_node_to_fast_dom`), resource-scan
342/// (iterative worklist in `scan_external_resources`) and `<body>`-lookup
343/// (`find_body_recursive`) passes. These bound descent per nesting level, so a pathologically deep
344/// document (e.g. tens of thousands of nested `<div>`s) would overflow the native
345/// stack. Beyond this depth, deeper children are ignored rather than crashing.
346/// 512 is far past any realistic hand-authored markup while staying comfortably
347/// inside the default thread stack.
348const MAX_XML_NESTING_DEPTH: usize = 512;
349
350/// AUDIT 2026-07-08: maximum recursion depth for [`ComponentFieldType::parse`],
351/// which recurses through `Option<..>` / `Vec<..>` wrappers. Caps attacker
352/// strings such as `"Option<".repeat(100_000)` that would otherwise overflow the
353/// stack. 64 nested type wrappers is far beyond any real field type.
354const MAX_TYPE_PARSE_DEPTH: usize = 64;
355
356#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
357#[repr(C)]
358pub struct Xml {
359    pub root: XmlNodeChildVec,
360}
361
362impl Xml {
363    /// Scan the XML/HTML document for external resource URLs.
364    ///
365    /// This function traverses the entire document tree and extracts URLs from:
366    /// - `<img src="...">` - Images
367    /// - `<link href="...">` - Stylesheets, icons, fonts
368    /// - `<script src="...">` - Scripts
369    /// - `<video src="...">`, `<source src="...">` - Video
370    /// - `<audio src="...">` - Audio
371    /// - `<a href="...">` - Links (classified as Unknown)
372    /// - CSS `url()` in style attributes
373    /// - `<style>` blocks with @import or `url()`
374    #[must_use]
375    pub fn scan_external_resources(&self) -> ExternalResourceVec {
376        let mut resources = Vec::new();
377
378        // AUDIT 2026-07-08: iterative DFS with an explicit worklist. The old
379        // per-node recursion overflowed the stack on pathologically deep markup
380        // (a single-purpose scan frame is large: string lowercasing + closure +
381        // wide match). An explicit stack keeps memory on the heap; `depth` still
382        // bounds how deep we descend so unbounded input can't grow the worklist
383        // without limit.
384        let mut stack: Vec<(&XmlNodeChild, usize)> = Vec::new();
385        for child in self.root.as_ref() {
386            stack.push((child, 0));
387        }
388        while let Some((child, depth)) = stack.pop() {
389            match child {
390                XmlNodeChild::Text(text) => {
391                    // CSS @import / url() in text content (inside <style> tags).
392                    Self::extract_css_urls(text.as_str(), &mut resources);
393                }
394                XmlNodeChild::Element(node) => {
395                    if depth > MAX_XML_NESTING_DEPTH {
396                        // Deeper subtrees are simply not scanned.
397                        continue;
398                    }
399                    Self::scan_node(node, &mut resources);
400                    for c in node.children.as_ref() {
401                        stack.push((c, depth + 1));
402                    }
403                }
404            }
405        }
406
407        resources.into()
408    }
409
410    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
411    fn scan_node(node: &XmlNode, resources: &mut Vec<ExternalResource>) {
412        let tag_name = node.node_type.inner.as_str().to_lowercase();
413
414        // Get attribute lookup helper
415        let get_attr = |name: &str| -> Option<String> {
416            node.attributes
417                .inner
418                .as_ref()
419                .iter()
420                .find(|pair| pair.key.as_str().eq_ignore_ascii_case(name))
421                .map(|pair| pair.value.as_str().to_string())
422        };
423
424        match tag_name.as_str() {
425            "img" => {
426                if let Some(src) = get_attr("src") {
427                    let mime = Self::guess_mime_from_url(&src, "image");
428                    resources.push(ExternalResource {
429                        url: AzString::from(src),
430                        kind: ExternalResourceKind::Image,
431                        mime_type: mime.into(),
432                        source_element: AzString::from("img"),
433                        source_attribute: AzString::from("src"),
434                    });
435                }
436                // Also check srcset
437                if let Some(srcset) = get_attr("srcset") {
438                    for src in Self::parse_srcset(&srcset) {
439                        let mime = Self::guess_mime_from_url(&src, "image");
440                        resources.push(ExternalResource {
441                            url: AzString::from(src),
442                            kind: ExternalResourceKind::Image,
443                            mime_type: mime.into(),
444                            source_element: AzString::from("img"),
445                            source_attribute: AzString::from("srcset"),
446                        });
447                    }
448                }
449            }
450            "link" => {
451                if let Some(href) = get_attr("href") {
452                    let rel = get_attr("rel").unwrap_or_default().to_lowercase();
453                    let type_attr = get_attr("type");
454                    let as_attr = get_attr("as").unwrap_or_default().to_lowercase();
455
456                    let (kind, category) = if rel.contains("stylesheet") {
457                        (ExternalResourceKind::Stylesheet, "stylesheet")
458                    } else if rel.contains("icon") || rel.contains("apple-touch-icon") {
459                        (ExternalResourceKind::Icon, "image")
460                    } else if as_attr == "font" {
461                        (ExternalResourceKind::Font, "font")
462                    } else if as_attr == "script" {
463                        (ExternalResourceKind::Script, "script")
464                    } else if as_attr == "image" {
465                        (ExternalResourceKind::Image, "image")
466                    } else {
467                        (ExternalResourceKind::Unknown, "")
468                    };
469
470                    let mime = type_attr
471                        .map(|t| MimeTypeHint::new(&t))
472                        .or_else(|| Self::guess_mime_from_url(&href, category));
473
474                    resources.push(ExternalResource {
475                        url: AzString::from(href),
476                        kind,
477                        mime_type: mime.into(),
478                        source_element: AzString::from("link"),
479                        source_attribute: AzString::from("href"),
480                    });
481                }
482            }
483            "script" => {
484                if let Some(src) = get_attr("src") {
485                    let type_attr = get_attr("type");
486                    let mime = type_attr
487                        .map(|t| MimeTypeHint::new(&t))
488                        .or_else(|| Some(MimeTypeHint::new("application/javascript")));
489
490                    resources.push(ExternalResource {
491                        url: AzString::from(src),
492                        kind: ExternalResourceKind::Script,
493                        mime_type: mime.into(),
494                        source_element: AzString::from("script"),
495                        source_attribute: AzString::from("src"),
496                    });
497                }
498            }
499            "video" => {
500                if let Some(src) = get_attr("src") {
501                    let mime = Self::guess_mime_from_url(&src, "video");
502                    resources.push(ExternalResource {
503                        url: AzString::from(src),
504                        kind: ExternalResourceKind::Video,
505                        mime_type: mime.into(),
506                        source_element: AzString::from("video"),
507                        source_attribute: AzString::from("src"),
508                    });
509                }
510                if let Some(poster) = get_attr("poster") {
511                    let mime = Self::guess_mime_from_url(&poster, "image");
512                    resources.push(ExternalResource {
513                        url: AzString::from(poster),
514                        kind: ExternalResourceKind::Image,
515                        mime_type: mime.into(),
516                        source_element: AzString::from("video"),
517                        source_attribute: AzString::from("poster"),
518                    });
519                }
520            }
521            "audio" => {
522                if let Some(src) = get_attr("src") {
523                    let mime = Self::guess_mime_from_url(&src, "audio");
524                    resources.push(ExternalResource {
525                        url: AzString::from(src),
526                        kind: ExternalResourceKind::Audio,
527                        mime_type: mime.into(),
528                        source_element: AzString::from("audio"),
529                        source_attribute: AzString::from("src"),
530                    });
531                }
532            }
533            "source" => {
534                if let Some(src) = get_attr("src") {
535                    let type_attr = get_attr("type");
536                    // Determine kind based on type or parent (heuristic: assume video)
537                    let kind = if type_attr.as_ref().is_some_and(|t| t.starts_with("audio")) {
538                        ExternalResourceKind::Audio
539                    } else {
540                        ExternalResourceKind::Video
541                    };
542                    let mime = type_attr.map(|t| MimeTypeHint::new(&t)).or_else(|| {
543                        Self::guess_mime_from_url(
544                            &src,
545                            if kind == ExternalResourceKind::Audio {
546                                "audio"
547                            } else {
548                                "video"
549                            },
550                        )
551                    });
552
553                    resources.push(ExternalResource {
554                        url: AzString::from(src),
555                        kind,
556                        mime_type: mime.into(),
557                        source_element: AzString::from("source"),
558                        source_attribute: AzString::from("src"),
559                    });
560                }
561                // Also handle srcset for picture elements
562                if let Some(srcset) = get_attr("srcset") {
563                    for src in Self::parse_srcset(&srcset) {
564                        let mime = Self::guess_mime_from_url(&src, "image");
565                        resources.push(ExternalResource {
566                            url: AzString::from(src),
567                            kind: ExternalResourceKind::Image,
568                            mime_type: mime.into(),
569                            source_element: AzString::from("source"),
570                            source_attribute: AzString::from("srcset"),
571                        });
572                    }
573                }
574            }
575            "a" => {
576                if let Some(href) = get_attr("href") {
577                    // Only include if it looks like a resource, not a page link
578                    if Self::looks_like_resource(&href) {
579                        let mime = Self::guess_mime_from_url(&href, "");
580                        resources.push(ExternalResource {
581                            url: AzString::from(href),
582                            kind: ExternalResourceKind::Unknown,
583                            mime_type: mime.into(),
584                            source_element: AzString::from("a"),
585                            source_attribute: AzString::from("href"),
586                        });
587                    }
588                }
589            }
590            "virtualized-view" | "embed" | "object" => {
591                let src_attr = if tag_name == "object" { "data" } else { "src" };
592                if let Some(src) = get_attr(src_attr) {
593                    resources.push(ExternalResource {
594                        url: AzString::from(src),
595                        kind: ExternalResourceKind::Unknown,
596                        mime_type: OptionMimeTypeHint::None,
597                        source_element: AzString::from(tag_name.clone()),
598                        source_attribute: AzString::from(src_attr),
599                    });
600                }
601            }
602            "style" => {
603                // Scan text content for CSS URLs
604                for child in node.children.as_ref() {
605                    if let XmlNodeChild::Text(text) = child {
606                        Self::extract_css_urls(text.as_str(), resources);
607                    }
608                }
609            }
610            _ => {}
611        }
612
613        // Check inline style attribute for url()
614        if let Some(style) = get_attr("style") {
615            Self::extract_css_urls(&style, resources);
616        }
617
618        // Check for background attribute (deprecated but still used)
619        if let Some(bg) = get_attr("background") {
620            let mime = Self::guess_mime_from_url(&bg, "image");
621            resources.push(ExternalResource {
622                url: AzString::from(bg),
623                kind: ExternalResourceKind::Image,
624                mime_type: mime.into(),
625                source_element: AzString::from(tag_name),
626                source_attribute: AzString::from("background"),
627            });
628        }
629
630        // Children are walked by the iterative driver in `scan_external_resources`.
631    }
632
633    /// Extract URLs from CSS content (handles `url()` and @import)
634    fn extract_css_urls(css: &str, resources: &mut Vec<ExternalResource>) {
635        // AUDIT 2026-07-08: fold to lowercase ONCE using ASCII-only folding.
636        // `to_ascii_lowercase` never changes a string's byte length (only A-Z are
637        // touched, multi-byte code points are left verbatim), so every byte offset
638        // into `lower` maps 1:1 onto `css`. The old code called `to_lowercase()`
639        // every iteration (O(n^2)) and then sliced the ORIGINAL `css` with an
640        // offset computed in the lowercased temporary -- for characters whose
641        // lowercase changes byte length (e.g. 'İ' U+0130, 2 bytes -> 3 bytes) that
642        // offset landed off a char boundary and panicked. Searching in `lower` and
643        // slicing `css` at the same offset also makes the `url(` / `@import` scans
644        // case-insensitive for free.
645        let lower = css.to_ascii_lowercase();
646
647        // url(...) scan (case-insensitive)
648        let mut search_from = 0;
649        while let Some(rel) = lower[search_from..].find("url(") {
650            let url_start = search_from + rel;
651            let after = url_start + 4;
652            // Skip a `url(` that is the argument of an `@import` — the @import scan
653            // below emits it, correctly tagged as a Stylesheet. Without this guard the
654            // same URL is pushed twice (once here, mistagged "url()").
655            if lower[..url_start].trim_end().ends_with("@import") {
656                search_from = after;
657                continue;
658            }
659            let after_url = &css[after..];
660            if let Some(url) = Self::extract_url_value(after_url) {
661                let mime = Self::guess_mime_from_url(&url, "");
662                let kind = Self::guess_kind_from_url(&url);
663                resources.push(ExternalResource {
664                    url: AzString::from(url),
665                    kind,
666                    mime_type: mime.into(),
667                    source_element: AzString::from("style"),
668                    source_attribute: AzString::from("url()"),
669                });
670            }
671            search_from = after;
672        }
673
674        // Handle @import "url" or @import url(...) (case-insensitive)
675        let mut search_from = 0;
676        while let Some(rel) = lower[search_from..].find("@import") {
677            let after = search_from + rel + 7;
678            let after_import = &css[after..];
679            let trimmed = after_import.trim_start();
680
681            // Match `url(` case-insensitively without allocating. `get(..4)`
682            // returns `None` if byte 4 is not a char boundary, so the slice below
683            // can never panic on multi-byte input.
684            let import_url = if trimmed
685                .get(..4)
686                .is_some_and(|p| p.eq_ignore_ascii_case("url("))
687            {
688                Self::extract_url_value(&trimmed[4..])
689            } else {
690                Self::extract_quoted_string(trimmed)
691            };
692
693            if let Some(url) = import_url {
694                resources.push(ExternalResource {
695                    url: AzString::from(url),
696                    kind: ExternalResourceKind::Stylesheet,
697                    mime_type: Some(MimeTypeHint::new("text/css")).into(),
698                    source_element: AzString::from("style"),
699                    source_attribute: AzString::from("@import"),
700                });
701            }
702
703            search_from = after;
704        }
705    }
706
707    /// Extract value from url(...) - handles quoted and unquoted URLs
708    fn extract_url_value(s: &str) -> Option<String> {
709        let trimmed = s.trim_start();
710        if trimmed.starts_with('"') {
711            Self::extract_quoted_string(trimmed)
712        } else if let Some(rest) = trimmed.strip_prefix('\'') {
713            let end = rest.find('\'')?;
714            Some(rest[..end].to_string())
715        } else {
716            let end = trimmed.find(')')?;
717            Some(trimmed[..end].trim().to_string())
718        }
719    }
720
721    /// Extract a quoted string value
722    fn extract_quoted_string(s: &str) -> Option<String> {
723        if let Some(rest) = s.strip_prefix('"') {
724            let end = rest.find('"')?;
725            Some(rest[..end].to_string())
726        } else if let Some(rest) = s.strip_prefix('\'') {
727            let end = rest.find('\'')?;
728            Some(rest[..end].to_string())
729        } else {
730            None
731        }
732    }
733
734    /// Parse srcset attribute into individual URLs
735    fn parse_srcset(srcset: &str) -> Vec<String> {
736        srcset
737            .split(',')
738            .filter_map(|entry| {
739                let trimmed = entry.trim();
740                // srcset format: "url 1x" or "url 100w"
741                trimmed
742                    .split_whitespace()
743                    .next()
744                    .map(alloc::string::ToString::to_string)
745            })
746            .filter(|url| !url.is_empty())
747            .collect()
748    }
749
750    /// Check if a URL looks like a downloadable resource (not a page)
751    fn looks_like_resource(url: &str) -> bool {
752        let lower = url.to_lowercase();
753        // Check for common resource extensions
754        let resource_exts = [
755            ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp", ".ttf", ".otf",
756            ".woff", ".woff2", ".eot", ".css", ".js", ".mp4", ".webm", ".ogg", ".mp3", ".wav",
757            ".pdf", ".zip", ".tar", ".gz",
758        ];
759        resource_exts.iter().any(|ext| lower.ends_with(ext))
760    }
761
762    /// Guess the resource kind from URL based on file extension.
763    // `url` is lowercased into `path` below, so these literal `.ext` checks are
764    // already case-insensitive — the lint can't see the runtime lowercasing.
765    #[allow(clippy::case_sensitive_file_extension_comparisons)]
766    fn guess_kind_from_url(url: &str) -> ExternalResourceKind {
767        let lower = url.to_lowercase();
768        // Strip query string before checking extension
769        let path = lower.split('?').next().unwrap_or(&lower);
770        if path.ends_with(".png")
771            || path.ends_with(".jpg")
772            || path.ends_with(".jpeg")
773            || path.ends_with(".gif")
774            || path.ends_with(".webp")
775            || path.ends_with(".svg")
776            || path.ends_with(".bmp")
777            || path.ends_with(".avif")
778        {
779            ExternalResourceKind::Image
780        } else if path.ends_with(".ttf")
781            || path.ends_with(".otf")
782            || path.ends_with(".woff")
783            || path.ends_with(".woff2")
784            || path.ends_with(".eot")
785        {
786            ExternalResourceKind::Font
787        } else if path.ends_with(".css") {
788            ExternalResourceKind::Stylesheet
789        } else if path.ends_with(".js") || path.ends_with(".mjs") {
790            ExternalResourceKind::Script
791        } else if path.ends_with(".mp4") || path.ends_with(".webm") || path.ends_with(".ogg") {
792            ExternalResourceKind::Video
793        } else if path.ends_with(".mp3") || path.ends_with(".wav") || path.ends_with(".flac") {
794            ExternalResourceKind::Audio
795        } else if path.ends_with(".ico") {
796            ExternalResourceKind::Icon
797        } else {
798            ExternalResourceKind::Unknown
799        }
800    }
801
802    /// Guess MIME type from URL based on extension
803    fn guess_mime_from_url(url: &str, category: &str) -> Option<MimeTypeHint> {
804        let lower = url.to_lowercase();
805        // Find extension
806        let ext = lower.rsplit('.').next()?;
807        // Remove query string if present
808        let ext = ext.split('?').next()?;
809
810        // Check if it's a valid extension
811        let valid_exts = [
812            "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "avif", "ttf", "otf", "woff",
813            "woff2", "eot", "css", "js", "mjs", "mp4", "webm", "ogg", "mp3", "wav", "flac",
814        ];
815
816        if valid_exts.contains(&ext) {
817            Some(MimeTypeHint::from_extension(ext))
818        } else if !category.is_empty() {
819            // Use category hint for default
820            match category {
821                "image" => Some(MimeTypeHint::new("image/*")),
822                "font" => Some(MimeTypeHint::new("font/*")),
823                "stylesheet" => Some(MimeTypeHint::new("text/css")),
824                "script" => Some(MimeTypeHint::new("application/javascript")),
825                "video" => Some(MimeTypeHint::new("video/*")),
826                "audio" => Some(MimeTypeHint::new("audio/*")),
827                _ => None,
828            }
829        } else {
830            None
831        }
832    }
833}
834
835#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
836#[repr(C)]
837pub struct NonXmlCharError {
838    pub ch: u32, /* u32 = char, but ABI stable */
839    pub pos: XmlTextPos,
840}
841
842#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
843#[repr(C)]
844pub struct InvalidCharError {
845    pub expected: u8,
846    pub got: u8,
847    pub pos: XmlTextPos,
848}
849
850#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
851#[repr(C)]
852pub struct InvalidCharMultipleError {
853    pub expected: u8,
854    pub got: U8Vec,
855    pub pos: XmlTextPos,
856}
857
858#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
859#[repr(C)]
860pub struct InvalidQuoteError {
861    pub got: u8,
862    pub pos: XmlTextPos,
863}
864
865#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
866#[repr(C)]
867pub struct InvalidSpaceError {
868    pub got: u8,
869    pub pos: XmlTextPos,
870}
871
872#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
873#[repr(C)]
874pub struct InvalidStringError {
875    pub got: AzString,
876    pub pos: XmlTextPos,
877}
878
879#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
880#[repr(C, u8)]
881pub enum XmlStreamError {
882    UnexpectedEndOfStream,
883    InvalidName,
884    NonXmlChar(NonXmlCharError),
885    InvalidChar(InvalidCharError),
886    InvalidCharMultiple(InvalidCharMultipleError),
887    InvalidQuote(InvalidQuoteError),
888    InvalidSpace(InvalidSpaceError),
889    InvalidString(InvalidStringError),
890    InvalidReference,
891    InvalidExternalID,
892    InvalidCommentData,
893    InvalidCommentEnd,
894    InvalidCharacterData,
895}
896
897impl fmt::Display for XmlStreamError {
898    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
899        use self::XmlStreamError::{
900            InvalidChar, InvalidCharMultiple, InvalidCharacterData, InvalidCommentData,
901            InvalidCommentEnd, InvalidExternalID, InvalidName, InvalidQuote, InvalidReference,
902            InvalidSpace, InvalidString, NonXmlChar, UnexpectedEndOfStream,
903        };
904        match self {
905            UnexpectedEndOfStream => write!(f, "Unexpected end of stream"),
906            InvalidName => write!(f, "Invalid name"),
907            NonXmlChar(nx) => write!(
908                f,
909                "Non-XML character: {:?} at {}",
910                core::char::from_u32(nx.ch),
911                nx.pos
912            ),
913            InvalidChar(ic) => write!(
914                f,
915                "Invalid character: expected: {}, got: {} at {}",
916                ic.expected as char, ic.got as char, ic.pos
917            ),
918            InvalidCharMultiple(imc) => write!(
919                f,
920                "Multiple invalid characters: expected: {}, got: {:?} at {}",
921                imc.expected,
922                imc.got.as_ref(),
923                imc.pos
924            ),
925            InvalidQuote(iq) => write!(f, "Invalid quote: got {} at {}", iq.got as char, iq.pos),
926            InvalidSpace(is) => write!(f, "Invalid space: got {} at {}", is.got as char, is.pos),
927            InvalidString(ise) => write!(
928                f,
929                "Invalid string: got \"{}\" at {}",
930                ise.got.as_str(),
931                ise.pos
932            ),
933            InvalidReference => write!(f, "Invalid reference"),
934            InvalidExternalID => write!(f, "Invalid external ID"),
935            InvalidCommentData => write!(f, "Invalid comment data"),
936            InvalidCommentEnd => write!(f, "Invalid comment end"),
937            InvalidCharacterData => write!(f, "Invalid character data"),
938        }
939    }
940}
941
942#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Ord, Hash, Eq)]
943#[repr(C)]
944pub struct XmlTextPos {
945    pub row: u32,
946    pub col: u32,
947}
948
949impl fmt::Display for XmlTextPos {
950    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
951        write!(f, "line {}:{}", self.row, self.col)
952    }
953}
954
955#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
956#[repr(C)]
957pub struct XmlTextError {
958    pub stream_error: XmlStreamError,
959    pub pos: XmlTextPos,
960}
961
962#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
963#[repr(C, u8)]
964pub enum XmlParseError {
965    InvalidDeclaration(XmlTextError),
966    InvalidComment(XmlTextError),
967    InvalidPI(XmlTextError),
968    InvalidDoctype(XmlTextError),
969    InvalidEntity(XmlTextError),
970    InvalidElement(XmlTextError),
971    InvalidAttribute(XmlTextError),
972    InvalidCdata(XmlTextError),
973    InvalidCharData(XmlTextError),
974    UnknownToken(XmlTextPos),
975}
976
977impl fmt::Display for XmlParseError {
978    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
979        use self::XmlParseError::{
980            InvalidAttribute, InvalidCdata, InvalidCharData, InvalidComment, InvalidDeclaration,
981            InvalidDoctype, InvalidElement, InvalidEntity, InvalidPI, UnknownToken,
982        };
983        match self {
984            InvalidDeclaration(e) => {
985                write!(f, "Invalid declaration: {} at {}", e.stream_error, e.pos)
986            }
987            InvalidComment(e) => write!(f, "Invalid comment: {} at {}", e.stream_error, e.pos),
988            InvalidPI(e) => write!(
989                f,
990                "Invalid processing instruction: {} at {}",
991                e.stream_error, e.pos
992            ),
993            InvalidDoctype(e) => write!(f, "Invalid doctype: {} at {}", e.stream_error, e.pos),
994            InvalidEntity(e) => write!(f, "Invalid entity: {} at {}", e.stream_error, e.pos),
995            InvalidElement(e) => write!(f, "Invalid element: {} at {}", e.stream_error, e.pos),
996            InvalidAttribute(e) => write!(f, "Invalid attribute: {} at {}", e.stream_error, e.pos),
997            InvalidCdata(e) => write!(f, "Invalid CDATA: {} at {}", e.stream_error, e.pos),
998            InvalidCharData(e) => write!(f, "Invalid char data: {} at {}", e.stream_error, e.pos),
999            UnknownToken(e) => write!(f, "Unknown token at {e}"),
1000        }
1001    }
1002}
1003
1004impl_result!(
1005    Xml,
1006    XmlError,
1007    ResultXmlXmlError,
1008    copy = false,
1009    [Debug, PartialEq, Eq, PartialOrd, Clone]
1010);
1011
1012#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1013#[repr(C)]
1014pub struct DuplicatedNamespaceError {
1015    pub ns: AzString,
1016    pub pos: XmlTextPos,
1017}
1018
1019#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1020#[repr(C)]
1021pub struct UnknownNamespaceError {
1022    pub ns: AzString,
1023    pub pos: XmlTextPos,
1024}
1025
1026#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1027#[repr(C)]
1028pub struct UnexpectedCloseTagError {
1029    pub expected: AzString,
1030    pub actual: AzString,
1031    pub pos: XmlTextPos,
1032}
1033
1034#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1035#[repr(C)]
1036pub struct UnknownEntityReferenceError {
1037    pub entity: AzString,
1038    pub pos: XmlTextPos,
1039}
1040
1041#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1042#[repr(C)]
1043pub struct DuplicatedAttributeError {
1044    pub attribute: AzString,
1045    pub pos: XmlTextPos,
1046}
1047
1048/// Error for mismatched open/close tags in XML hierarchy
1049#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1050#[repr(C)]
1051pub struct MalformedHierarchyError {
1052    /// The tag that was expected (from the opening tag)
1053    pub expected: AzString,
1054    /// The tag that was actually found (the closing tag)
1055    pub got: AzString,
1056}
1057
1058#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1059#[repr(C, u8)]
1060pub enum XmlError {
1061    NoParserAvailable,
1062    InvalidXmlPrefixUri(XmlTextPos),
1063    UnexpectedXmlUri(XmlTextPos),
1064    UnexpectedXmlnsUri(XmlTextPos),
1065    InvalidElementNamePrefix(XmlTextPos),
1066    DuplicatedNamespace(DuplicatedNamespaceError),
1067    UnknownNamespace(UnknownNamespaceError),
1068    UnexpectedCloseTag(UnexpectedCloseTagError),
1069    UnexpectedEntityCloseTag(XmlTextPos),
1070    UnknownEntityReference(UnknownEntityReferenceError),
1071    MalformedEntityReference(XmlTextPos),
1072    EntityReferenceLoop(XmlTextPos),
1073    InvalidAttributeValue(XmlTextPos),
1074    DuplicatedAttribute(DuplicatedAttributeError),
1075    NoRootNode,
1076    SizeLimit,
1077    DtdDetected,
1078    /// Invalid hierarchy close tags, i.e `<app></p></app>`
1079    MalformedHierarchy(MalformedHierarchyError),
1080    ParserError(XmlParseError),
1081    UnclosedRootNode,
1082    UnexpectedDeclaration(XmlTextPos),
1083    NodesLimitReached,
1084    AttributesLimitReached,
1085    NamespacesLimitReached,
1086    InvalidName(XmlTextPos),
1087    NonXmlChar(XmlTextPos),
1088    InvalidChar(XmlTextPos),
1089    InvalidChar2(XmlTextPos),
1090    InvalidString(XmlTextPos),
1091    InvalidExternalID(XmlTextPos),
1092    InvalidComment(XmlTextPos),
1093    InvalidCharacterData(XmlTextPos),
1094    UnknownToken(XmlTextPos),
1095    UnexpectedEndOfStream,
1096}
1097
1098impl fmt::Display for XmlError {
1099    #[allow(clippy::too_many_lines)] // large but cohesive: one arm per variant
1100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1101        use self::XmlError::{
1102            AttributesLimitReached, DtdDetected, DuplicatedAttribute, DuplicatedNamespace,
1103            EntityReferenceLoop, InvalidAttributeValue, InvalidChar, InvalidChar2,
1104            InvalidCharacterData, InvalidComment, InvalidElementNamePrefix, InvalidExternalID,
1105            InvalidName, InvalidString, InvalidXmlPrefixUri, MalformedEntityReference,
1106            MalformedHierarchy, NamespacesLimitReached, NoParserAvailable, NoRootNode,
1107            NodesLimitReached, NonXmlChar, ParserError, SizeLimit, UnclosedRootNode,
1108            UnexpectedCloseTag, UnexpectedDeclaration, UnexpectedEndOfStream,
1109            UnexpectedEntityCloseTag, UnexpectedXmlUri, UnexpectedXmlnsUri, UnknownEntityReference,
1110            UnknownNamespace, UnknownToken,
1111        };
1112        match self {
1113            NoParserAvailable => write!(
1114                f,
1115                "Library was compiled without XML parser (XML parser not available)"
1116            ),
1117            InvalidXmlPrefixUri(pos) => {
1118                write!(f, "Invalid XML Prefix URI at line {}:{}", pos.row, pos.col)
1119            }
1120            UnexpectedXmlUri(pos) => {
1121                write!(f, "Unexpected XML URI at line {}:{}", pos.row, pos.col)
1122            }
1123            UnexpectedXmlnsUri(pos) => write!(
1124                f,
1125                "Unexpected XML namespace URI at line {}:{}",
1126                pos.row, pos.col
1127            ),
1128            InvalidElementNamePrefix(pos) => write!(
1129                f,
1130                "Invalid element name prefix at line {}:{}",
1131                pos.row, pos.col
1132            ),
1133            DuplicatedNamespace(ns) => write!(
1134                f,
1135                "Duplicated namespace: \"{}\" at {}",
1136                ns.ns.as_str(),
1137                ns.pos
1138            ),
1139            UnknownNamespace(uns) => write!(
1140                f,
1141                "Unknown namespace: \"{}\" at {}",
1142                uns.ns.as_str(),
1143                uns.pos
1144            ),
1145            UnexpectedCloseTag(ct) => write!(
1146                f,
1147                "Unexpected close tag: expected \"{}\", got \"{}\" at {}",
1148                ct.expected.as_str(),
1149                ct.actual.as_str(),
1150                ct.pos
1151            ),
1152            UnexpectedEntityCloseTag(pos) => write!(
1153                f,
1154                "Unexpected entity close tag at line {}:{}",
1155                pos.row, pos.col
1156            ),
1157            UnknownEntityReference(uer) => write!(
1158                f,
1159                "Unexpected entity reference: \"{}\" at {}",
1160                uer.entity, uer.pos
1161            ),
1162            MalformedEntityReference(pos) => write!(
1163                f,
1164                "Malformed entity reference at line {}:{}",
1165                pos.row, pos.col
1166            ),
1167            EntityReferenceLoop(pos) => write!(
1168                f,
1169                "Entity reference loop (recursive entity reference) at line {}:{}",
1170                pos.row, pos.col
1171            ),
1172            InvalidAttributeValue(pos) => {
1173                write!(f, "Invalid attribute value at line {}:{}", pos.row, pos.col)
1174            }
1175            DuplicatedAttribute(ae) => write!(
1176                f,
1177                "Duplicated attribute \"{}\" at line {}:{}",
1178                ae.attribute.as_str(),
1179                ae.pos.row,
1180                ae.pos.col
1181            ),
1182            NoRootNode => write!(f, "No root node found"),
1183            SizeLimit => write!(f, "XML file too large (size limit reached)"),
1184            DtdDetected => write!(f, "Document type descriptor detected"),
1185            MalformedHierarchy(e) => write!(
1186                f,
1187                "Malformed hierarchy: expected <{}/> closing tag, got <{}/>",
1188                e.expected.as_str(),
1189                e.got.as_str()
1190            ),
1191            ParserError(p) => write!(f, "{p}"),
1192            UnclosedRootNode => write!(f, "unclosed root node"),
1193            UnexpectedDeclaration(tp) => write!(f, "unexpected declaration at {tp}"),
1194            NodesLimitReached => write!(f, "nodes limit reached"),
1195            AttributesLimitReached => write!(f, "attributes limit reached"),
1196            NamespacesLimitReached => write!(f, "namespaces limit reached"),
1197            InvalidName(tp) => write!(f, "invalid name at {tp}"),
1198            NonXmlChar(tp) => write!(f, "non xml char at {tp}"),
1199            InvalidChar(tp) => write!(f, "invalid char at {tp}"),
1200            InvalidChar2(tp) => write!(f, "invalid char2 at {tp}"),
1201            InvalidString(tp) => write!(f, "invalid string at {tp}"),
1202            InvalidExternalID(tp) => write!(f, "invalid externalid at {tp}"),
1203            InvalidComment(tp) => write!(f, "invalid comment at {tp}"),
1204            InvalidCharacterData(tp) => write!(f, "invalid character data at {tp}"),
1205            UnknownToken(tp) => write!(f, "unknown token at {tp}"),
1206            UnexpectedEndOfStream => write!(f, "unexpected end of stream"),
1207        }
1208    }
1209}
1210
1211// ============================================================================
1212// New repr(C) component system
1213// ============================================================================
1214
1215/// Identifies a component within a library collection.
1216/// e.g. collection="builtin", name="div" for the `<div>` element,
1217/// or collection="shadcn", name="avatar" for a custom component.
1218#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1219#[repr(C)]
1220pub struct ComponentId {
1221    /// Library / collection name: "builtin", "shadcn", "myproject"
1222    pub collection: AzString,
1223    /// Component name within the collection: "div", "avatar", "card"
1224    pub name: AzString,
1225}
1226
1227impl ComponentId {
1228    #[must_use]
1229    pub fn builtin(name: &str) -> Self {
1230        Self {
1231            collection: AzString::from_const_str("builtin"),
1232            name: AzString::from(name),
1233        }
1234    }
1235
1236    #[must_use]
1237    pub fn new(collection: &str, name: &str) -> Self {
1238        Self {
1239            collection: AzString::from(collection),
1240            name: AzString::from(name),
1241        }
1242    }
1243
1244    /// Returns "collection:name" format string
1245    #[must_use]
1246    pub fn qualified_name(&self) -> String {
1247        format!("{}:{}", self.collection.as_str(), self.name.as_str())
1248    }
1249}
1250
1251// ============================================================================
1252// Component type system — rich type descriptors for component fields
1253// ============================================================================
1254
1255/// A single argument in a callback signature.
1256#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1257#[repr(C)]
1258pub struct ComponentCallbackArg {
1259    /// Argument name, e.g. "`button_id`"
1260    pub name: AzString,
1261    /// Argument type
1262    pub arg_type: ComponentFieldType,
1263}
1264
1265impl_vec!(
1266    ComponentCallbackArg,
1267    ComponentCallbackArgVec,
1268    ComponentCallbackArgVecDestructor,
1269    ComponentCallbackArgVecDestructorType,
1270    ComponentCallbackArgVecSlice,
1271    OptionComponentCallbackArg
1272);
1273impl_option!(
1274    ComponentCallbackArg,
1275    OptionComponentCallbackArg,
1276    copy = false,
1277    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1278);
1279impl_vec_debug!(ComponentCallbackArg, ComponentCallbackArgVec);
1280impl_vec_partialeq!(ComponentCallbackArg, ComponentCallbackArgVec);
1281impl_vec_eq!(ComponentCallbackArg, ComponentCallbackArgVec);
1282impl_vec_partialord!(ComponentCallbackArg, ComponentCallbackArgVec);
1283impl_vec_ord!(ComponentCallbackArg, ComponentCallbackArgVec);
1284impl_vec_hash!(ComponentCallbackArg, ComponentCallbackArgVec);
1285impl_vec_clone!(
1286    ComponentCallbackArg,
1287    ComponentCallbackArgVec,
1288    ComponentCallbackArgVecDestructor
1289);
1290
1291/// Callback signature: return type + argument list.
1292#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1293#[repr(C)]
1294pub struct ComponentCallbackSignature {
1295    /// Return type name, e.g. "Update"
1296    pub return_type: AzString,
1297    /// Callback arguments (excluding the implicit `&mut RefAny` and `&mut CallbackInfo`)
1298    pub args: ComponentCallbackArgVec,
1299}
1300
1301/// Heap-allocated box for recursive `ComponentFieldType` (e.g. `Option<String>`).
1302/// Uses raw pointer indirection to break the infinite size.
1303#[repr(C)]
1304pub struct ComponentFieldTypeBox {
1305    pub ptr: *mut ComponentFieldType,
1306}
1307
1308impl ComponentFieldTypeBox {
1309    #[must_use]
1310    pub fn new(t: ComponentFieldType) -> Self {
1311        Self {
1312            ptr: Box::into_raw(Box::new(t)),
1313        }
1314    }
1315
1316    #[must_use]
1317    pub fn as_ref(&self) -> &ComponentFieldType {
1318        unsafe { &*self.ptr }
1319    }
1320}
1321
1322impl Clone for ComponentFieldTypeBox {
1323    fn clone(&self) -> Self {
1324        Self::new(unsafe { (*self.ptr).clone() })
1325    }
1326}
1327
1328impl Drop for ComponentFieldTypeBox {
1329    fn drop(&mut self) {
1330        // Null the pointer as we free it, so a *second* drop is a no-op instead
1331        // of a double free. This type is a by-value payload of the
1332        // `ComponentFieldType` enum, whose codegen FFI mirror gets
1333        // `impl Drop { _delete }` (= drop_in_place of the real type) AND Rust
1334        // field drop-glue — dropping each by-value field twice. Without this
1335        // take-and-null the second drop would `Box::from_raw` a dangling pointer.
1336        let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
1337        if !ptr.is_null() {
1338            unsafe {
1339                drop(Box::from_raw(ptr));
1340            }
1341        }
1342    }
1343}
1344
1345impl fmt::Debug for ComponentFieldTypeBox {
1346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1347        if self.ptr.is_null() {
1348            write!(f, "ComponentFieldTypeBox(null)")
1349        } else {
1350            write!(f, "ComponentFieldTypeBox({:?})", unsafe { &*self.ptr })
1351        }
1352    }
1353}
1354
1355impl PartialEq for ComponentFieldTypeBox {
1356    fn eq(&self, other: &Self) -> bool {
1357        if self.ptr.is_null() && other.ptr.is_null() {
1358            return true;
1359        }
1360        if self.ptr.is_null() || other.ptr.is_null() {
1361            return false;
1362        }
1363        unsafe { *self.ptr == *other.ptr }
1364    }
1365}
1366
1367impl Eq for ComponentFieldTypeBox {}
1368
1369impl PartialOrd for ComponentFieldTypeBox {
1370    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1371        Some(self.cmp(other))
1372    }
1373}
1374
1375impl Ord for ComponentFieldTypeBox {
1376    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1377        match (self.ptr.is_null(), other.ptr.is_null()) {
1378            (true, true) => core::cmp::Ordering::Equal,
1379            (true, false) => core::cmp::Ordering::Less,
1380            (false, true) => core::cmp::Ordering::Greater,
1381            (false, false) => unsafe { (*self.ptr).cmp(&*other.ptr) },
1382        }
1383    }
1384}
1385
1386impl Hash for ComponentFieldTypeBox {
1387    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1388        if !self.ptr.is_null() {
1389            unsafe {
1390                (*self.ptr).hash(state);
1391            }
1392        }
1393    }
1394}
1395
1396/// Heap-allocated box for recursive `ComponentFieldValue` (e.g. `Some(value)`).
1397/// Uses raw pointer indirection to break the infinite size.
1398#[repr(C)]
1399pub struct ComponentFieldValueBox {
1400    pub ptr: *mut ComponentFieldValue,
1401}
1402
1403impl ComponentFieldValueBox {
1404    #[must_use]
1405    pub fn new(v: ComponentFieldValue) -> Self {
1406        Self {
1407            ptr: Box::into_raw(Box::new(v)),
1408        }
1409    }
1410
1411    #[must_use]
1412    pub fn as_ref(&self) -> &ComponentFieldValue {
1413        unsafe { &*self.ptr }
1414    }
1415}
1416
1417impl Clone for ComponentFieldValueBox {
1418    fn clone(&self) -> Self {
1419        Self::new(unsafe { (*self.ptr).clone() })
1420    }
1421}
1422
1423impl Drop for ComponentFieldValueBox {
1424    fn drop(&mut self) {
1425        // Take-and-null so a second drop (codegen FFI double-drop of a by-value
1426        // field, see `ComponentFieldTypeBox`) is a no-op, not a double free.
1427        let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
1428        if !ptr.is_null() {
1429            unsafe {
1430                drop(Box::from_raw(ptr));
1431            }
1432        }
1433    }
1434}
1435
1436impl fmt::Debug for ComponentFieldValueBox {
1437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1438        if self.ptr.is_null() {
1439            write!(f, "ComponentFieldValueBox(null)")
1440        } else {
1441            write!(f, "ComponentFieldValueBox({:?})", unsafe { &*self.ptr })
1442        }
1443    }
1444}
1445
1446impl PartialEq for ComponentFieldValueBox {
1447    fn eq(&self, other: &Self) -> bool {
1448        if self.ptr.is_null() && other.ptr.is_null() {
1449            return true;
1450        }
1451        if self.ptr.is_null() || other.ptr.is_null() {
1452            return false;
1453        }
1454        unsafe { *self.ptr == *other.ptr }
1455    }
1456}
1457
1458/// Rich type descriptor for a component field.
1459/// Replaces the old `AzString` type names ("String", "bool", etc.) with
1460/// a structured enum that the debugger can use for type-aware editing.
1461#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1462#[repr(C, u8)]
1463pub enum ComponentFieldType {
1464    String,
1465    Bool,
1466    I32,
1467    I64,
1468    U32,
1469    U64,
1470    Usize,
1471    F32,
1472    F64,
1473    ColorU,
1474    CssProperty,
1475    ImageRef,
1476    FontRef,
1477    /// `StyledDom` slot — field name = slot name
1478    StyledDom,
1479    /// Callback with typed signature
1480    Callback(ComponentCallbackSignature),
1481    /// `RefAny` data binding with type hint
1482    RefAny(AzString),
1483    /// Optional value (recursive via Box)
1484    OptionType(ComponentFieldTypeBox),
1485    /// Vec of values (recursive via Box)
1486    VecType(ComponentFieldTypeBox),
1487    /// Reference to a struct defined in the same library
1488    StructRef(AzString),
1489    /// Reference to an enum defined in the same library
1490    EnumRef(AzString),
1491}
1492
1493impl ComponentFieldType {
1494    /// Parse a field type string like "String", "Option<Bool>", "Vec<I32>",
1495    /// "Callback(fn(LayoutCallbackInfo) -> Dom)", "StructRef(MyStruct)" etc.
1496    /// Returns `None` if the string cannot be parsed.
1497    #[must_use]
1498    pub fn parse(s: &str) -> Option<Self> {
1499        Self::parse_depth(s, 0)
1500    }
1501
1502    /// Depth-bounded implementation of [`parse`](Self::parse).
1503    ///
1504    /// AUDIT 2026-07-08: `Option<..>` / `Vec<..>` wrappers recurse once per level,
1505    /// so an attacker string like `"Option<".repeat(100_000)` (with matching `>`)
1506    /// overflowed the stack. Recursion is capped at [`MAX_TYPE_PARSE_DEPTH`];
1507    /// beyond it, parsing fails (`None`) instead of crashing.
1508    fn parse_depth(s: &str, depth: usize) -> Option<Self> {
1509        if depth > MAX_TYPE_PARSE_DEPTH {
1510            return None;
1511        }
1512        let s = s.trim();
1513        match s {
1514            "String" | "string" => return Some(Self::String),
1515            "Bool" | "bool" => return Some(Self::Bool),
1516            "I32" | "i32" => return Some(Self::I32),
1517            "I64" | "i64" => return Some(Self::I64),
1518            "U32" | "u32" => return Some(Self::U32),
1519            "U64" | "u64" => return Some(Self::U64),
1520            "Usize" | "usize" => return Some(Self::Usize),
1521            "F32" | "f32" => return Some(Self::F32),
1522            "F64" | "f64" => return Some(Self::F64),
1523            "ColorU" => return Some(Self::ColorU),
1524            "CssProperty" => return Some(Self::CssProperty),
1525            "ImageRef" => return Some(Self::ImageRef),
1526            "FontRef" => return Some(Self::FontRef),
1527            "StyledDom" => return Some(Self::StyledDom),
1528            "RefAny" => return Some(Self::RefAny(AzString::from(""))),
1529            _ => {}
1530        }
1531
1532        // Option<T>
1533        if let Some(inner) = s.strip_prefix("Option<").and_then(|r| r.strip_suffix('>')) {
1534            let inner_type = Self::parse_depth(inner, depth + 1)?;
1535            return Some(Self::OptionType(ComponentFieldTypeBox::new(inner_type)));
1536        }
1537
1538        // Vec<T>
1539        if let Some(inner) = s.strip_prefix("Vec<").and_then(|r| r.strip_suffix('>')) {
1540            let inner_type = Self::parse_depth(inner, depth + 1)?;
1541            return Some(Self::VecType(ComponentFieldTypeBox::new(inner_type)));
1542        }
1543
1544        // Callback(signature)
1545        if let Some(sig) = s
1546            .strip_prefix("Callback(")
1547            .and_then(|r| r.strip_suffix(')'))
1548        {
1549            return Some(Self::Callback(ComponentCallbackSignature {
1550                return_type: AzString::from(sig),
1551                args: Vec::new().into(),
1552            }));
1553        }
1554
1555        // RefAny(TypeHint)
1556        if let Some(hint) = s.strip_prefix("RefAny(").and_then(|r| r.strip_suffix(')')) {
1557            return Some(Self::RefAny(AzString::from(hint)));
1558        }
1559
1560        // EnumRef(Name) — explicit
1561        if let Some(name) = s.strip_prefix("EnumRef(").and_then(|r| r.strip_suffix(')')) {
1562            return Some(Self::EnumRef(AzString::from(name)));
1563        }
1564
1565        // StructRef(Name) — explicit
1566        if let Some(name) = s
1567            .strip_prefix("StructRef(")
1568            .and_then(|r| r.strip_suffix(')'))
1569        {
1570            return Some(Self::StructRef(AzString::from(name)));
1571        }
1572
1573        // If starts with uppercase, treat as StructRef
1574        if s.chars().next().is_some_and(char::is_uppercase) {
1575            return Some(Self::StructRef(AzString::from(s)));
1576        }
1577
1578        None
1579    }
1580
1581    /// Format this field type to its canonical string representation.
1582    /// This is the inverse of `parse`.
1583    #[must_use]
1584    pub fn format(&self) -> String {
1585        match self {
1586            Self::String => "String".to_string(),
1587            Self::Bool => "Bool".to_string(),
1588            Self::I32 => "I32".to_string(),
1589            Self::I64 => "I64".to_string(),
1590            Self::U32 => "U32".to_string(),
1591            Self::U64 => "U64".to_string(),
1592            Self::Usize => "Usize".to_string(),
1593            Self::F32 => "F32".to_string(),
1594            Self::F64 => "F64".to_string(),
1595            Self::ColorU => "ColorU".to_string(),
1596            Self::CssProperty => "CssProperty".to_string(),
1597            Self::ImageRef => "ImageRef".to_string(),
1598            Self::FontRef => "FontRef".to_string(),
1599            Self::StyledDom => "StyledDom".to_string(),
1600            Self::Callback(sig) => format!("Callback({})", sig.return_type.as_str()),
1601            Self::RefAny(hint) => {
1602                if hint.as_str().is_empty() {
1603                    "RefAny".to_string()
1604                } else {
1605                    format!("RefAny({})", hint.as_str())
1606                }
1607            }
1608            Self::OptionType(inner) => format!("Option<{}>", inner.as_ref().format()),
1609            Self::VecType(inner) => format!("Vec<{}>", inner.as_ref().format()),
1610            Self::StructRef(name) | Self::EnumRef(name) => name.as_str().to_string(),
1611        }
1612    }
1613}
1614
1615impl fmt::Display for ComponentFieldType {
1616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1617        f.write_str(&self.format())
1618    }
1619}
1620
1621/// A single variant in a component enum model.
1622#[derive(Debug, Clone, PartialEq)]
1623#[repr(C)]
1624pub struct ComponentEnumVariant {
1625    /// Variant name, e.g. "Admin", "Editor", "Viewer"
1626    pub name: AzString,
1627    /// Human-readable description for this variant
1628    pub description: AzString,
1629    /// Optional associated fields for this variant
1630    pub fields: ComponentDataFieldVec,
1631}
1632
1633impl_vec!(
1634    ComponentEnumVariant,
1635    ComponentEnumVariantVec,
1636    ComponentEnumVariantVecDestructor,
1637    ComponentEnumVariantVecDestructorType,
1638    ComponentEnumVariantVecSlice,
1639    OptionComponentEnumVariant
1640);
1641impl_option!(
1642    ComponentEnumVariant,
1643    OptionComponentEnumVariant,
1644    copy = false,
1645    [Debug, Clone, PartialEq]
1646);
1647impl_vec_debug!(ComponentEnumVariant, ComponentEnumVariantVec);
1648impl_vec_partialeq!(ComponentEnumVariant, ComponentEnumVariantVec);
1649impl_vec_clone!(
1650    ComponentEnumVariant,
1651    ComponentEnumVariantVec,
1652    ComponentEnumVariantVecDestructor
1653);
1654
1655/// A named enum model for code generation.
1656/// Stored in `ComponentLibrary::enum_models`.
1657#[derive(Debug, Clone, PartialEq)]
1658#[repr(C)]
1659pub struct ComponentEnumModel {
1660    /// Enum name, e.g. "`UserRole`"
1661    pub name: AzString,
1662    /// Human-readable description
1663    pub description: AzString,
1664    /// Variants
1665    pub variants: ComponentEnumVariantVec,
1666}
1667
1668impl_vec!(
1669    ComponentEnumModel,
1670    ComponentEnumModelVec,
1671    ComponentEnumModelVecDestructor,
1672    ComponentEnumModelVecDestructorType,
1673    ComponentEnumModelVecSlice,
1674    OptionComponentEnumModel
1675);
1676impl_option!(
1677    ComponentEnumModel,
1678    OptionComponentEnumModel,
1679    copy = false,
1680    [Debug, Clone, PartialEq]
1681);
1682impl_vec_debug!(ComponentEnumModel, ComponentEnumModelVec);
1683impl_vec_partialeq!(ComponentEnumModel, ComponentEnumModelVec);
1684impl_vec_clone!(
1685    ComponentEnumModel,
1686    ComponentEnumModelVec,
1687    ComponentEnumModelVecDestructor
1688);
1689
1690/// Default value for a component field.
1691#[derive(Debug, Clone, PartialEq)]
1692#[repr(C, u8)]
1693pub enum ComponentDefaultValue {
1694    /// No default value (field is required)
1695    None,
1696    /// String literal default
1697    String(AzString),
1698    /// Boolean default
1699    Bool(bool),
1700    /// i32 default
1701    I32(i32),
1702    /// i64 default
1703    I64(i64),
1704    /// u32 default
1705    U32(u32),
1706    /// u64 default
1707    U64(u64),
1708    /// usize default
1709    Usize(usize),
1710    /// f32 default
1711    F32(f32),
1712    /// f64 default
1713    F64(f64),
1714    /// `ColorU` default
1715    ColorU(ColorU),
1716    /// Default is an instance of another component
1717    ComponentInstance(ComponentInstanceDefault),
1718    /// Default callback function pointer name
1719    CallbackFnPointer(AzString),
1720    /// JSON string representing a complex default value
1721    Json(AzString),
1722}
1723
1724impl_option!(
1725    ComponentDefaultValue,
1726    OptionComponentDefaultValue,
1727    copy = false,
1728    [Debug, Clone, PartialEq]
1729);
1730
1731/// Default component instance for a `StyledDom` slot.
1732#[derive(Debug, Clone, PartialEq)]
1733#[repr(C)]
1734pub struct ComponentInstanceDefault {
1735    /// Library name, e.g. "builtin"
1736    pub library: AzString,
1737    /// Component tag, e.g. "a"
1738    pub component: AzString,
1739    /// Field overrides for this instance
1740    pub field_overrides: ComponentFieldOverrideVec,
1741}
1742
1743/// An override for a single field in a component instance.
1744#[derive(Debug, Clone, PartialEq, Eq)]
1745#[repr(C)]
1746pub struct ComponentFieldOverride {
1747    /// Field name to override
1748    pub field_name: AzString,
1749    /// Value source for this override
1750    pub source: ComponentFieldValueSource,
1751}
1752
1753impl_vec!(
1754    ComponentFieldOverride,
1755    ComponentFieldOverrideVec,
1756    ComponentFieldOverrideVecDestructor,
1757    ComponentFieldOverrideVecDestructorType,
1758    ComponentFieldOverrideVecSlice,
1759    OptionComponentFieldOverride
1760);
1761impl_option!(
1762    ComponentFieldOverride,
1763    OptionComponentFieldOverride,
1764    copy = false,
1765    [Debug, Clone, PartialEq, Eq]
1766);
1767impl_vec_debug!(ComponentFieldOverride, ComponentFieldOverrideVec);
1768impl_vec_partialeq!(ComponentFieldOverride, ComponentFieldOverrideVec);
1769impl_vec_clone!(
1770    ComponentFieldOverride,
1771    ComponentFieldOverrideVec,
1772    ComponentFieldOverrideVecDestructor
1773);
1774
1775/// How a field value is sourced at the instance level.
1776#[derive(Debug, Clone, PartialEq, Eq)]
1777#[repr(C, u8)]
1778pub enum ComponentFieldValueSource {
1779    /// Use the component's default value
1780    Default,
1781    /// Hardcoded literal value (as string, parsed at runtime)
1782    Literal(AzString),
1783    /// Bound to an app state path (e.g. "`app_state.user.name`")
1784    Binding(AzString),
1785}
1786#[allow(variant_size_differences)]
1787// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
1788/// Runtime value for a component field — the "instance" counterpart
1789/// to `ComponentFieldType` (which is the "class" / type descriptor).
1790#[derive(Debug, Clone, PartialEq)]
1791#[repr(C, u8)]
1792#[allow(clippy::large_enum_variant)] // #[repr(C,u8)] FFI enum: boxing a variant changes the C ABI/api.json
1793pub enum ComponentFieldValue {
1794    String(AzString),
1795    Bool(bool),
1796    I32(i32),
1797    I64(i64),
1798    U32(u32),
1799    U64(u64),
1800    Usize(usize),
1801    F32(f32),
1802    F64(f64),
1803    ColorU(ColorU),
1804    /// Option<T> with no value
1805    None,
1806    /// Option<T> with a value
1807    Some(ComponentFieldValueBox),
1808    /// Vec of values
1809    Vec(ComponentFieldValueVec),
1810    /// `StyledDom` slot content
1811    StyledDom(StyledDom),
1812    /// Struct fields, in order
1813    Struct(ComponentFieldNamedValueVec),
1814    /// Enum variant
1815    Enum {
1816        variant: AzString,
1817        fields: ComponentFieldNamedValueVec,
1818    },
1819    /// Callback function reference (function name as string)
1820    Callback(AzString),
1821    /// Opaque reference-counted data
1822    RefAny(crate::refany::RefAny),
1823}
1824
1825/// Named field value: (`field_name`, value) pair.
1826#[derive(Debug, Clone, PartialEq)]
1827#[repr(C)]
1828pub struct ComponentFieldNamedValue {
1829    pub name: AzString,
1830    pub value: ComponentFieldValue,
1831}
1832
1833impl_vec!(
1834    ComponentFieldNamedValue,
1835    ComponentFieldNamedValueVec,
1836    ComponentFieldNamedValueVecDestructor,
1837    ComponentFieldNamedValueVecDestructorType,
1838    ComponentFieldNamedValueVecSlice,
1839    OptionComponentFieldNamedValue
1840);
1841impl_option!(
1842    ComponentFieldNamedValue,
1843    OptionComponentFieldNamedValue,
1844    copy = false,
1845    [Debug, Clone, PartialEq]
1846);
1847impl_vec_debug!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1848impl_vec_partialeq!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1849impl_vec_clone!(
1850    ComponentFieldNamedValue,
1851    ComponentFieldNamedValueVec,
1852    ComponentFieldNamedValueVecDestructor
1853);
1854
1855impl ComponentFieldNamedValueVec {
1856    /// Look up a field by name, return a reference to its value.
1857    #[must_use]
1858    pub fn get_field(&self, name: &str) -> Option<&ComponentFieldValue> {
1859        self.as_ref().iter().find_map(|v| {
1860            if v.name.as_str() == name {
1861                Some(&v.value)
1862            } else {
1863                None
1864            }
1865        })
1866    }
1867
1868    /// Convenience: get a field as `&str` if it is `ComponentFieldValue::String`.
1869    #[must_use]
1870    pub fn get_string(&self, name: &str) -> Option<&AzString> {
1871        match self.get_field(name) {
1872            Some(ComponentFieldValue::String(s)) => Some(s),
1873            _ => None,
1874        }
1875    }
1876}
1877
1878impl_vec!(
1879    ComponentFieldValue,
1880    ComponentFieldValueVec,
1881    ComponentFieldValueVecDestructor,
1882    ComponentFieldValueVecDestructorType,
1883    ComponentFieldValueVecSlice,
1884    OptionComponentFieldValue
1885);
1886impl_option!(
1887    ComponentFieldValue,
1888    OptionComponentFieldValue,
1889    copy = false,
1890    [Debug, Clone, PartialEq]
1891);
1892impl_vec_debug!(ComponentFieldValue, ComponentFieldValueVec);
1893impl_vec_partialeq!(ComponentFieldValue, ComponentFieldValueVec);
1894impl_vec_clone!(
1895    ComponentFieldValue,
1896    ComponentFieldValueVec,
1897    ComponentFieldValueVecDestructor
1898);
1899
1900/// A field in the component's internal data model.
1901#[derive(Debug, Clone, PartialEq)]
1902#[repr(C)]
1903pub struct ComponentDataField {
1904    /// Field name, e.g. "counter", "text", "number"
1905    pub name: AzString,
1906    /// Rich type descriptor for this field
1907    pub field_type: ComponentFieldType,
1908    /// Typed default value, or None if the field is required
1909    pub default_value: OptionComponentDefaultValue,
1910    /// Whether this field is required (must be provided by the parent)
1911    pub required: bool,
1912    /// Human-readable description
1913    pub description: AzString,
1914}
1915
1916impl_vec!(
1917    ComponentDataField,
1918    ComponentDataFieldVec,
1919    ComponentDataFieldVecDestructor,
1920    ComponentDataFieldVecDestructorType,
1921    ComponentDataFieldVecSlice,
1922    OptionComponentDataField
1923);
1924impl_option!(
1925    ComponentDataField,
1926    OptionComponentDataField,
1927    copy = false,
1928    [Debug, Clone, PartialEq]
1929);
1930impl_vec_debug!(ComponentDataField, ComponentDataFieldVec);
1931impl_vec_partialeq!(ComponentDataField, ComponentDataFieldVec);
1932impl_vec_clone!(
1933    ComponentDataField,
1934    ComponentDataFieldVec,
1935    ComponentDataFieldVecDestructor
1936);
1937
1938/// A named data model (struct definition) for code generation.
1939///
1940/// Stored in `ComponentLibrary::data_models`. Components reference these
1941/// by name in `ComponentDataField::field_type`, enabling nested/structured
1942/// data models. For example, a `UserCard` component might have a field
1943/// `user: UserProfile` where `UserProfile` is a `ComponentDataModel`.
1944#[derive(Debug, Clone)]
1945#[repr(C)]
1946pub struct ComponentDataModel {
1947    /// Type name, e.g. "`UserProfile`", "`TodoItem`"
1948    pub name: AzString,
1949    /// Human-readable description
1950    pub description: AzString,
1951    /// Fields in this struct
1952    pub fields: ComponentDataFieldVec,
1953}
1954
1955impl ComponentDataModel {
1956    /// Look up a field by name.
1957    #[must_use]
1958    pub fn get_field(&self, name: &str) -> Option<&ComponentDataField> {
1959        self.fields
1960            .as_ref()
1961            .iter()
1962            .find(|f| f.name.as_str() == name)
1963    }
1964
1965    /// Look up a field's default value as a string, if it exists and is a String variant.
1966    #[must_use]
1967    pub fn get_default_string(&self, name: &str) -> Option<&AzString> {
1968        self.get_field(name).and_then(|f| match &f.default_value {
1969            OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s),
1970            _ => None,
1971        })
1972    }
1973
1974    /// Clone this data model, overriding the default value for a field by name.
1975    /// If the field is not found, the data model is returned unchanged.
1976    #[must_use]
1977    pub fn with_default(mut self, name: &str, value: ComponentDefaultValue) -> Self {
1978        let mut fields_vec = core::mem::replace(
1979            &mut self.fields,
1980            ComponentDataFieldVec::from_const_slice(&[]),
1981        )
1982        .into_library_owned_vec();
1983        for f in &mut fields_vec {
1984            if f.name.as_str() == name {
1985                f.default_value = OptionComponentDefaultValue::Some(value);
1986                break;
1987            }
1988        }
1989        self.fields = ComponentDataFieldVec::from_vec(fields_vec);
1990        self
1991    }
1992}
1993
1994impl_vec!(
1995    ComponentDataModel,
1996    ComponentDataModelVec,
1997    ComponentDataModelVecDestructor,
1998    ComponentDataModelVecDestructorType,
1999    ComponentDataModelVecSlice,
2000    OptionComponentDataModel
2001);
2002impl_option!(
2003    ComponentDataModel,
2004    OptionComponentDataModel,
2005    copy = false,
2006    [Debug, Clone]
2007);
2008impl_vec_debug!(ComponentDataModel, ComponentDataModelVec);
2009impl_vec_clone!(
2010    ComponentDataModel,
2011    ComponentDataModelVec,
2012    ComponentDataModelVecDestructor
2013);
2014impl_vec_mut!(ComponentDataModel, ComponentDataModelVec);
2015
2016// ============================================================================
2017// Serde support for ComponentDataModel (feature-gated)
2018// ============================================================================
2019
2020#[cfg(feature = "serde-json")]
2021mod serde_impl {
2022    #[allow(clippy::wildcard_imports)] // serde impl module mirrors the parent surface
2023    use super::*;
2024    use serde::ser::SerializeStruct;
2025    use serde::{Deserialize, Deserializer, Serialize, Serializer};
2026
2027    // --- AzString helpers ---
2028
2029    fn ser_azstring<S: Serializer>(s: &AzString, serializer: S) -> Result<S::Ok, S::Error> {
2030        serializer.serialize_str(s.as_str())
2031    }
2032
2033    fn de_azstring<'de, D: Deserializer<'de>>(deserializer: D) -> Result<AzString, D::Error> {
2034        let s = String::deserialize(deserializer)?;
2035        Ok(AzString::from(s.as_str()))
2036    }
2037
2038    // --- ComponentFieldType ---
2039
2040    impl Serialize for ComponentFieldType {
2041        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2042            serializer.serialize_str(&field_type_to_string(self))
2043        }
2044    }
2045
2046    impl<'de> Deserialize<'de> for ComponentFieldType {
2047        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2048            let s = String::deserialize(deserializer)?;
2049            Ok(string_to_field_type(&s))
2050        }
2051    }
2052
2053    fn field_type_to_string(ft: &ComponentFieldType) -> String {
2054        match ft {
2055            ComponentFieldType::String => "String".into(),
2056            ComponentFieldType::Bool => "bool".into(),
2057            ComponentFieldType::I32 => "i32".into(),
2058            ComponentFieldType::I64 => "i64".into(),
2059            ComponentFieldType::U32 => "u32".into(),
2060            ComponentFieldType::U64 => "u64".into(),
2061            ComponentFieldType::Usize => "usize".into(),
2062            ComponentFieldType::F32 => "f32".into(),
2063            ComponentFieldType::F64 => "f64".into(),
2064            ComponentFieldType::ColorU => "ColorU".into(),
2065            ComponentFieldType::CssProperty => "CssProperty".into(),
2066            ComponentFieldType::ImageRef => "ImageRef".into(),
2067            ComponentFieldType::FontRef => "FontRef".into(),
2068            ComponentFieldType::StyledDom => "Dom".into(),
2069            ComponentFieldType::Callback(sig) => {
2070                alloc::format!("Callback({})", sig.return_type.as_str())
2071            }
2072            ComponentFieldType::RefAny(hint) => alloc::format!("RefAny({})", hint.as_str()),
2073            ComponentFieldType::OptionType(inner) => {
2074                alloc::format!("Option<{}>", field_type_to_string(inner.as_ref()))
2075            }
2076            ComponentFieldType::VecType(inner) => {
2077                alloc::format!("Vec<{}>", field_type_to_string(inner.as_ref()))
2078            }
2079            ComponentFieldType::StructRef(name) => alloc::format!("struct:{}", name.as_str()),
2080            ComponentFieldType::EnumRef(name) => alloc::format!("enum:{}", name.as_str()),
2081        }
2082    }
2083
2084    // A 5-arm strip_prefix dispatch ladder. `option_if_let_else` (nursery)
2085    // wants `map_or_else` here, which would nest five closures inside each
2086    // other's else-branch — strictly less readable than the ladder.
2087    #[allow(clippy::option_if_let_else)]
2088    fn string_to_field_type(s: &str) -> ComponentFieldType {
2089        match s {
2090            "String" | "string" => ComponentFieldType::String,
2091            "bool" | "Bool" => ComponentFieldType::Bool,
2092            "i32" | "I32" => ComponentFieldType::I32,
2093            "i64" | "I64" => ComponentFieldType::I64,
2094            "u32" | "U32" => ComponentFieldType::U32,
2095            "u64" | "U64" => ComponentFieldType::U64,
2096            "usize" | "Usize" => ComponentFieldType::Usize,
2097            "f32" | "F32" => ComponentFieldType::F32,
2098            "f64" | "F64" => ComponentFieldType::F64,
2099            "ColorU" | "Color" | "color" => ComponentFieldType::ColorU,
2100            "CssProperty" => ComponentFieldType::CssProperty,
2101            "ImageRef" | "Image" => ComponentFieldType::ImageRef,
2102            "FontRef" | "Font" => ComponentFieldType::FontRef,
2103            "Dom" | "StyledDom" | "Children" => ComponentFieldType::StyledDom,
2104            other => {
2105                if let Some(inner) = other
2106                    .strip_prefix("Option<")
2107                    .and_then(|s| s.strip_suffix('>'))
2108                {
2109                    ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
2110                        string_to_field_type(inner),
2111                    ))
2112                } else if let Some(inner) =
2113                    other.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>'))
2114                {
2115                    ComponentFieldType::VecType(ComponentFieldTypeBox::new(string_to_field_type(
2116                        inner,
2117                    )))
2118                } else if let Some(name) = other.strip_prefix("struct:") {
2119                    ComponentFieldType::StructRef(AzString::from(name))
2120                } else if let Some(name) = other.strip_prefix("enum:") {
2121                    ComponentFieldType::EnumRef(AzString::from(name))
2122                } else if other.starts_with("Callback") {
2123                    let ret = other
2124                        .strip_prefix("Callback(")
2125                        .and_then(|s| s.strip_suffix(')'))
2126                        .unwrap_or("()");
2127                    ComponentFieldType::Callback(ComponentCallbackSignature {
2128                        return_type: AzString::from(ret),
2129                        args: ComponentCallbackArgVec::from_const_slice(&[]),
2130                    })
2131                } else if other.starts_with("RefAny") {
2132                    let hint = other
2133                        .strip_prefix("RefAny(")
2134                        .and_then(|s| s.strip_suffix(')'))
2135                        .unwrap_or("");
2136                    ComponentFieldType::RefAny(AzString::from(hint))
2137                } else {
2138                    ComponentFieldType::String // fallback
2139                }
2140            }
2141        }
2142    }
2143
2144    // --- ComponentDefaultValue ---
2145
2146    impl Serialize for ComponentDefaultValue {
2147        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2148            use serde::ser::SerializeMap;
2149            match self {
2150                Self::None => serializer.serialize_none(),
2151                Self::String(s) => serializer.serialize_str(s.as_str()),
2152                Self::Bool(b) => serializer.serialize_bool(*b),
2153                Self::I32(v) => serializer.serialize_i32(*v),
2154                Self::I64(v) => serializer.serialize_i64(*v),
2155                Self::U32(v) => serializer.serialize_u32(*v),
2156                Self::U64(v) => serializer.serialize_u64(*v),
2157                Self::Usize(v) => serializer.serialize_u64(*v as u64),
2158                Self::F32(v) => serializer.serialize_f32(*v),
2159                Self::F64(v) => serializer.serialize_f64(*v),
2160                Self::ColorU(c) => serializer.serialize_str(&alloc::format!(
2161                    "#{:02x}{:02x}{:02x}{:02x}",
2162                    c.r,
2163                    c.g,
2164                    c.b,
2165                    c.a
2166                )),
2167                Self::ComponentInstance(ci) => {
2168                    let mut map = serializer.serialize_map(Some(2))?;
2169                    map.serialize_entry("library", ci.library.as_str())?;
2170                    map.serialize_entry("component", ci.component.as_str())?;
2171                    map.end()
2172                }
2173                Self::CallbackFnPointer(name) => serializer.serialize_str(name.as_str()),
2174                Self::Json(json_str) => {
2175                    // Serialize raw JSON string as-is by parsing and re-emitting
2176                    match serde_json::from_str::<serde_json::Value>(json_str.as_str()) {
2177                        Ok(v) => v.serialize(serializer),
2178                        Err(_) => serializer.serialize_str(json_str.as_str()),
2179                    }
2180                }
2181            }
2182        }
2183    }
2184
2185    impl<'de> Deserialize<'de> for ComponentDefaultValue {
2186        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2187            let val = serde_json::Value::deserialize(deserializer)?;
2188            // NOTE: `Value::Null` is deliberately NOT its own arm — it maps to
2189            // `Self::None`, which is exactly what the catch-all below produces.
2190            Ok(match val {
2191                serde_json::Value::Bool(b) => Self::Bool(b),
2192                serde_json::Value::Number(n) => n.as_i64().map_or_else(
2193                    || n.as_f64().map_or(Self::None, Self::F64),
2194                    |i| i32::try_from(i).map_or(Self::I64(i), Self::I32),
2195                ),
2196                serde_json::Value::String(s) => Self::String(AzString::from(s.as_str())),
2197                _ => Self::None,
2198            })
2199        }
2200    }
2201
2202    // --- OptionComponentDefaultValue ---
2203
2204    impl Serialize for OptionComponentDefaultValue {
2205        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2206            match self {
2207                Self::Some(v) => v.serialize(serializer),
2208                Self::None => serializer.serialize_none(),
2209            }
2210        }
2211    }
2212
2213    impl<'de> Deserialize<'de> for OptionComponentDefaultValue {
2214        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2215            let val = Option::<ComponentDefaultValue>::deserialize(deserializer)?;
2216            Ok(val.map_or(Self::None, Self::Some))
2217        }
2218    }
2219
2220    // --- ComponentDataField ---
2221
2222    impl Serialize for ComponentDataField {
2223        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2224            let mut s = serializer.serialize_struct("ComponentDataField", 5)?;
2225            s.serialize_field("name", self.name.as_str())?;
2226            s.serialize_field("type", &self.field_type)?;
2227            s.serialize_field("default", &self.default_value)?;
2228            s.serialize_field("required", &self.required)?;
2229            s.serialize_field("description", self.description.as_str())?;
2230            s.end()
2231        }
2232    }
2233
2234    impl<'de> Deserialize<'de> for ComponentDataField {
2235        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2236            #[derive(Deserialize)]
2237            struct Helper {
2238                name: String,
2239                #[serde(rename = "type", default = "default_type")]
2240                field_type: ComponentFieldType,
2241                #[serde(default)]
2242                default: OptionComponentDefaultValue,
2243                #[serde(default)]
2244                required: bool,
2245                #[serde(default)]
2246                description: String,
2247            }
2248            const fn default_type() -> ComponentFieldType {
2249                ComponentFieldType::String
2250            }
2251
2252            let h = Helper::deserialize(deserializer)?;
2253            Ok(Self {
2254                name: AzString::from(h.name.as_str()),
2255                field_type: h.field_type,
2256                default_value: h.default,
2257                required: h.required,
2258                description: AzString::from(h.description.as_str()),
2259            })
2260        }
2261    }
2262
2263    // --- ComponentDataModel ---
2264
2265    impl Serialize for ComponentDataModel {
2266        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2267            let mut s = serializer.serialize_struct("ComponentDataModel", 3)?;
2268            s.serialize_field("name", self.name.as_str())?;
2269            s.serialize_field("description", self.description.as_str())?;
2270            let fields: Vec<&ComponentDataField> = self.fields.as_ref().iter().collect();
2271            s.serialize_field("fields", &fields)?;
2272            s.end()
2273        }
2274    }
2275
2276    impl<'de> Deserialize<'de> for ComponentDataModel {
2277        /// A data model is a JSON **object**. This deliberately drives the
2278        /// deserializer with `deserialize_map` instead of `deserialize_struct`:
2279        /// the struct hint makes serde accept a *sequence* as well (the
2280        /// positional encoding used by compact formats), so `from_json("[]")`
2281        /// used to succeed and hand back a nameless, field-less model instead of
2282        /// reporting that the input is not a data model at all. Every key stays
2283        /// optional, so `{}` still deserializes to the empty model.
2284        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2285            use serde::de::{IgnoredAny, MapAccess, Visitor};
2286
2287            struct ModelVisitor;
2288
2289            impl<'de> Visitor<'de> for ModelVisitor {
2290                type Value = ComponentDataModel;
2291
2292                fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2293                    f.write_str("a data model object with `name`, `description` and `fields`")
2294                }
2295
2296                fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
2297                    let mut name: Option<String> = None;
2298                    let mut description: Option<String> = None;
2299                    let mut fields: Option<Vec<ComponentDataField>> = None;
2300
2301                    while let Some(key) = map.next_key::<String>()? {
2302                        match key.as_str() {
2303                            "name" => name = Some(map.next_value()?),
2304                            "description" => description = Some(map.next_value()?),
2305                            "fields" => fields = Some(map.next_value()?),
2306                            // Unknown keys are ignored (forward compatibility),
2307                            // but their values must still be consumed.
2308                            _ => {
2309                                map.next_value::<IgnoredAny>()?;
2310                            }
2311                        }
2312                    }
2313
2314                    Ok(ComponentDataModel {
2315                        name: AzString::from(name.unwrap_or_default().as_str()),
2316                        description: AzString::from(description.unwrap_or_default().as_str()),
2317                        fields: ComponentDataFieldVec::from_vec(fields.unwrap_or_default()),
2318                    })
2319                }
2320            }
2321
2322            deserializer.deserialize_map(ModelVisitor)
2323        }
2324    }
2325}
2326
2327// NOTE: no `pub use serde_impl::*` — the module holds only private helpers and
2328// trait impls, and trait impls are in scope crate-wide (and for downstream
2329// users) regardless of the defining module's visibility.
2330
2331#[cfg(feature = "serde-json")]
2332impl ComponentDataModel {
2333    /// Serialize this data model to a JSON string.
2334    ///
2335    /// # Errors
2336    ///
2337    /// Returns the serializer's error message if the model cannot be
2338    /// represented as JSON.
2339    pub fn to_json(&self) -> Result<String, String> {
2340        serde_json::to_string_pretty(self).map_err(|e| alloc::format!("{e}"))
2341    }
2342
2343    /// Deserialize a data model from a JSON string.
2344    ///
2345    /// # Errors
2346    ///
2347    /// Returns the parser's error message if `json` is malformed or does not
2348    /// match the data-model shape.
2349    pub fn from_json(json: &str) -> Result<Self, String> {
2350        serde_json::from_str(json).map_err(|e| alloc::format!("{e}"))
2351    }
2352}
2353
2354/// Source of a component definition — determines whether it can be exported
2355#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2356#[repr(C)]
2357#[derive(Default)]
2358pub enum ComponentSource {
2359    /// Built into the DLL (HTML elements). Never exported.
2360    Builtin,
2361    /// Compiled Rust widget (Button, `TextInput`, etc.). Never exported.
2362    Compiled,
2363    /// Defined via JSON/XML at runtime. Can be exported.
2364    #[default]
2365    UserDefined,
2366}
2367
2368impl ComponentSource {
2369    #[must_use]
2370    pub fn create() -> Self {
2371        Self::default()
2372    }
2373}
2374
2375/// The target language for code compilation
2376// Threaded by reference through the codegen call graph; kept non-Copy so
2377// deriving Copy doesn't force trivially_copy_pass_by_ref churn across the many
2378// &CompileTarget codegen callers for a perf-neutral change.
2379#[allow(missing_copy_implementations)]
2380#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2381#[repr(C)]
2382pub enum CompileTarget {
2383    Rust,
2384    C,
2385    Cpp,
2386    Python,
2387}
2388
2389impl_result!(
2390    StyledDom,
2391    RenderDomError,
2392    ResultStyledDomRenderDomError,
2393    copy = false,
2394    [Debug, Clone, PartialEq]
2395);
2396
2397impl_result!(
2398    AzString,
2399    CompileError,
2400    ResultStringCompileError,
2401    copy = false,
2402    [Debug, Clone, PartialEq]
2403);
2404
2405/// Render function type: takes component definition + data model (with current values
2406/// in `default_value` fields) + component map for recursive sub-component instantiation,
2407/// returns `StyledDom`.
2408///
2409/// The `data` parameter is typically `def.data_model` cloned and with caller-provided
2410/// values substituted into the `default_value` fields.
2411pub type ComponentRenderFn =
2412    fn(&ComponentDef, &ComponentDataModel, &ComponentMap) -> ResultStyledDomRenderDomError;
2413
2414/// Compile function type: takes component definition + target language + data model, returns source code.
2415pub type ComponentCompileFn = fn(
2416    &ComponentDef,
2417    &CompileTarget,
2418    &ComponentDataModel,
2419    indent: usize,
2420) -> ResultStringCompileError;
2421
2422/// Raw function pointer type that returns a single `ComponentDef` when called.
2423/// Used as the `cb` field in `RegisterComponentFn`.
2424pub type RegisterComponentFnType = extern "C" fn() -> ComponentDef;
2425
2426/// Callback struct for registering individual components at startup.
2427///
2428/// In C: pass a bare `extern "C" fn() -> ComponentDef` function pointer —
2429/// it converts automatically via `From<RegisterComponentFnType>`.
2430///
2431/// In Python: construct this struct with `cb` set to a trampoline and
2432/// `ctx` set to `Some(RefAny(...))` wrapping the Python callable.
2433#[repr(C)]
2434pub struct RegisterComponentFn {
2435    pub cb: RegisterComponentFnType,
2436    /// For FFI: stores the foreign callable (e.g., `PyFunction`).
2437    /// Native Rust/C code sets this to None.
2438    pub ctx: crate::refany::OptionRefAny,
2439}
2440
2441impl_callback!(RegisterComponentFn, RegisterComponentFnType);
2442
2443/// Raw function pointer type that returns a complete `ComponentLibrary` when called.
2444/// Used as the `cb` field in `RegisterComponentLibraryFn`.
2445pub type RegisterComponentLibraryFnType = extern "C" fn() -> ComponentLibrary;
2446
2447/// Callback struct for registering entire component libraries at startup.
2448///
2449/// In C: pass a bare `extern "C" fn() -> ComponentLibrary` function pointer —
2450/// it converts automatically via `From<RegisterComponentLibraryFnType>`.
2451///
2452/// In Python: construct this struct with `cb` set to a trampoline and
2453/// `ctx` set to `Some(RefAny(...))` wrapping the Python callable.
2454#[repr(C)]
2455pub struct RegisterComponentLibraryFn {
2456    pub cb: RegisterComponentLibraryFnType,
2457    /// For FFI: stores the foreign callable (e.g., `PyFunction`).
2458    /// Native Rust/C code sets this to None.
2459    pub ctx: crate::refany::OptionRefAny,
2460}
2461
2462impl_callback!(RegisterComponentLibraryFn, RegisterComponentLibraryFnType);
2463
2464/// A component definition — the "class" / "template" of a component.
2465/// Can come from Rust builtins, compiled widgets, JSON, or user creation in debugger.
2466///
2467#[derive(Clone)]
2468#[repr(C)]
2469pub struct ComponentDef {
2470    /// Collection + name, e.g. builtin:div, shadcn:avatar
2471    pub id: ComponentId,
2472    /// Human-readable display name, e.g. "Link" for builtin:a, "Avatar" for shadcn:avatar
2473    pub display_name: AzString,
2474    /// Markdown documentation for the component
2475    pub description: AzString,
2476    /// The component's CSS
2477    pub css: AzString,
2478    /// Where this component was defined (determines exportability)
2479    pub source: ComponentSource,
2480    /// Unified data model: all value fields, callback slots, and child slots
2481    /// in a single named struct. Code gen uses `data_model.name` as the
2482    /// input struct type name (e.g. "`ButtonData`").
2483    /// The `default_value` on each field doubles as the "current value" for
2484    /// preview rendering — callers override defaults before calling `render_fn`.
2485    pub data_model: ComponentDataModel,
2486    /// Render to live DOM
2487    pub render_fn: ComponentRenderFn,
2488    /// Compile to source code in target language
2489    pub compile_fn: ComponentCompileFn,
2490    /// Source code for `render_fn` (user-defined components only)
2491    pub render_fn_source: OptionString,
2492    /// Source code for `compile_fn` (user-defined components only)
2493    pub compile_fn_source: OptionString,
2494}
2495
2496impl fmt::Debug for ComponentDef {
2497    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2498        f.debug_struct("ComponentDef")
2499            .field("id", &self.id)
2500            .field("display_name", &self.display_name)
2501            .field("source", &self.source)
2502            .field("data_model", &self.data_model.name)
2503            .finish_non_exhaustive()
2504    }
2505}
2506
2507impl_vec!(
2508    ComponentDef,
2509    ComponentDefVec,
2510    ComponentDefVecDestructor,
2511    ComponentDefVecDestructorType,
2512    ComponentDefVecSlice,
2513    OptionComponentDef
2514);
2515impl_option!(ComponentDef, OptionComponentDef, copy = false, [Clone]);
2516impl_vec_debug!(ComponentDef, ComponentDefVec);
2517impl_vec_clone!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor);
2518impl_vec_mut!(ComponentDef, ComponentDefVec);
2519
2520/// A named collection of component definitions
2521#[derive(Debug, Clone)]
2522#[repr(C)]
2523pub struct ComponentLibrary {
2524    /// Library identifier, e.g. "builtin", "shadcn", "myproject"
2525    pub name: AzString,
2526    /// Version string
2527    pub version: AzString,
2528    /// Human-readable description
2529    pub description: AzString,
2530    /// The components in this library
2531    pub components: ComponentDefVec,
2532    /// Whether this library can be exported (false for builtin/compiled)
2533    pub exportable: bool,
2534    /// Whether this library can be modified by the user (add/remove/edit components).
2535    /// False for builtin and compiled libraries. True for user-created libraries.
2536    pub modifiable: bool,
2537    /// Named data model types defined by this library.
2538    /// Components reference these by name in their `field_type`.
2539    pub data_models: ComponentDataModelVec,
2540    /// Named enum types defined by this library.
2541    /// Components reference these via `ComponentFieldType::EnumRef(name)`.
2542    pub enum_models: ComponentEnumModelVec,
2543}
2544
2545impl_vec!(
2546    ComponentLibrary,
2547    ComponentLibraryVec,
2548    ComponentLibraryVecDestructor,
2549    ComponentLibraryVecDestructorType,
2550    ComponentLibraryVecSlice,
2551    OptionComponentLibrary
2552);
2553impl_option!(
2554    ComponentLibrary,
2555    OptionComponentLibrary,
2556    copy = false,
2557    [Debug, Clone]
2558);
2559impl_vec_debug!(ComponentLibrary, ComponentLibraryVec);
2560impl_vec_clone!(
2561    ComponentLibrary,
2562    ComponentLibraryVec,
2563    ComponentLibraryVecDestructor
2564);
2565impl_vec_mut!(ComponentLibrary, ComponentLibraryVec);
2566
2567/// The component map — holds libraries with namespaced components.
2568#[derive(Debug, Clone)]
2569#[repr(C)]
2570pub struct ComponentMap {
2571    /// Libraries indexed by name. "builtin" is always present.
2572    pub libraries: ComponentLibraryVec,
2573}
2574
2575impl ComponentMap {
2576    /// Qualified lookup: "shadcn:avatar" -> finds library "shadcn", component "avatar"
2577    #[must_use]
2578    pub fn get(&self, collection: &str, name: &str) -> Option<&ComponentDef> {
2579        self.libraries
2580            .iter()
2581            .find(|lib| lib.name.as_str() == collection)
2582            .and_then(|lib| lib.components.iter().find(|c| c.id.name.as_str() == name))
2583    }
2584
2585    /// Unqualified lookup: "div" -> searches ONLY the "builtin" library.
2586    #[must_use]
2587    pub fn get_unqualified(&self, name: &str) -> Option<&ComponentDef> {
2588        self.get("builtin", name)
2589    }
2590
2591    /// Parse a "collection:name" string into a lookup
2592    #[must_use]
2593    pub fn get_by_qualified_name(&self, qualified: &str) -> Option<&ComponentDef> {
2594        if let Some((collection, name)) = qualified.split_once(':') {
2595            self.get(collection, name)
2596        } else {
2597            self.get_unqualified(qualified)
2598        }
2599    }
2600
2601    /// Get all libraries that can be exported (user-defined only)
2602    #[must_use]
2603    pub fn get_exportable_libraries(&self) -> Vec<&ComponentLibrary> {
2604        self.libraries.iter().filter(|lib| lib.exportable).collect()
2605    }
2606
2607    /// Get all component definitions across all libraries
2608    #[must_use]
2609    pub fn all_components(&self) -> Vec<&ComponentDef> {
2610        self.libraries
2611            .iter()
2612            .flat_map(|lib| lib.components.iter())
2613            .collect()
2614    }
2615}
2616
2617// ============================================================================
2618// Builtin component bridge — wraps existing render/compile into ComponentDef
2619// ============================================================================
2620
2621/// Single source of truth mapping HTML/SVG tag names to node variants.
2622///
2623/// Each `"tag" => Variant` entry expands to **both** a `NodeType::Variant` arm in
2624/// [`tag_to_node_type`] and a `NodeTypeTag::Variant` arm in [`tag_to_node_type_tag`],
2625/// so the two lookups can never drift apart. Tags whose two enums diverge —
2626/// `img`, `image`, `icon` — are handled as explicit special cases inside each
2627/// generated function and are intentionally absent from this table.
2628macro_rules! html_tag_node_types {
2629    ($($tag:literal => $variant:ident),* $(,)?) => {
2630        /// Map a builtin tag name to its corresponding `NodeType`.
2631        /// Falls back to `NodeType::Div` for unknown tags.
2632        #[must_use] pub fn tag_to_node_type(tag: &str) -> NodeType {
2633            match tag {
2634                // `<img>` becomes a replaced `NodeType::Image`. The `src` attribute is not
2635                // available here, so a placeholder `NullImage` (0x0, empty tag) is created;
2636                // `xml_node_to_dom_fast` overrides it with a `NullImage` whose `tag` carries
2637                // the `src` bytes so a renderer (e.g. printpdf) can resolve the actual image.
2638                "img" => NodeType::Image(azul_css::css::BoxOrStatic::heap(
2639                    crate::resources::ImageRef::null_image(
2640                        0,
2641                        0,
2642                        crate::resources::RawImageFormat::RGBA8,
2643                        alloc::vec::Vec::new(),
2644                    ),
2645                )),
2646                // `<icon>content_copy</icon>` becomes an un-named `NodeType::Icon`;
2647                // the icon SPEC is its text content, consumed by the icon
2648                // resolution pass (`resolve_icons_in_styled_dom`) against the
2649                // registered icon packs — exactly like a ligature icon font
2650                // turns glyph text into an icon. The builders stay generic.
2651                "icon" => NodeType::Icon(azul_css::css::BoxOrStatic::heap(
2652                    azul_css::AzString::from_const_str(""),
2653                )),
2654                // `<transient-window>` starts CLOSED with every default; the
2655                // parser applies `open=` / `anchor=` / `dismiss=` / `size=` /
2656                // `tearoff=` onto this config afterwards (see
2657                // `apply_transient_window_attrs`). Carrying the config inline is
2658                // what lets a closed popup cost nothing.
2659                "transient-window" => NodeType::TransientWindow(
2660                    crate::transient::TransientWindowConfig::closed(),
2661                ),
2662                $($tag => NodeType::$variant,)*
2663                _ => NodeType::Div,
2664            }
2665        }
2666
2667        /// Map a tag name to its CSS `NodeTypeTag` for CSS matching in the compile pipeline.
2668        /// Falls back to `NodeTypeTag::Div` for unknown tags.
2669        fn tag_to_node_type_tag(tag: &str) -> NodeTypeTag {
2670            match tag {
2671                // `img`/`image`/`icon` have no 1:1 `NodeType` equivalent (see
2672                // `tag_to_node_type`), so they map to dedicated `NodeTypeTag` variants.
2673                "img" | "image" => NodeTypeTag::Img,
2674                "icon" => NodeTypeTag::Icon,
2675                "transient-window" => NodeTypeTag::TransientWindow,
2676                $($tag => NodeTypeTag::$variant,)*
2677                _ => NodeTypeTag::Div,
2678            }
2679        }
2680    };
2681}
2682
2683html_tag_node_types! {
2684    // Document structure
2685    "html" => Html,
2686    "head" => Head,
2687    "title" => Title,
2688    "body" => Body,
2689    // Block-level
2690    "div" => Div,
2691    "header" => Header,
2692    "footer" => Footer,
2693    "section" => Section,
2694    "article" => Article,
2695    "aside" => Aside,
2696    "nav" => Nav,
2697    "main" => Main,
2698    "figure" => Figure,
2699    "figcaption" => FigCaption,
2700    "address" => Address,
2701    "details" => Details,
2702    "summary" => Summary,
2703    "dialog" => Dialog,
2704    // Headings
2705    "h1" => H1,
2706    "h2" => H2,
2707    "h3" => H3,
2708    "h4" => H4,
2709    "h5" => H5,
2710    "h6" => H6,
2711    // Text content
2712    "p" => P,
2713    "span" => Span,
2714    "pre" => Pre,
2715    "code" => Code,
2716    "blockquote" => BlockQuote,
2717    "br" => Br,
2718    "hr" => Hr,
2719    "pagebreak" => PageBreak,
2720    // Lists
2721    "ul" => Ul,
2722    "ol" => Ol,
2723    "li" => Li,
2724    "dl" => Dl,
2725    "dt" => Dt,
2726    "dd" => Dd,
2727    "menu" => Menu,
2728    "menuitem" => MenuItem,
2729    "dir" => Dir,
2730    // Tables
2731    "table" => Table,
2732    "caption" => Caption,
2733    "thead" => THead,
2734    "tbody" => TBody,
2735    "tfoot" => TFoot,
2736    "tr" => Tr,
2737    "th" => Th,
2738    "td" => Td,
2739    "colgroup" => ColGroup,
2740    "col" => Col,
2741    // Forms
2742    "form" => Form,
2743    "fieldset" => FieldSet,
2744    "legend" => Legend,
2745    "label" => Label,
2746    "input" => Input,
2747    "button" => Button,
2748    "select" => Select,
2749    "optgroup" => OptGroup,
2750    "option" => SelectOption,
2751    "textarea" => TextArea,
2752    "output" => Output,
2753    "progress" => Progress,
2754    "meter" => Meter,
2755    "datalist" => DataList,
2756    // Inline
2757    "a" => A,
2758    "strong" => Strong,
2759    "em" => Em,
2760    "b" => B,
2761    "i" => I,
2762    "u" => U,
2763    "s" => S,
2764    "small" => Small,
2765    "mark" => Mark,
2766    "del" => Del,
2767    "ins" => Ins,
2768    "samp" => Samp,
2769    "kbd" => Kbd,
2770    "var" => Var,
2771    "cite" => Cite,
2772    "dfn" => Dfn,
2773    "abbr" => Abbr,
2774    "acronym" => Acronym,
2775    "q" => Q,
2776    "time" => Time,
2777    "sub" => Sub,
2778    "sup" => Sup,
2779    "big" => Big,
2780    "bdo" => Bdo,
2781    "bdi" => Bdi,
2782    "wbr" => Wbr,
2783    "ruby" => Ruby,
2784    "rt" => Rt,
2785    "rtc" => Rtc,
2786    "rp" => Rp,
2787    "data" => Data,
2788    // Embedded content (`img` is a special case in the generated fns)
2789    "canvas" => Canvas,
2790    "object" => Object,
2791    "param" => Param,
2792    "embed" => Embed,
2793    "audio" => Audio,
2794    "video" => Video,
2795    "source" => Source,
2796    "track" => Track,
2797    "map" => Map,
2798    "area" => Area,
2799    // SVG elements
2800    "svg" => Svg,
2801    "g" => SvgG,
2802    "defs" => SvgDefs,
2803    "symbol" => SvgSymbol,
2804    "use" => SvgUse,
2805    "switch" => SvgSwitch,
2806    "path" => SvgPath,
2807    "circle" => SvgCircle,
2808    "rect" => SvgRect,
2809    "ellipse" => SvgEllipse,
2810    "line" => SvgLine,
2811    "polygon" => SvgPolygon,
2812    "polyline" => SvgPolyline,
2813    "tspan" => SvgTspan,
2814    "textpath" => SvgTextPath,
2815    "lineargradient" => SvgLinearGradient,
2816    "radialgradient" => SvgRadialGradient,
2817    "stop" => SvgStop,
2818    "pattern" => SvgPattern,
2819    "clippath" => SvgClipPathElement,
2820    "mask" => SvgMask,
2821    "filter" => SvgFilter,
2822    "feblend" => SvgFeBlend,
2823    "fecolormatrix" => SvgFeColorMatrix,
2824    "fecomponenttransfer" => SvgFeComponentTransfer,
2825    "fecomposite" => SvgFeComposite,
2826    "feconvolvematrix" => SvgFeConvolveMatrix,
2827    "fediffuselighting" => SvgFeDiffuseLighting,
2828    "fedisplacementmap" => SvgFeDisplacementMap,
2829    "fedistantlight" => SvgFeDistantLight,
2830    "fedropshadow" => SvgFeDropShadow,
2831    "feflood" => SvgFeFlood,
2832    "fefuncr" => SvgFeFuncR,
2833    "fefuncg" => SvgFeFuncG,
2834    "fefuncb" => SvgFeFuncB,
2835    "fefunca" => SvgFeFuncA,
2836    "fegaussianblur" => SvgFeGaussianBlur,
2837    "feimage" => SvgFeImage,
2838    "femerge" => SvgFeMerge,
2839    "femergenode" => SvgFeMergeNode,
2840    "femorphology" => SvgFeMorphology,
2841    "feoffset" => SvgFeOffset,
2842    "fepointlight" => SvgFePointLight,
2843    "fespecularlighting" => SvgFeSpecularLighting,
2844    "fespotlight" => SvgFeSpotLight,
2845    "fetile" => SvgFeTile,
2846    "feturbulence" => SvgFeTurbulence,
2847    "foreignobject" => SvgForeignObject,
2848    "desc" => SvgDesc,
2849    "view" => SvgView,
2850    "animate" => SvgAnimate,
2851    "animatemotion" => SvgAnimateMotion,
2852    "animatetransform" => SvgAnimateTransform,
2853    "set" => SvgSet,
2854    "mpath" => SvgMpath,
2855    // Metadata
2856    "meta" => Meta,
2857    "link" => Link,
2858    "script" => Script,
2859    "style" => Style,
2860    "base" => Base,
2861}
2862
2863/// Default render function for builtin HTML elements.
2864/// Delegates to creating a DOM node of the appropriate `NodeType`.
2865fn builtin_render_fn(
2866    def: &ComponentDef,
2867    data: &ComponentDataModel,
2868    _component_map: &ComponentMap,
2869) -> ResultStyledDomRenderDomError {
2870    let node_type = tag_to_node_type(def.id.name.as_str());
2871    let mut dom = Dom::create_node(node_type);
2872    if let Some(text_str) = data.get_default_string("text") {
2873        let prepared = prepare_string(text_str);
2874        if !prepared.is_empty() {
2875            dom = dom.with_children(
2876                alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
2877                    prepared
2878                )]
2879                .into(),
2880            );
2881        }
2882    }
2883    let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut dom, Css::empty()));
2884    r.into()
2885}
2886
2887/// Default compile function for builtin HTML elements.
2888/// Generates `Dom::create_node(NodeType::Div)` style code for the target language.
2889fn builtin_compile_fn(
2890    def: &ComponentDef,
2891    target: &CompileTarget,
2892    data: &ComponentDataModel,
2893    indent: usize,
2894) -> ResultStringCompileError {
2895    let node_type = tag_to_node_type(def.id.name.as_str());
2896    let type_name = format!("{node_type:?}"); // "Div", "Body", "P", etc.
2897    let text = data.get_default_string("text");
2898
2899    let r: Result<AzString, CompileError> = match target {
2900        CompileTarget::Rust => {
2901            text.map_or_else(|| Ok(format!("Dom::create_node(NodeType::{type_name})").into()), |text_str| Ok(format!(
2902                    "Dom::create_node(NodeType::{}).with_children(vec![Dom::create_text_do_not_use_without_block_level_wrapper(\"{}\")])",
2903                    type_name,
2904                    text_str.as_str().replace('\\', "\\\\").replace('"', "\\\"")
2905                ).into()))
2906        }
2907        CompileTarget::C => {
2908            text.map_or_else(|| Ok(format!("AzDom_create{type_name}()").into()), |text_str| Ok(format!(
2909                    "AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{}\"))",
2910                    text_str
2911                        .as_str()
2912                        .replace('\\', "\\\\")
2913                        .replace('"', "\\\"")
2914                )
2915                .into()))
2916        }
2917        CompileTarget::Cpp => Ok(format!("Dom::create_{}()", type_name.to_lowercase()).into()),
2918        CompileTarget::Python => Ok(format!("Dom.create_{}()", type_name.to_lowercase()).into()),
2919    };
2920    r.into()
2921}
2922
2923/// Pushes a `<div>` containing `"field_name: value"` text into the children list.
2924fn push_scalar_field(children: &mut Vec<Dom>, field_name: &str, value: &dyn fmt::Display) {
2925    use crate::dom::{Dom, NodeType};
2926    let text = alloc::format!("{field_name}: {value}");
2927    children.push(
2928        Dom::create_node(NodeType::Div).with_children(
2929            alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
2930                text
2931            )]
2932            .into(),
2933        ),
2934    );
2935}
2936
2937/// Default render function for user-defined (JSON-imported) components.
2938///
2939/// Interprets the `ComponentDef` structure generically:
2940/// 1. Creates a wrapper `<div>` with the component's CSS class
2941/// 2. For each data field, renders content based on type:
2942///    - String fields → text node with current value
2943///    - Bool fields → conditional display
2944///    - `StyledDom` fields → embeds the child DOM subtree
2945///    - StructRef/EnumRef → recursively renders sub-components if found in `ComponentMap`
2946///    - Other scalar fields → text display of the value
2947/// 3. Applies the component's scoped CSS
2948#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
2949#[must_use]
2950pub fn user_defined_render_fn(
2951    def: &ComponentDef,
2952    data: &ComponentDataModel,
2953    component_map: &ComponentMap,
2954) -> ResultStyledDomRenderDomError {
2955    use crate::dom::{Dom, NodeType};
2956    use azul_css::css::Css;
2957
2958    let mut children: Vec<Dom> = Vec::new();
2959
2960    for field in data.fields.as_ref() {
2961        let field_name = field.name.as_str();
2962
2963        // Get the current value from default_value
2964        match &field.default_value {
2965            OptionComponentDefaultValue::None => {
2966                // Required field with no value — skip in preview
2967            }
2968            OptionComponentDefaultValue::Some(default_val) => {
2969                match default_val {
2970                    ComponentDefaultValue::String(s) => {
2971                        let text = s.as_str().trim();
2972                        if !text.is_empty() {
2973                            let label_dom = Dom::create_node(NodeType::Div).with_children(
2974                                alloc::vec![
2975                                    Dom::create_text_do_not_use_without_block_level_wrapper(
2976                                        text.to_string()
2977                                    )
2978                                ]
2979                                .into(),
2980                            );
2981                            children.push(label_dom);
2982                        }
2983                    }
2984                    ComponentDefaultValue::Bool(v) => {
2985                        push_scalar_field(&mut children, field_name, v);
2986                    }
2987                    ComponentDefaultValue::I32(v) => {
2988                        push_scalar_field(&mut children, field_name, v);
2989                    }
2990                    ComponentDefaultValue::I64(v) => {
2991                        push_scalar_field(&mut children, field_name, v);
2992                    }
2993                    ComponentDefaultValue::U32(v) => {
2994                        push_scalar_field(&mut children, field_name, v);
2995                    }
2996                    ComponentDefaultValue::U64(v) => {
2997                        push_scalar_field(&mut children, field_name, v);
2998                    }
2999                    ComponentDefaultValue::Usize(v) => {
3000                        push_scalar_field(&mut children, field_name, v);
3001                    }
3002                    ComponentDefaultValue::F32(v) => {
3003                        push_scalar_field(&mut children, field_name, v);
3004                    }
3005                    ComponentDefaultValue::F64(v) => {
3006                        push_scalar_field(&mut children, field_name, v);
3007                    }
3008                    ComponentDefaultValue::ColorU(c) => {
3009                        let text = alloc::format!(
3010                            "{}: #{:02x}{:02x}{:02x}{:02x}",
3011                            field_name,
3012                            c.r,
3013                            c.g,
3014                            c.b,
3015                            c.a
3016                        );
3017                        children.push(
3018                            Dom::create_node(NodeType::Div).with_children(
3019                                alloc::vec![
3020                                    Dom::create_text_do_not_use_without_block_level_wrapper(text)
3021                                ]
3022                                .into(),
3023                            ),
3024                        );
3025                    }
3026                    ComponentDefaultValue::ComponentInstance(ci) => {
3027                        // Recursively instantiate sub-component from ComponentMap
3028                        if let Some(sub_comp) =
3029                            component_map.get(ci.library.as_str(), ci.component.as_str())
3030                        {
3031                            let sub_data = sub_comp.data_model.clone();
3032                            match (sub_comp.render_fn)(sub_comp, &sub_data, component_map) {
3033                                ResultStyledDomRenderDomError::Ok(_styled_dom) => {
3034                                    // Sub-component rendered successfully — add a placeholder
3035                                    // (StyledDom cannot be directly converted back to Dom)
3036                                    let text = alloc::format!(
3037                                        "[{}:{}]",
3038                                        ci.library.as_str(),
3039                                        ci.component.as_str()
3040                                    );
3041                                    children.push(
3042                                        Dom::create_node(NodeType::Div).with_children(
3043                                            alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into(),
3044                                        ),
3045                                    );
3046                                }
3047                                ResultStyledDomRenderDomError::Err(_) => {
3048                                    // On error, show a placeholder
3049                                    let text = alloc::format!(
3050                                        "[Error rendering {}:{}]",
3051                                        ci.library.as_str(),
3052                                        ci.component.as_str()
3053                                    );
3054                                    children.push(
3055                                        Dom::create_node(NodeType::Div).with_children(
3056                                            alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into(),
3057                                        ),
3058                                    );
3059                                }
3060                            }
3061                        } else {
3062                            let text = alloc::format!(
3063                                "[Unknown component {}:{}]",
3064                                ci.library.as_str(),
3065                                ci.component.as_str()
3066                            );
3067                            children.push(
3068                                Dom::create_node(NodeType::Div).with_children(
3069                                    alloc::vec![
3070                                        Dom::create_text_do_not_use_without_block_level_wrapper(
3071                                            text
3072                                        )
3073                                    ]
3074                                    .into(),
3075                                ),
3076                            );
3077                        }
3078                    }
3079                    ComponentDefaultValue::CallbackFnPointer(name) => {
3080                        // Callbacks are not rendered, just acknowledged
3081                        let text = alloc::format!("{}: fn({})", field_name, name.as_str());
3082                        children.push(
3083                            Dom::create_node(NodeType::Div).with_children(
3084                                alloc::vec![
3085                                    Dom::create_text_do_not_use_without_block_level_wrapper(text)
3086                                ]
3087                                .into(),
3088                            ),
3089                        );
3090                    }
3091                    ComponentDefaultValue::Json(json_str) => {
3092                        let text = alloc::format!("{}: {}", field_name, json_str.as_str());
3093                        children.push(
3094                            Dom::create_node(NodeType::Div).with_children(
3095                                alloc::vec![
3096                                    Dom::create_text_do_not_use_without_block_level_wrapper(text)
3097                                ]
3098                                .into(),
3099                            ),
3100                        );
3101                    }
3102                    ComponentDefaultValue::None => {
3103                        // No default, skip
3104                    }
3105                }
3106            }
3107        }
3108    }
3109
3110    let mut wrapper = Dom::create_node(NodeType::Div);
3111    if !children.is_empty() {
3112        wrapper = wrapper.with_children(children.into());
3113    }
3114
3115    // Apply component CSS
3116    let css = if def.css.as_str().is_empty() {
3117        Css::empty()
3118    } else {
3119        Css::from_string(def.css.clone())
3120    };
3121
3122    let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut wrapper, css));
3123    r.into()
3124}
3125
3126/// Default compile function for user-defined (JSON-imported) components.
3127///
3128/// Generates source code that creates the component's DOM structure for the
3129/// target language. For each data field, emits the appropriate code:
3130/// - String fields → text node creation
3131/// - Scalar fields → formatted display
3132/// - `ComponentInstance` → function call to sub-component's render function
3133/// - `StyledDom` slots → child parameter pass-through
3134#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3135#[must_use]
3136pub fn user_defined_compile_fn(
3137    def: &ComponentDef,
3138    target: &CompileTarget,
3139    data: &ComponentDataModel,
3140    indent: usize,
3141) -> ResultStringCompileError {
3142    let tag = def.id.name.as_str();
3143    let indent_str = " ".repeat(indent * 4);
3144    let inner_indent = " ".repeat((indent + 1) * 4);
3145
3146    let r: Result<AzString, CompileError> = match target {
3147        CompileTarget::Rust => {
3148            let mut lines = Vec::new();
3149            lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3150            lines.push(alloc::format!(
3151                "{indent_str}let mut children: Vec<Dom> = Vec::new();"
3152            ));
3153
3154            for field in data.fields.as_ref() {
3155                let fname = field.name.as_str();
3156                match &field.default_value {
3157                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3158                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3159                        lines.push(alloc::format!(
3160                            "{inner_indent}children.push(Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"));"
3161                        ));
3162                    }
3163                    OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => {
3164                        lines.push(alloc::format!(
3165                            "{inner_indent}children.push(Dom::create_text_do_not_use_without_block_level_wrapper(format!(\"{{}}: {{}}\", \"{fname}\", {b}).as_str()));"
3166                        ));
3167                    }
3168                    OptionComponentDefaultValue::Some(
3169                        ComponentDefaultValue::ComponentInstance(ci),
3170                    ) => {
3171                        let fn_name =
3172                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3173                        lines.push(alloc::format!(
3174                            "{}children.push({}()); // sub-component {}:{}",
3175                            inner_indent,
3176                            fn_name,
3177                            ci.library.as_str(),
3178                            ci.component.as_str()
3179                        ));
3180                    }
3181                    _ => {
3182                        // For other types, generate a placeholder comment
3183                        lines.push(alloc::format!(
3184                            "{}// field '{}': {:?}",
3185                            inner_indent,
3186                            fname,
3187                            field.field_type
3188                        ));
3189                    }
3190                }
3191            }
3192
3193            lines.push(alloc::format!(
3194                "{indent_str}Dom::create_node(NodeType::Div).with_children(children.into())"
3195            ));
3196            Ok(lines.join("\n").into())
3197        }
3198        CompileTarget::C => {
3199            let mut lines = Vec::new();
3200            lines.push(alloc::format!("{indent_str}/* Component: {tag} */"));
3201            lines.push(alloc::format!(
3202                "{indent_str}AzDom root = AzDom_createDiv();"
3203            ));
3204
3205            for field in data.fields.as_ref() {
3206                let fname = field.name.as_str();
3207                match &field.default_value {
3208                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3209                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3210                        lines.push(alloc::format!(
3211                            "{inner_indent}AzDom_addChild(&root, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{escaped}\")));"
3212                        ));
3213                    }
3214                    OptionComponentDefaultValue::Some(
3215                        ComponentDefaultValue::ComponentInstance(ci),
3216                    ) => {
3217                        let fn_name =
3218                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3219                        lines.push(alloc::format!(
3220                            "{inner_indent}AzDom_addChild(&root, {fn_name}());"
3221                        ));
3222                    }
3223                    _ => {
3224                        lines.push(alloc::format!("{inner_indent}/* field '{fname}' */"));
3225                    }
3226                }
3227            }
3228
3229            lines.push(alloc::format!("{indent_str}return root;"));
3230            Ok(lines.join("\n").into())
3231        }
3232        CompileTarget::Cpp => {
3233            let mut lines = Vec::new();
3234            lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3235            lines.push(alloc::format!("{indent_str}auto root = Dom::create_div();"));
3236
3237            for field in data.fields.as_ref() {
3238                let fname = field.name.as_str();
3239                match &field.default_value {
3240                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3241                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3242                        lines.push(alloc::format!(
3243                            "{inner_indent}root.add_child(Dom::create_text_do_not_use_without_block_level_wrapper(String(\"{escaped}\")));"
3244                        ));
3245                    }
3246                    OptionComponentDefaultValue::Some(
3247                        ComponentDefaultValue::ComponentInstance(ci),
3248                    ) => {
3249                        let fn_name =
3250                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3251                        lines.push(alloc::format!("{inner_indent}root.add_child({fn_name}());"));
3252                    }
3253                    _ => {
3254                        lines.push(alloc::format!("{inner_indent}// field '{fname}'"));
3255                    }
3256                }
3257            }
3258
3259            lines.push(alloc::format!("{indent_str}return root;"));
3260            Ok(lines.join("\n").into())
3261        }
3262        CompileTarget::Python => {
3263            let mut lines = Vec::new();
3264            lines.push(alloc::format!("{indent_str}# Component: {tag}"));
3265            lines.push(alloc::format!("{indent_str}root = Dom.create_div()"));
3266
3267            for field in data.fields.as_ref() {
3268                let fname = field.name.as_str();
3269                match &field.default_value {
3270                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3271                        let escaped = s
3272                            .as_str()
3273                            .replace('\\', "\\\\")
3274                            .replace('"', "\\\"")
3275                            .replace('\'', "\\'");
3276                        lines.push(alloc::format!(
3277                            "{inner_indent}root = root.with_child(Dom.create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"))"
3278                        ));
3279                    }
3280                    OptionComponentDefaultValue::Some(
3281                        ComponentDefaultValue::ComponentInstance(ci),
3282                    ) => {
3283                        let fn_name =
3284                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3285                        lines.push(alloc::format!(
3286                            "{inner_indent}root = root.with_child({fn_name}())"
3287                        ));
3288                    }
3289                    _ => {
3290                        lines.push(alloc::format!("{inner_indent}# field '{fname}'"));
3291                    }
3292                }
3293            }
3294
3295            lines.push(alloc::format!("{indent_str}return root"));
3296            Ok(lines.join("\n").into())
3297        }
3298    };
3299    r.into()
3300}
3301
3302/// Create a `ComponentDef` for a builtin HTML element.
3303///
3304/// # Arguments
3305/// * `tag` - HTML tag name (e.g. "button", "div")
3306/// * `display_name` - Human-readable name (e.g. "Button", "Div")
3307/// * `default_text` - Default text content for the preview, or `None` if the element has no text.
3308///   Pass `Some("Button text")` for `<button>`, `Some("")` for text elements like `<span>` that
3309///   accept text but have no meaningful default.
3310/// * `css` - Component-level CSS string. For most builtin elements this is `""` because
3311///   styling comes from `ua_css.rs` and the `SystemStyle`. Components that need extra
3312///   styling (e.g. a future high-level button widget) can pass CSS here.
3313fn builtin_component_def(
3314    tag: &str,
3315    display_name: &str,
3316    default_text: Option<&str>,
3317    css: &str,
3318) -> ComponentDef {
3319    let mut fields = builtin_data_model(tag);
3320    // If a default_text is provided, this element accepts text content
3321    if let Some(text) = default_text {
3322        fields.push(data_field(
3323            "text",
3324            ComponentFieldType::String,
3325            Some(ComponentDefaultValue::String(AzString::from(text))),
3326            "Text content of the element",
3327        ));
3328    }
3329    let model_name = format!("{display_name}Data");
3330    ComponentDef {
3331        id: ComponentId::builtin(tag),
3332        display_name: AzString::from(display_name),
3333        description: AzString::from(format!("HTML <{tag}> element").as_str()),
3334        css: AzString::from(css),
3335        source: ComponentSource::Builtin,
3336        data_model: ComponentDataModel {
3337            name: AzString::from(model_name.as_str()),
3338            description: AzString::from(format!("Data model for <{tag}>").as_str()),
3339            fields: fields.into(),
3340        },
3341        render_fn: builtin_render_fn,
3342        compile_fn: builtin_compile_fn,
3343        render_fn_source: None.into(),
3344        compile_fn_source: None.into(),
3345    }
3346}
3347
3348/// Helper to create a `ComponentDataField` with a rich type
3349fn data_field(
3350    name: &str,
3351    ft: ComponentFieldType,
3352    default: Option<ComponentDefaultValue>,
3353    description: &str,
3354) -> ComponentDataField {
3355    let required = default.is_none();
3356    ComponentDataField {
3357        name: AzString::from(name),
3358        field_type: ft,
3359        default_value: default.map_or_else(
3360            || OptionComponentDefaultValue::None,
3361            OptionComponentDefaultValue::Some,
3362        ),
3363        required,
3364        description: AzString::from(description),
3365    }
3366}
3367
3368/// Returns the tag-specific data model fields for builtin HTML elements.
3369/// These are the component's "main data model" — the attributes that define
3370/// what the component needs as configuration (e.g., `href` for `<a>`,
3371/// `src` for `<img>`). Universal HTML attributes (id, class, style, etc.)
3372/// are NOT included here — they are added separately by the debug server.
3373#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3374fn builtin_data_model(tag: &str) -> Vec<ComponentDataField> {
3375    use ComponentDefaultValue as D;
3376    use ComponentFieldType::{Bool, String, I32};
3377    match tag {
3378        "a" => alloc::vec![
3379            data_field(
3380                "href",
3381                String,
3382                Some(D::String(AzString::from_const_str(""))),
3383                "URL the link points to"
3384            ),
3385            data_field(
3386                "target",
3387                String,
3388                Some(D::String(AzString::from_const_str(""))),
3389                "Where to open the linked document (_blank, _self, _parent, _top)"
3390            ),
3391            data_field(
3392                "rel",
3393                String,
3394                Some(D::String(AzString::from_const_str(""))),
3395                "Relationship between current and linked document"
3396            ),
3397        ],
3398        "img" | "image" => alloc::vec![
3399            data_field("src", String, None, "URL of the image"),
3400            data_field(
3401                "alt",
3402                String,
3403                Some(D::String(AzString::from_const_str(""))),
3404                "Alternative text for the image"
3405            ),
3406            data_field(
3407                "width",
3408                String,
3409                Some(D::String(AzString::from_const_str(""))),
3410                "Width of the image"
3411            ),
3412            data_field(
3413                "height",
3414                String,
3415                Some(D::String(AzString::from_const_str(""))),
3416                "Height of the image"
3417            ),
3418        ],
3419        "form" => alloc::vec![
3420            data_field(
3421                "action",
3422                String,
3423                Some(D::String(AzString::from_const_str(""))),
3424                "URL where form data is submitted"
3425            ),
3426            data_field(
3427                "method",
3428                String,
3429                Some(D::String(AzString::from_const_str("GET"))),
3430                "HTTP method for form submission (GET or POST)"
3431            ),
3432        ],
3433        "label" => alloc::vec![data_field(
3434            "for",
3435            String,
3436            Some(D::String(AzString::from_const_str(""))),
3437            "ID of the form element this label is for"
3438        ),],
3439        "button" => alloc::vec![
3440            data_field(
3441                "type",
3442                String,
3443                Some(D::String(AzString::from_const_str("button"))),
3444                "Button type (button, submit, reset)"
3445            ),
3446            data_field(
3447                "disabled",
3448                Bool,
3449                Some(D::Bool(false)),
3450                "Whether the button is disabled"
3451            ),
3452        ],
3453        "td" | "th" => alloc::vec![
3454            data_field(
3455                "colspan",
3456                I32,
3457                Some(D::I32(1)),
3458                "Number of columns the cell spans"
3459            ),
3460            data_field(
3461                "rowspan",
3462                I32,
3463                Some(D::I32(1)),
3464                "Number of rows the cell spans"
3465            ),
3466        ],
3467        "icon" => alloc::vec![data_field(
3468            "name",
3469            String,
3470            Some(D::String(AzString::from_const_str(""))),
3471            "Icon name"
3472        ),],
3473        "ol" => alloc::vec![
3474            data_field(
3475                "start",
3476                I32,
3477                Some(D::I32(1)),
3478                "Start value for the ordered list"
3479            ),
3480            data_field(
3481                "type",
3482                String,
3483                Some(D::String(AzString::from_const_str("1"))),
3484                "Numbering type (1, A, a, I, i)"
3485            ),
3486        ],
3487        // Form controls
3488        "input" => alloc::vec![
3489            data_field(
3490                "type",
3491                String,
3492                Some(D::String(AzString::from_const_str("text"))),
3493                "Input type (text, password, email, number, checkbox, radio, etc.)"
3494            ),
3495            data_field(
3496                "name",
3497                String,
3498                Some(D::String(AzString::from_const_str(""))),
3499                "Name of the input for form submission"
3500            ),
3501            data_field(
3502                "value",
3503                String,
3504                Some(D::String(AzString::from_const_str(""))),
3505                "Current value of the input"
3506            ),
3507            data_field(
3508                "placeholder",
3509                String,
3510                Some(D::String(AzString::from_const_str(""))),
3511                "Placeholder text"
3512            ),
3513            data_field(
3514                "disabled",
3515                Bool,
3516                Some(D::Bool(false)),
3517                "Whether the input is disabled"
3518            ),
3519            data_field(
3520                "required",
3521                Bool,
3522                Some(D::Bool(false)),
3523                "Whether the input is required"
3524            ),
3525            data_field(
3526                "readonly",
3527                Bool,
3528                Some(D::Bool(false)),
3529                "Whether the input is read-only"
3530            ),
3531            data_field(
3532                "checked",
3533                Bool,
3534                Some(D::Bool(false)),
3535                "Whether the checkbox/radio is checked"
3536            ),
3537            data_field(
3538                "min",
3539                String,
3540                Some(D::String(AzString::from_const_str(""))),
3541                "Minimum value (for number, range, date)"
3542            ),
3543            data_field(
3544                "max",
3545                String,
3546                Some(D::String(AzString::from_const_str(""))),
3547                "Maximum value (for number, range, date)"
3548            ),
3549            data_field(
3550                "step",
3551                String,
3552                Some(D::String(AzString::from_const_str(""))),
3553                "Step increment (for number, range)"
3554            ),
3555            data_field(
3556                "pattern",
3557                String,
3558                Some(D::String(AzString::from_const_str(""))),
3559                "Regex pattern for validation"
3560            ),
3561            data_field(
3562                "maxlength",
3563                String,
3564                Some(D::String(AzString::from_const_str(""))),
3565                "Maximum number of characters"
3566            ),
3567        ],
3568        "select" => alloc::vec![
3569            data_field(
3570                "name",
3571                String,
3572                Some(D::String(AzString::from_const_str(""))),
3573                "Name for form submission"
3574            ),
3575            data_field(
3576                "multiple",
3577                Bool,
3578                Some(D::Bool(false)),
3579                "Whether multiple options can be selected"
3580            ),
3581            data_field(
3582                "disabled",
3583                Bool,
3584                Some(D::Bool(false)),
3585                "Whether the select is disabled"
3586            ),
3587            data_field(
3588                "required",
3589                Bool,
3590                Some(D::Bool(false)),
3591                "Whether selection is required"
3592            ),
3593            data_field(
3594                "size",
3595                String,
3596                Some(D::String(AzString::from_const_str(""))),
3597                "Number of visible options"
3598            ),
3599        ],
3600        "option" => alloc::vec![
3601            data_field(
3602                "value",
3603                String,
3604                Some(D::String(AzString::from_const_str(""))),
3605                "Value submitted with the form"
3606            ),
3607            data_field(
3608                "selected",
3609                Bool,
3610                Some(D::Bool(false)),
3611                "Whether this option is selected"
3612            ),
3613            data_field(
3614                "disabled",
3615                Bool,
3616                Some(D::Bool(false)),
3617                "Whether this option is disabled"
3618            ),
3619        ],
3620        "optgroup" => alloc::vec![
3621            data_field(
3622                "label",
3623                String,
3624                Some(D::String(AzString::from_const_str(""))),
3625                "Label for the option group"
3626            ),
3627            data_field(
3628                "disabled",
3629                Bool,
3630                Some(D::Bool(false)),
3631                "Whether the group is disabled"
3632            ),
3633        ],
3634        "textarea" => alloc::vec![
3635            data_field(
3636                "name",
3637                String,
3638                Some(D::String(AzString::from_const_str(""))),
3639                "Name for form submission"
3640            ),
3641            data_field(
3642                "placeholder",
3643                String,
3644                Some(D::String(AzString::from_const_str(""))),
3645                "Placeholder text"
3646            ),
3647            data_field("rows", I32, Some(D::I32(2)), "Number of visible text lines"),
3648            data_field(
3649                "cols",
3650                I32,
3651                Some(D::I32(20)),
3652                "Visible width in average character widths"
3653            ),
3654            data_field(
3655                "disabled",
3656                Bool,
3657                Some(D::Bool(false)),
3658                "Whether the textarea is disabled"
3659            ),
3660            data_field(
3661                "required",
3662                Bool,
3663                Some(D::Bool(false)),
3664                "Whether content is required"
3665            ),
3666            data_field(
3667                "readonly",
3668                Bool,
3669                Some(D::Bool(false)),
3670                "Whether the textarea is read-only"
3671            ),
3672            data_field(
3673                "maxlength",
3674                String,
3675                Some(D::String(AzString::from_const_str(""))),
3676                "Maximum number of characters"
3677            ),
3678        ],
3679        "fieldset" => alloc::vec![data_field(
3680            "disabled",
3681            Bool,
3682            Some(D::Bool(false)),
3683            "Whether all controls in the fieldset are disabled"
3684        ),],
3685        "output" => alloc::vec![
3686            data_field(
3687                "for",
3688                String,
3689                Some(D::String(AzString::from_const_str(""))),
3690                "IDs of elements that contributed to the output"
3691            ),
3692            data_field(
3693                "name",
3694                String,
3695                Some(D::String(AzString::from_const_str(""))),
3696                "Name for form submission"
3697            ),
3698        ],
3699        "progress" => alloc::vec![
3700            data_field(
3701                "value",
3702                String,
3703                Some(D::String(AzString::from_const_str(""))),
3704                "Current progress value"
3705            ),
3706            data_field(
3707                "max",
3708                String,
3709                Some(D::String(AzString::from_const_str("1"))),
3710                "Maximum value"
3711            ),
3712        ],
3713        "meter" => alloc::vec![
3714            data_field(
3715                "value",
3716                String,
3717                Some(D::String(AzString::from_const_str(""))),
3718                "Current value"
3719            ),
3720            data_field(
3721                "min",
3722                String,
3723                Some(D::String(AzString::from_const_str("0"))),
3724                "Minimum value"
3725            ),
3726            data_field(
3727                "max",
3728                String,
3729                Some(D::String(AzString::from_const_str("1"))),
3730                "Maximum value"
3731            ),
3732            data_field(
3733                "low",
3734                String,
3735                Some(D::String(AzString::from_const_str(""))),
3736                "Low threshold"
3737            ),
3738            data_field(
3739                "high",
3740                String,
3741                Some(D::String(AzString::from_const_str(""))),
3742                "High threshold"
3743            ),
3744            data_field(
3745                "optimum",
3746                String,
3747                Some(D::String(AzString::from_const_str(""))),
3748                "Optimum value"
3749            ),
3750        ],
3751        // Interactive
3752        "details" => alloc::vec![data_field(
3753            "open",
3754            Bool,
3755            Some(D::Bool(false)),
3756            "Whether the details are visible"
3757        ),],
3758        "dialog" => alloc::vec![data_field(
3759            "open",
3760            Bool,
3761            Some(D::Bool(false)),
3762            "Whether the dialog is active and can be interacted with"
3763        ),],
3764        // Embedded content
3765        "audio" | "video" => alloc::vec![
3766            data_field(
3767                "src",
3768                String,
3769                Some(D::String(AzString::from_const_str(""))),
3770                "URL of the media resource"
3771            ),
3772            data_field(
3773                "controls",
3774                Bool,
3775                Some(D::Bool(false)),
3776                "Whether to show playback controls"
3777            ),
3778            data_field(
3779                "autoplay",
3780                Bool,
3781                Some(D::Bool(false)),
3782                "Whether to start playing automatically"
3783            ),
3784            data_field(
3785                "loop",
3786                Bool,
3787                Some(D::Bool(false)),
3788                "Whether to loop playback"
3789            ),
3790            data_field(
3791                "muted",
3792                Bool,
3793                Some(D::Bool(false)),
3794                "Whether audio is muted"
3795            ),
3796            data_field(
3797                "preload",
3798                String,
3799                Some(D::String(AzString::from_const_str("auto"))),
3800                "Preload hint (none, metadata, auto)"
3801            ),
3802        ],
3803        "source" => alloc::vec![
3804            data_field("src", String, None, "URL of the media resource"),
3805            data_field(
3806                "type",
3807                String,
3808                Some(D::String(AzString::from_const_str(""))),
3809                "MIME type of the resource"
3810            ),
3811        ],
3812        "track" => alloc::vec![
3813            data_field("src", String, None, "URL of the track file"),
3814            data_field(
3815                "kind",
3816                String,
3817                Some(D::String(AzString::from_const_str("subtitles"))),
3818                "Kind of text track (subtitles, captions, descriptions, chapters, metadata)"
3819            ),
3820            data_field(
3821                "srclang",
3822                String,
3823                Some(D::String(AzString::from_const_str(""))),
3824                "Language of the track text"
3825            ),
3826            data_field(
3827                "label",
3828                String,
3829                Some(D::String(AzString::from_const_str(""))),
3830                "User-readable title for the track"
3831            ),
3832            data_field(
3833                "default",
3834                Bool,
3835                Some(D::Bool(false)),
3836                "Whether this is the default track"
3837            ),
3838        ],
3839        "canvas" => alloc::vec![
3840            data_field(
3841                "width",
3842                String,
3843                Some(D::String(AzString::from_const_str("300"))),
3844                "Width of the canvas in pixels"
3845            ),
3846            data_field(
3847                "height",
3848                String,
3849                Some(D::String(AzString::from_const_str("150"))),
3850                "Height of the canvas in pixels"
3851            ),
3852        ],
3853        "embed" => alloc::vec![
3854            data_field("src", String, None, "URL of the resource to embed"),
3855            data_field(
3856                "type",
3857                String,
3858                Some(D::String(AzString::from_const_str(""))),
3859                "MIME type of the embedded content"
3860            ),
3861            data_field(
3862                "width",
3863                String,
3864                Some(D::String(AzString::from_const_str(""))),
3865                "Width"
3866            ),
3867            data_field(
3868                "height",
3869                String,
3870                Some(D::String(AzString::from_const_str(""))),
3871                "Height"
3872            ),
3873        ],
3874        "object" => alloc::vec![
3875            data_field(
3876                "data",
3877                String,
3878                Some(D::String(AzString::from_const_str(""))),
3879                "URL of the resource"
3880            ),
3881            data_field(
3882                "type",
3883                String,
3884                Some(D::String(AzString::from_const_str(""))),
3885                "MIME type of the resource"
3886            ),
3887            data_field(
3888                "width",
3889                String,
3890                Some(D::String(AzString::from_const_str(""))),
3891                "Width"
3892            ),
3893            data_field(
3894                "height",
3895                String,
3896                Some(D::String(AzString::from_const_str(""))),
3897                "Height"
3898            ),
3899        ],
3900        "param" => alloc::vec![
3901            data_field("name", String, None, "Name of the parameter"),
3902            data_field(
3903                "value",
3904                String,
3905                Some(D::String(AzString::from_const_str(""))),
3906                "Value of the parameter"
3907            ),
3908        ],
3909        "area" => alloc::vec![
3910            data_field(
3911                "shape",
3912                String,
3913                Some(D::String(AzString::from_const_str("default"))),
3914                "Shape of the area (default, rect, circle, poly)"
3915            ),
3916            data_field(
3917                "coords",
3918                String,
3919                Some(D::String(AzString::from_const_str(""))),
3920                "Coordinates of the area"
3921            ),
3922            data_field(
3923                "href",
3924                String,
3925                Some(D::String(AzString::from_const_str(""))),
3926                "URL for the area link"
3927            ),
3928            data_field(
3929                "alt",
3930                String,
3931                Some(D::String(AzString::from_const_str(""))),
3932                "Alternative text"
3933            ),
3934            data_field(
3935                "target",
3936                String,
3937                Some(D::String(AzString::from_const_str(""))),
3938                "Where to open the linked document"
3939            ),
3940        ],
3941        "map" => alloc::vec![data_field(
3942            "name",
3943            String,
3944            None,
3945            "Name of the image map (referenced by usemap)"
3946        ),],
3947        // Inline semantics with special attributes
3948        "time" => alloc::vec![data_field(
3949            "datetime",
3950            String,
3951            Some(D::String(AzString::from_const_str(""))),
3952            "Machine-readable date/time value"
3953        ),],
3954        "data" => alloc::vec![data_field(
3955            "value",
3956            String,
3957            Some(D::String(AzString::from_const_str(""))),
3958            "Machine-readable value"
3959        ),],
3960        "abbr" | "acronym" | "dfn" => alloc::vec![data_field(
3961            "title",
3962            String,
3963            Some(D::String(AzString::from_const_str(""))),
3964            "Full expansion or definition"
3965        ),],
3966        "q" | "blockquote" => alloc::vec![data_field(
3967            "cite",
3968            String,
3969            Some(D::String(AzString::from_const_str(""))),
3970            "URL of the source of the quotation"
3971        ),],
3972        "del" | "ins" => alloc::vec![
3973            data_field(
3974                "cite",
3975                String,
3976                Some(D::String(AzString::from_const_str(""))),
3977                "URL explaining the change"
3978            ),
3979            data_field(
3980                "datetime",
3981                String,
3982                Some(D::String(AzString::from_const_str(""))),
3983                "Date/time of the change"
3984            ),
3985        ],
3986        "bdo" => alloc::vec![data_field(
3987            "dir",
3988            String,
3989            Some(D::String(AzString::from_const_str("ltr"))),
3990            "Text direction (ltr, rtl)"
3991        ),],
3992        "col" | "colgroup" => alloc::vec![data_field(
3993            "span",
3994            I32,
3995            Some(D::I32(1)),
3996            "Number of columns the element spans"
3997        ),],
3998        // Metadata
3999        "meta" => alloc::vec![
4000            data_field(
4001                "name",
4002                String,
4003                Some(D::String(AzString::from_const_str(""))),
4004                "Metadata name"
4005            ),
4006            data_field(
4007                "content",
4008                String,
4009                Some(D::String(AzString::from_const_str(""))),
4010                "Metadata value"
4011            ),
4012            data_field(
4013                "charset",
4014                String,
4015                Some(D::String(AzString::from_const_str(""))),
4016                "Character encoding"
4017            ),
4018            data_field(
4019                "http-equiv",
4020                String,
4021                Some(D::String(AzString::from_const_str(""))),
4022                "HTTP header equivalent"
4023            ),
4024        ],
4025        "link" => alloc::vec![
4026            data_field("rel", String, None, "Relationship type"),
4027            data_field(
4028                "href",
4029                String,
4030                Some(D::String(AzString::from_const_str(""))),
4031                "URL of the linked resource"
4032            ),
4033            data_field(
4034                "type",
4035                String,
4036                Some(D::String(AzString::from_const_str(""))),
4037                "MIME type of the linked resource"
4038            ),
4039        ],
4040        "script" => alloc::vec![
4041            data_field(
4042                "src",
4043                String,
4044                Some(D::String(AzString::from_const_str(""))),
4045                "URL of external script"
4046            ),
4047            data_field(
4048                "type",
4049                String,
4050                Some(D::String(AzString::from_const_str(""))),
4051                "MIME type or module"
4052            ),
4053            data_field(
4054                "async",
4055                Bool,
4056                Some(D::Bool(false)),
4057                "Execute asynchronously"
4058            ),
4059            data_field(
4060                "defer",
4061                Bool,
4062                Some(D::Bool(false)),
4063                "Defer execution until page load"
4064            ),
4065        ],
4066        "style" => alloc::vec![data_field(
4067            "type",
4068            String,
4069            Some(D::String(AzString::from_const_str("text/css"))),
4070            "MIME type of the style sheet"
4071        ),],
4072        "base" => alloc::vec![
4073            data_field(
4074                "href",
4075                String,
4076                Some(D::String(AzString::from_const_str(""))),
4077                "Base URL for relative URLs"
4078            ),
4079            data_field(
4080                "target",
4081                String,
4082                Some(D::String(AzString::from_const_str(""))),
4083                "Default target for hyperlinks"
4084            ),
4085        ],
4086        _ => alloc::vec![],
4087    }
4088}
4089
4090impl Default for ComponentMap {
4091    /// Returns an empty `ComponentMap` with no libraries.
4092    ///
4093    /// Use `AppConfig::create()` (which registers the 52 builtins via
4094    /// `register_builtin_components`) followed by `ComponentMap::from_libraries()`
4095    /// to get a fully-populated map.
4096    fn default() -> Self {
4097        Self {
4098            libraries: ComponentLibraryVec::from_const_slice(&[]),
4099        }
4100    }
4101}
4102
4103impl ComponentMap {
4104    #[must_use]
4105    pub fn create() -> Self {
4106        Self::default()
4107    }
4108
4109    /// Create a `ComponentMap` with the 52 built-in HTML element components pre-registered.
4110    #[must_use]
4111    pub fn with_builtin() -> Self {
4112        Self {
4113            libraries: alloc::vec![register_builtin_components()].into(),
4114        }
4115    }
4116
4117    /// Build a `ComponentMap` from the libraries stored in an `AppConfig`.
4118    ///
4119    /// The `component_libraries` field already contains builtins (registered in
4120    /// `AppConfig::create()`) plus any user-added libraries.  No merging needed —
4121    /// `add_component_library` / `add_component` handle insertion at registration time.
4122    #[must_use]
4123    pub fn from_libraries(libs: &ComponentLibraryVec) -> Self {
4124        Self {
4125            libraries: libs.clone(),
4126        }
4127    }
4128}
4129
4130/// Convert XML attributes to a `ComponentDataModel` by cloning the component's
4131/// base data model and overriding field defaults with values from the XML attributes.
4132///
4133/// This is the bridge between the XML parsing layer (key-value string pairs)
4134/// and the typed component data model. For each field in the base model,
4135/// if a matching XML attribute exists, its string value is set as the new default.
4136///
4137/// # Arguments
4138/// * `base_model` - The component's data model template (from `ComponentDef::data_model`)
4139/// * `xml_attributes` - The XML node's attribute map
4140/// * `text_content` - Optional text content from child text nodes
4141///
4142/// # Returns
4143/// A cloned `ComponentDataModel` with overridden defaults
4144fn xml_attrs_to_data_model(
4145    base_model: &ComponentDataModel,
4146    xml_attributes: &XmlAttributeMap,
4147    text_content: Option<&str>,
4148) -> ComponentDataModel {
4149    let mut model = base_model.clone();
4150
4151    // Override defaults from XML attributes
4152    let mut fields_vec = core::mem::replace(
4153        &mut model.fields,
4154        ComponentDataFieldVec::from_const_slice(&[]),
4155    )
4156    .into_library_owned_vec();
4157
4158    for field in &mut fields_vec {
4159        if let Some(attr_value) = xml_attributes.get_key(field.name.as_str()) {
4160            // Override the default_value with the XML attribute's string value
4161            field.default_value = OptionComponentDefaultValue::Some(ComponentDefaultValue::String(
4162                attr_value.clone(),
4163            ));
4164        }
4165    }
4166
4167    model.fields = ComponentDataFieldVec::from_vec(fields_vec);
4168
4169    // Handle text content — set the "text" field if present
4170    if let Some(text) = text_content {
4171        let prepared = prepare_string(text);
4172        if !prepared.is_empty() {
4173            model = model.with_default(
4174                "text",
4175                ComponentDefaultValue::String(AzString::from(prepared.as_str())),
4176            );
4177        }
4178    }
4179
4180    model
4181}
4182
4183// ============================================================================
4184// Structural builtin components: if, for, map
4185// ============================================================================
4186
4187/// `builtin:if` — conditional rendering.
4188/// Takes `condition: Bool`, `then: StyledDom`, and optionally `else: StyledDom`.
4189fn builtin_if_component() -> ComponentDef {
4190    ComponentDef {
4191        id: ComponentId::builtin("if"),
4192        display_name: AzString::from_const_str("If"),
4193        description: AzString::from_const_str("Conditional rendering: shows 'then' if condition is true, else shows 'else' (if provided)."),
4194        css: AzString::from_const_str(""),
4195        source: ComponentSource::Builtin,
4196        data_model: ComponentDataModel {
4197            name: AzString::from_const_str("IfData"),
4198            description: AzString::from_const_str("Data for conditional rendering"),
4199            fields: alloc::vec![
4200                data_field("condition", ComponentFieldType::Bool, Some(ComponentDefaultValue::Bool(false)), "The boolean condition to evaluate"),
4201            ].into(),
4202        },
4203        render_fn: builtin_if_render_fn,
4204        compile_fn: builtin_if_compile_fn,
4205        render_fn_source: None.into(),
4206        compile_fn_source: None.into(),
4207    }
4208}
4209
4210fn builtin_if_render_fn(
4211    _comp: &ComponentDef,
4212    data_model: &ComponentDataModel,
4213    _component_map: &ComponentMap,
4214) -> ResultStyledDomRenderDomError {
4215    // Evaluate the condition field
4216    let condition = data_model
4217        .fields
4218        .iter()
4219        .find(|f| f.name.as_str() == "condition")
4220        .and_then(|f| match &f.default_value {
4221            OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => Some(*b),
4222            _ => None,
4223        })
4224        .unwrap_or(false);
4225
4226    let label = if condition {
4227        "if: true (then branch)"
4228    } else {
4229        "if: false (else branch)"
4230    };
4231    let mut dom = Dom::create_node(NodeType::Div).with_children(
4232        alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
4233            label
4234        )]
4235        .into(),
4236    );
4237    let css = Css::empty();
4238    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4239}
4240
4241fn builtin_if_compile_fn(
4242    _comp: &ComponentDef,
4243    target: &CompileTarget,
4244    _data: &ComponentDataModel,
4245    _indent: usize,
4246) -> ResultStringCompileError {
4247    match target {
4248        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4249            "if data.condition {\n    // then branch\n    Dom::create_div()\n} else {\n    // else branch\n    Dom::create_div()\n}"
4250        )),
4251        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4252            "if (data.condition) {\n    // then branch\n    AzDom_createDiv();\n} else {\n    // else branch\n    AzDom_createDiv();\n}"
4253        )),
4254        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4255            "if (data.condition) {\n    // then branch\n    Dom::create_div();\n} else {\n    // else branch\n    Dom::create_div();\n}"
4256        )),
4257        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4258            "if data.condition:\n    # then branch\n    Dom.create_div()\nelse:\n    # else branch\n    Dom.create_div()"
4259        )),
4260    }
4261}
4262
4263/// `builtin:for` — iterative rendering.
4264/// Takes `count: U32` (number of iterations), renders children N times.
4265fn builtin_for_component() -> ComponentDef {
4266    ComponentDef {
4267        id: ComponentId::builtin("for"),
4268        display_name: AzString::from_const_str("For Loop"),
4269        description: AzString::from_const_str(
4270            "Iterative rendering: repeats children 'count' times.",
4271        ),
4272        css: AzString::from_const_str(""),
4273        source: ComponentSource::Builtin,
4274        data_model: ComponentDataModel {
4275            name: AzString::from_const_str("ForData"),
4276            description: AzString::from_const_str("Data for iterative rendering"),
4277            fields: alloc::vec![data_field(
4278                "count",
4279                ComponentFieldType::U32,
4280                Some(ComponentDefaultValue::U32(3)),
4281                "Number of iterations"
4282            ),]
4283            .into(),
4284        },
4285        render_fn: builtin_for_render_fn,
4286        compile_fn: builtin_for_compile_fn,
4287        render_fn_source: None.into(),
4288        compile_fn_source: None.into(),
4289    }
4290}
4291
4292fn builtin_for_render_fn(
4293    _comp: &ComponentDef,
4294    data_model: &ComponentDataModel,
4295    _component_map: &ComponentMap,
4296) -> ResultStyledDomRenderDomError {
4297    let count = data_model
4298        .fields
4299        .iter()
4300        .find(|f| f.name.as_str() == "count")
4301        .and_then(|f| match &f.default_value {
4302            OptionComponentDefaultValue::Some(ComponentDefaultValue::U32(n)) => Some(*n),
4303            _ => None,
4304        })
4305        .unwrap_or(3);
4306
4307    let mut items: Vec<Dom> = Vec::new();
4308    for i in 0..count {
4309        items.push(
4310            Dom::create_node(NodeType::Div).with_children(
4311                alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
4312                    alloc::format!("Item {i}")
4313                )]
4314                .into(),
4315            ),
4316        );
4317    }
4318    let mut dom = Dom::create_node(NodeType::Div).with_children(items.into());
4319    let css = Css::empty();
4320    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4321}
4322
4323fn builtin_for_compile_fn(
4324    _comp: &ComponentDef,
4325    target: &CompileTarget,
4326    _data: &ComponentDataModel,
4327    _indent: usize,
4328) -> ResultStringCompileError {
4329    match target {
4330        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4331            "let mut children = Vec::new();\nfor i in 0..data.count {\n    children.push(Dom::create_div());\n}\nDom::create_div().with_children(children)"
4332        )),
4333        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4334            "AzDom container = AzDom_createDiv();\nfor (uint32_t i = 0; i < data.count; i++) {\n    AzDom_addChild(&container, AzDom_createDiv());\n}"
4335        )),
4336        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4337            "auto container = Dom::create_div();\nfor (uint32_t i = 0; i < data.count; i++) {\n    container.add_child(Dom::create_div());\n}"
4338        )),
4339        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4340            "container = Dom.create_div()\nfor i in range(data.count):\n    container = container.with_child(Dom.create_div())"
4341        )),
4342    }
4343}
4344
4345/// `builtin:map` — map data to DOM.
4346/// Takes `data_json: String` (JSON array) + maps each element.
4347fn builtin_map_component() -> ComponentDef {
4348    ComponentDef {
4349        id: ComponentId::builtin("map"),
4350        display_name: AzString::from_const_str("Map"),
4351        description: AzString::from_const_str(
4352            "Map data to DOM: applies a template to each item in a collection.",
4353        ),
4354        css: AzString::from_const_str(""),
4355        source: ComponentSource::Builtin,
4356        data_model: ComponentDataModel {
4357            name: AzString::from_const_str("MapData"),
4358            description: AzString::from_const_str("Data for map rendering"),
4359            fields: alloc::vec![data_field(
4360                "data_json",
4361                ComponentFieldType::String,
4362                Some(ComponentDefaultValue::String(AzString::from_const_str(
4363                    "[]"
4364                ))),
4365                "JSON array of items to map over"
4366            ),]
4367            .into(),
4368        },
4369        render_fn: builtin_map_render_fn,
4370        compile_fn: builtin_map_compile_fn,
4371        render_fn_source: None.into(),
4372        compile_fn_source: None.into(),
4373    }
4374}
4375
4376fn builtin_map_render_fn(
4377    _comp: &ComponentDef,
4378    data_model: &ComponentDataModel,
4379    _component_map: &ComponentMap,
4380) -> ResultStyledDomRenderDomError {
4381    // For now, render a placeholder — actual mapping requires callback support
4382    let data_str = data_model
4383        .fields
4384        .iter()
4385        .find(|f| f.name.as_str() == "data_json")
4386        .and_then(|f| match &f.default_value {
4387            OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
4388                Some(s.as_str().to_string())
4389            }
4390            _ => None,
4391        })
4392        .unwrap_or_else(|| "[]".to_string());
4393
4394    let label = alloc::format!("map: data_json={data_str}");
4395    let mut dom = Dom::create_node(NodeType::Div).with_children(
4396        alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
4397            label
4398        )]
4399        .into(),
4400    );
4401    let css = Css::empty();
4402    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4403}
4404
4405fn builtin_map_compile_fn(
4406    _comp: &ComponentDef,
4407    target: &CompileTarget,
4408    _data: &ComponentDataModel,
4409    _indent: usize,
4410) -> ResultStringCompileError {
4411    match target {
4412        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4413            "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)"
4414        )),
4415        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4416            "// Parse data.data_json and map each item\nAzDom container = AzDom_createDiv();\n// TODO: iterate parsed JSON array"
4417        )),
4418        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4419            "// Parse data.data_json and map each item\nauto container = Dom::create_div();\n// TODO: iterate parsed JSON array"
4420        )),
4421        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4422            "import json\nitems = json.loads(data.data_json)\ncontainer = Dom.create_div()\nfor item in items:\n    container = container.with_child(Dom.create_div())"
4423        )),
4424    }
4425}
4426
4427/// Register the 52 built-in HTML element components.
4428///
4429/// This is an `extern "C"` function pointer compatible with
4430/// `RegisterComponentLibraryFnType`, so it can be passed directly to
4431/// `AppConfig::add_component_library()`.
4432///
4433/// Called once during `AppConfig::create()` — the framework dogfoods
4434/// its own component registration system for builtins.
4435#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
4436#[must_use]
4437pub extern "C" fn register_builtin_components() -> ComponentLibrary {
4438    ComponentLibrary {
4439        name: AzString::from_const_str("builtin"),
4440        version: AzString::from_const_str("1.0.0"),
4441        description: AzString::from_const_str("Built-in HTML elements"),
4442        exportable: false,
4443        modifiable: false,
4444        data_models: Vec::new().into(),
4445        enum_models: Vec::new().into(),
4446        components: alloc::vec![
4447            // Structural
4448            builtin_component_def("html", "HTML", None, ""),
4449            builtin_component_def("head", "Head", None, ""),
4450            builtin_component_def("title", "Title", Some(""), ""),
4451            builtin_component_def("body", "Body", None, ""),
4452            // Block-level
4453            builtin_component_def("div", "Div", None, ""),
4454            builtin_component_def("header", "Header", None, ""),
4455            builtin_component_def("footer", "Footer", None, ""),
4456            builtin_component_def("section", "Section", None, ""),
4457            builtin_component_def("article", "Article", None, ""),
4458            builtin_component_def("aside", "Aside", None, ""),
4459            builtin_component_def("nav", "Nav", None, ""),
4460            builtin_component_def("main", "Main", None, ""),
4461            builtin_component_def("figure", "Figure", None, ""),
4462            builtin_component_def("figcaption", "Figure Caption", Some(""), ""),
4463            builtin_component_def("address", "Address", Some(""), ""),
4464            builtin_component_def("details", "Details", None, ""),
4465            builtin_component_def("summary", "Summary", Some("Details"), ""),
4466            builtin_component_def("dialog", "Dialog", None, ""),
4467            // Headings — default text is the heading level name so preview is visible
4468            builtin_component_def("h1", "Heading 1", Some("Heading 1"), ""),
4469            builtin_component_def("h2", "Heading 2", Some("Heading 2"), ""),
4470            builtin_component_def("h3", "Heading 3", Some("Heading 3"), ""),
4471            builtin_component_def("h4", "Heading 4", Some("Heading 4"), ""),
4472            builtin_component_def("h5", "Heading 5", Some("Heading 5"), ""),
4473            builtin_component_def("h6", "Heading 6", Some("Heading 6"), ""),
4474            // Text content
4475            builtin_component_def("p", "Paragraph", Some("Paragraph text"), ""),
4476            builtin_component_def("span", "Span", Some(""), ""),
4477            builtin_component_def("pre", "Preformatted", Some(""), ""),
4478            builtin_component_def("code", "Code", Some(""), ""),
4479            builtin_component_def("blockquote", "Blockquote", Some(""), ""),
4480            builtin_component_def("br", "Line Break", None, ""),
4481            builtin_component_def("hr", "Horizontal Rule", None, ""),
4482            builtin_component_def("pagebreak", "Page Break", None, ""),
4483            builtin_component_def("icon", "Icon", Some(""), ""),
4484            // Lists
4485            builtin_component_def("ul", "Unordered List", None, ""),
4486            builtin_component_def("ol", "Ordered List", None, ""),
4487            builtin_component_def("li", "List Item", Some("List item"), ""),
4488            builtin_component_def("dl", "Description List", None, ""),
4489            builtin_component_def("dt", "Description Term", Some(""), ""),
4490            builtin_component_def("dd", "Description Details", Some(""), ""),
4491            builtin_component_def("menu", "Menu", None, ""),
4492            builtin_component_def("menuitem", "Menu Item", Some(""), ""),
4493            builtin_component_def("dir", "Directory List", None, ""),
4494            // Tables
4495            builtin_component_def("table", "Table", None, ""),
4496            builtin_component_def("caption", "Table Caption", Some(""), ""),
4497            builtin_component_def("thead", "Table Head", None, ""),
4498            builtin_component_def("tbody", "Table Body", None, ""),
4499            builtin_component_def("tfoot", "Table Foot", None, ""),
4500            builtin_component_def("tr", "Table Row", None, ""),
4501            builtin_component_def("th", "Table Header Cell", Some("Header"), ""),
4502            builtin_component_def("td", "Table Data Cell", Some(""), ""),
4503            builtin_component_def("colgroup", "Column Group", None, ""),
4504            builtin_component_def("col", "Column", None, ""),
4505            // Inline
4506            builtin_component_def("a", "Link", Some("Link text"), ""),
4507            builtin_component_def("strong", "Strong", Some(""), ""),
4508            builtin_component_def("em", "Emphasis", Some(""), ""),
4509            builtin_component_def("b", "Bold", Some(""), ""),
4510            builtin_component_def("i", "Italic", Some(""), ""),
4511            builtin_component_def("u", "Underline", Some(""), ""),
4512            builtin_component_def("s", "Strikethrough", Some(""), ""),
4513            builtin_component_def("small", "Small", Some(""), ""),
4514            builtin_component_def("mark", "Mark", Some(""), ""),
4515            builtin_component_def("del", "Deleted Text", Some(""), ""),
4516            builtin_component_def("ins", "Inserted Text", Some(""), ""),
4517            builtin_component_def("sub", "Subscript", Some(""), ""),
4518            builtin_component_def("sup", "Superscript", Some(""), ""),
4519            builtin_component_def("samp", "Sample Output", Some(""), ""),
4520            builtin_component_def("kbd", "Keyboard Input", Some(""), ""),
4521            builtin_component_def("var", "Variable", Some(""), ""),
4522            builtin_component_def("cite", "Citation", Some(""), ""),
4523            builtin_component_def("dfn", "Definition", Some(""), ""),
4524            builtin_component_def("abbr", "Abbreviation", Some(""), ""),
4525            builtin_component_def("acronym", "Acronym", Some(""), ""),
4526            builtin_component_def("q", "Inline Quote", Some(""), ""),
4527            builtin_component_def("time", "Time", Some(""), ""),
4528            builtin_component_def("big", "Big", Some(""), ""),
4529            builtin_component_def("bdo", "BiDi Override", Some(""), ""),
4530            builtin_component_def("bdi", "BiDi Isolate", Some(""), ""),
4531            builtin_component_def("wbr", "Word Break Opportunity", None, ""),
4532            builtin_component_def("ruby", "Ruby Annotation", None, ""),
4533            builtin_component_def("rt", "Ruby Text", Some(""), ""),
4534            builtin_component_def("rtc", "Ruby Text Container", None, ""),
4535            builtin_component_def("rp", "Ruby Parenthesis", Some(""), ""),
4536            builtin_component_def("data", "Data", Some(""), ""),
4537            // Forms
4538            builtin_component_def("form", "Form", None, ""),
4539            builtin_component_def("fieldset", "Field Set", None, ""),
4540            builtin_component_def("legend", "Legend", Some("Legend"), ""),
4541            builtin_component_def("label", "Label", Some("Label"), ""),
4542            builtin_component_def("input", "Input", None, ""),
4543            builtin_component_def("button", "Button", Some("Button text"), ""),
4544            builtin_component_def("select", "Select", None, ""),
4545            builtin_component_def("optgroup", "Option Group", None, ""),
4546            builtin_component_def("option", "Option", Some(""), ""),
4547            builtin_component_def("textarea", "Text Area", Some(""), ""),
4548            builtin_component_def("output", "Output", Some(""), ""),
4549            builtin_component_def("progress", "Progress", None, ""),
4550            builtin_component_def("meter", "Meter", None, ""),
4551            builtin_component_def("datalist", "Data List", None, ""),
4552            // Embedded content
4553            builtin_component_def("canvas", "Canvas", None, ""),
4554            builtin_component_def("object", "Object", None, ""),
4555            builtin_component_def("param", "Parameter", None, ""),
4556            builtin_component_def("embed", "Embed", None, ""),
4557            builtin_component_def("audio", "Audio", None, ""),
4558            builtin_component_def("video", "Video", None, ""),
4559            builtin_component_def("source", "Source", None, ""),
4560            builtin_component_def("track", "Track", None, ""),
4561            builtin_component_def("map", "Image Map", None, ""),
4562            builtin_component_def("area", "Map Area", None, ""),
4563            builtin_component_def("svg", "SVG", None, ""),
4564            // Metadata
4565            builtin_component_def("meta", "Meta", None, ""),
4566            builtin_component_def("link", "Link (Resource)", None, ""),
4567            builtin_component_def("script", "Script", Some(""), ""),
4568            builtin_component_def("style", "Style", Some(""), ""),
4569            builtin_component_def("base", "Base URL", None, ""),
4570            // Structural control-flow builtins (F1-F3)
4571            builtin_if_component(),
4572            builtin_for_component(),
4573            builtin_map_component(),
4574        ]
4575        .into(),
4576    }
4577}
4578
4579// ============================================================================
4580// End new component system types
4581// ============================================================================
4582
4583/// Wrapper for the XML parser - necessary to easily create a Dom from
4584/// XML without putting an XML solver into `azul-core`.
4585#[derive(Debug, Default)]
4586pub struct DomXml {
4587    pub parsed_dom: StyledDom,
4588}
4589
4590impl DomXml {
4591    /// Convenience function, only available in tests, useful for quickly writing UI tests.
4592    /// Wraps the XML string in the required `<app></app>` braces, panics if the XML couldn't be
4593    /// parsed.
4594    ///
4595    /// ## Example
4596    ///
4597    /// ```rust,ignore
4598    /// # use azul::dom::Dom;
4599    /// # use azul::xml::DomXml;
4600    /// let dom = DomXml::mock("<div id='test' />");
4601    /// dom.assert_eq(Dom::create_div().with_id("test"));
4602    /// ```
4603    ///
4604    /// # Panics
4605    ///
4606    /// Panics if the rendered DOM does not equal `other` (this is a test-only
4607    /// assertion helper).
4608    #[cfg(test)]
4609    pub fn assert_eq(self, other: StyledDom) {
4610        let mut body = Dom::create_body();
4611        let mut fixed = StyledDom::create(&mut body, Css::empty());
4612        fixed.append_child(other);
4613        assert!(
4614            !(self.parsed_dom != fixed),
4615            "\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: \
4616                 ----------\r\n{}\r\n",
4617            self.parsed_dom.get_html_string("", "", true),
4618            fixed.get_html_string("", "", true)
4619        );
4620    }
4621
4622    #[must_use]
4623    pub fn into_styled_dom(self) -> StyledDom {
4624        self.into()
4625    }
4626}
4627
4628impl From<DomXml> for StyledDom {
4629    fn from(val: DomXml) -> Self {
4630        val.parsed_dom
4631    }
4632}
4633
4634/// Represents a child of an XML node - either an element or text
4635#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4636#[repr(C, u8)]
4637pub enum XmlNodeChild {
4638    /// A text node
4639    Text(AzString),
4640    /// An element node
4641    Element(XmlNode),
4642}
4643
4644impl_option!(
4645    XmlNodeChild,
4646    OptionXmlNodeChild,
4647    copy = false,
4648    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4649);
4650
4651impl XmlNodeChild {
4652    /// Get the text content if this is a text node
4653    #[must_use]
4654    pub fn as_text(&self) -> Option<&str> {
4655        match self {
4656            Self::Text(s) => Some(s.as_str()),
4657            Self::Element(_) => None,
4658        }
4659    }
4660
4661    /// Get the element if this is an element node
4662    #[must_use]
4663    pub const fn as_element(&self) -> Option<&XmlNode> {
4664        match self {
4665            Self::Text(_) => None,
4666            Self::Element(node) => Some(node),
4667        }
4668    }
4669
4670    /// Get the element mutably if this is an element node
4671    pub const fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
4672        match self {
4673            Self::Text(_) => None,
4674            Self::Element(node) => Some(node),
4675        }
4676    }
4677}
4678
4679impl_vec!(
4680    XmlNodeChild,
4681    XmlNodeChildVec,
4682    XmlNodeChildVecDestructor,
4683    XmlNodeChildVecDestructorType,
4684    XmlNodeChildVecSlice,
4685    OptionXmlNodeChild
4686);
4687impl_vec_mut!(XmlNodeChild, XmlNodeChildVec);
4688impl_vec_debug!(XmlNodeChild, XmlNodeChildVec);
4689impl_vec_partialeq!(XmlNodeChild, XmlNodeChildVec);
4690impl_vec_eq!(XmlNodeChild, XmlNodeChildVec);
4691impl_vec_partialord!(XmlNodeChild, XmlNodeChildVec);
4692impl_vec_ord!(XmlNodeChild, XmlNodeChildVec);
4693impl_vec_hash!(XmlNodeChild, XmlNodeChildVec);
4694impl_vec_clone!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor);
4695
4696/// Represents one XML node tag
4697#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4698#[repr(C)]
4699pub struct XmlNode {
4700    /// Type of the node
4701    pub node_type: XmlTagName,
4702    /// Attributes of an XML node (note: not yet filtered and / or broken into function arguments!)
4703    pub attributes: XmlAttributeMap,
4704    /// Direct children of this node (can be text or element nodes)
4705    pub children: XmlNodeChildVec,
4706}
4707
4708impl_option!(
4709    XmlNode,
4710    OptionXmlNode,
4711    copy = false,
4712    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4713);
4714
4715impl XmlNode {
4716    pub fn create<I: Into<XmlTagName>>(node_type: I) -> Self {
4717        Self {
4718            node_type: node_type.into(),
4719            ..Default::default()
4720        }
4721    }
4722    #[must_use]
4723    pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
4724        Self {
4725            children: v.into(),
4726            ..self
4727        }
4728    }
4729
4730    /// Get all text content concatenated from direct children
4731    #[must_use]
4732    pub fn get_text_content(&self) -> String {
4733        self.children
4734            .as_ref()
4735            .iter()
4736            .filter_map(|child| child.as_text())
4737            .collect::<Vec<_>>()
4738            .join("")
4739    }
4740
4741    /// Check if this node has only text children (no element children)
4742    #[must_use]
4743    pub fn has_only_text_children(&self) -> bool {
4744        self.children
4745            .as_ref()
4746            .iter()
4747            .all(|child| matches!(child, XmlNodeChild::Text(_)))
4748    }
4749}
4750
4751impl_vec!(
4752    XmlNode,
4753    XmlNodeVec,
4754    XmlNodeVecDestructor,
4755    XmlNodeVecDestructorType,
4756    XmlNodeVecSlice,
4757    OptionXmlNode
4758);
4759impl_vec_mut!(XmlNode, XmlNodeVec);
4760impl_vec_debug!(XmlNode, XmlNodeVec);
4761impl_vec_partialeq!(XmlNode, XmlNodeVec);
4762impl_vec_eq!(XmlNode, XmlNodeVec);
4763impl_vec_partialord!(XmlNode, XmlNodeVec);
4764impl_vec_ord!(XmlNode, XmlNodeVec);
4765impl_vec_hash!(XmlNode, XmlNodeVec);
4766impl_vec_clone!(XmlNode, XmlNodeVec, XmlNodeVecDestructor);
4767
4768#[derive(Debug, Clone, PartialEq)]
4769#[repr(C, u8)]
4770pub enum DomXmlParseError {
4771    /// No `<html></html>` node component present
4772    NoHtmlNode,
4773    /// Multiple `<html>` nodes
4774    MultipleHtmlRootNodes,
4775    /// No ´<body></body>´ node in the root HTML
4776    NoBodyInHtml,
4777    /// The DOM can only have one <body> node, not multiple.
4778    MultipleBodyNodes,
4779    /// Note: Sadly, the error type can only be a string because xmlparser
4780    /// returns all errors as strings. There is an open PR to fix
4781    /// this deficiency, but since the XML parsing is only needed for
4782    /// hot-reloading and compiling, it doesn't matter that much.
4783    Xml(XmlError),
4784    /// Invalid hierarchy close tags, i.e `<app></p></app>`
4785    MalformedHierarchy(MalformedHierarchyError),
4786    /// A component raised an error while rendering the DOM - holds the component name + error
4787    /// string
4788    RenderDom(RenderDomError),
4789    /// Something went wrong while parsing an XML component
4790    Component(ComponentParseError),
4791    /// Error parsing global CSS in head node
4792    Css(CssParseErrorOwned),
4793}
4794
4795impl From<XmlError> for DomXmlParseError {
4796    fn from(e: XmlError) -> Self {
4797        Self::Xml(e)
4798    }
4799}
4800
4801impl From<ComponentParseError> for DomXmlParseError {
4802    fn from(e: ComponentParseError) -> Self {
4803        Self::Component(e)
4804    }
4805}
4806
4807impl From<RenderDomError> for DomXmlParseError {
4808    fn from(e: RenderDomError) -> Self {
4809        Self::RenderDom(e)
4810    }
4811}
4812
4813impl From<CssParseErrorOwned> for DomXmlParseError {
4814    fn from(e: CssParseErrorOwned) -> Self {
4815        Self::Css(e)
4816    }
4817}
4818
4819/// Error that can happen from the translation from XML code to Rust code -
4820/// stringified, since it is only used for printing and is not exposed in the public API
4821#[derive(Debug, Clone, PartialEq)]
4822#[repr(C, u8)]
4823pub enum CompileError {
4824    Dom(RenderDomError),
4825    Xml(DomXmlParseError),
4826    Css(CssParseErrorOwned),
4827}
4828
4829impl From<ComponentError> for CompileError {
4830    fn from(e: ComponentError) -> Self {
4831        Self::Dom(RenderDomError::Component(e))
4832    }
4833}
4834
4835impl From<CssParseErrorOwned> for CompileError {
4836    fn from(e: CssParseErrorOwned) -> Self {
4837        Self::Css(e)
4838    }
4839}
4840
4841impl fmt::Display for CompileError {
4842    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4843        use self::CompileError::{Css, Dom, Xml};
4844        match self {
4845            Dom(d) => write!(f, "{d}"),
4846            Xml(s) => write!(f, "{s}"),
4847            Css(s) => write!(f, "{}", s.to_shared()),
4848        }
4849    }
4850}
4851
4852impl From<RenderDomError> for CompileError {
4853    fn from(e: RenderDomError) -> Self {
4854        Self::Dom(e)
4855    }
4856}
4857
4858impl From<DomXmlParseError> for CompileError {
4859    fn from(e: DomXmlParseError) -> Self {
4860        Self::Xml(e)
4861    }
4862}
4863
4864/// Wrapper for `UselessFunctionArgument` error data.
4865#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4866#[repr(C)]
4867pub struct UselessFunctionArgumentError {
4868    pub component_name: AzString,
4869    pub argument_name: AzString,
4870    pub valid_args: StringVec,
4871}
4872
4873#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4874#[repr(C, u8)]
4875pub enum ComponentError {
4876    /// While instantiating a component, a function argument
4877    /// was encountered that the component won't use or react to.
4878    UselessFunctionArgument(UselessFunctionArgumentError),
4879    /// A certain node type can't be rendered, because the
4880    /// renderer for this node is not available isn't available
4881    ///
4882    /// `UnknownComponent(component_name)`
4883    UnknownComponent(AzString),
4884}
4885
4886#[derive(Debug, Clone, PartialEq)]
4887#[repr(C, u8)]
4888pub enum RenderDomError {
4889    Component(ComponentError),
4890    /// Error parsing the CSS on the component style
4891    CssError(CssParseErrorOwned),
4892}
4893
4894impl From<ComponentError> for RenderDomError {
4895    fn from(e: ComponentError) -> Self {
4896        Self::Component(e)
4897    }
4898}
4899
4900impl From<CssParseErrorOwned> for RenderDomError {
4901    fn from(e: CssParseErrorOwned) -> Self {
4902        Self::CssError(e)
4903    }
4904}
4905
4906/// Wrapper for `MissingType` error data.
4907#[derive(Debug, Clone, PartialEq, Eq)]
4908#[repr(C)]
4909pub struct MissingTypeError {
4910    pub arg_pos: usize,
4911    pub arg_name: AzString,
4912}
4913
4914/// Wrapper for `WhiteSpaceInComponentName` error data.
4915#[derive(Debug, Clone, PartialEq, Eq)]
4916#[repr(C)]
4917pub struct WhiteSpaceInComponentNameError {
4918    pub arg_pos: usize,
4919    pub arg_name: AzString,
4920}
4921
4922/// Wrapper for `WhiteSpaceInComponentType` error data.
4923#[derive(Debug, Clone, PartialEq, Eq)]
4924#[repr(C)]
4925pub struct WhiteSpaceInComponentTypeError {
4926    pub arg_pos: usize,
4927    pub arg_name: AzString,
4928    pub arg_type: AzString,
4929}
4930
4931#[derive(Debug, Clone, PartialEq)]
4932#[repr(C, u8)]
4933pub enum ComponentParseError {
4934    /// Given `XmlNode` is not a `<component />` node.
4935    NotAComponent,
4936    /// A `<component>` node does not have a `name` attribute.
4937    UnnamedComponent,
4938    /// Argument at position `usize` is either empty or has no name
4939    MissingName(usize),
4940    /// Argument at position `usize` with the name
4941    /// `String` doesn't have a `: type`
4942    MissingType(MissingTypeError),
4943    /// Component name may not contain a whitespace
4944    /// (probably missing a `:` between the name and the type)
4945    WhiteSpaceInComponentName(WhiteSpaceInComponentNameError),
4946    /// Component type may not contain a whitespace
4947    /// (probably missing a `,` between the type and the next name)
4948    WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError),
4949    /// Error parsing the <style> tag / CSS
4950    CssError(CssParseErrorOwned),
4951}
4952
4953impl fmt::Display for DomXmlParseError {
4954    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4955        use self::DomXmlParseError::{
4956            Component, Css, MalformedHierarchy, MultipleBodyNodes, MultipleHtmlRootNodes,
4957            NoBodyInHtml, NoHtmlNode, RenderDom, Xml,
4958        };
4959        match self {
4960            NoHtmlNode => write!(
4961                f,
4962                "No <html> node found as the root of the file - empty file?"
4963            ),
4964            MultipleHtmlRootNodes => write!(
4965                f,
4966                "Multiple <html> nodes found as the root of the file - only one root node allowed"
4967            ),
4968            NoBodyInHtml => write!(
4969                f,
4970                "No <body> node found as a direct child of an <html> node - malformed DOM \
4971                 hierarchy?"
4972            ),
4973            MultipleBodyNodes => write!(
4974                f,
4975                "Multiple <body> nodes present, only one <body> node is allowed"
4976            ),
4977            Xml(e) => write!(f, "Error parsing XML: {e}"),
4978            MalformedHierarchy(e) => write!(
4979                f,
4980                "Invalid </{}> tag: expected </{}>",
4981                e.got.as_str(),
4982                e.expected.as_str()
4983            ),
4984            RenderDom(e) => write!(f, "Error rendering DOM: {e}"),
4985            Component(c) => write!(f, "Error parsing component in <head> node:\r\n{c}"),
4986            Css(c) => write!(f, "Error parsing CSS in <head> node:\r\n{}", c.to_shared()),
4987        }
4988    }
4989}
4990
4991impl fmt::Display for ComponentParseError {
4992    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4993        use self::ComponentParseError::{
4994            CssError, MissingName, MissingType, NotAComponent, UnnamedComponent,
4995            WhiteSpaceInComponentName, WhiteSpaceInComponentType,
4996        };
4997        match self {
4998            NotAComponent => write!(f, "Expected <component/> node, found no such node"),
4999            UnnamedComponent => write!(
5000                f,
5001                "Found <component/> tag with out a \"name\" attribute, component must have a name"
5002            ),
5003            MissingName(arg_pos) => write!(
5004                f,
5005                "Argument at position {arg_pos} is either empty or has no name"
5006            ),
5007            MissingType(e) => write!(
5008                f,
5009                "Argument \"{}\" at position {} doesn't have a `: type`",
5010                e.arg_name, e.arg_pos
5011            ),
5012            WhiteSpaceInComponentName(e) => {
5013                write!(
5014                    f,
5015                    "Missing `:` between the name and the type in argument {} (around \"{}\")",
5016                    e.arg_pos, e.arg_name
5017                )
5018            }
5019            WhiteSpaceInComponentType(e) => {
5020                write!(
5021                    f,
5022                    "Missing `,` between two arguments (in argument {}, position {}, around \
5023                     \"{}\")",
5024                    e.arg_name, e.arg_pos, e.arg_type
5025                )
5026            }
5027            CssError(lsf) => write!(f, "Error parsing <style> tag: {}", lsf.to_shared()),
5028        }
5029    }
5030}
5031
5032impl fmt::Display for ComponentError {
5033    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5034        use self::ComponentError::{UnknownComponent, UselessFunctionArgument};
5035        match self {
5036            UselessFunctionArgument(e) => {
5037                write!(
5038                    f,
5039                    "Useless component argument \"{}\": \"{}\" - available args are: {:#?}",
5040                    e.component_name, e.argument_name, e.valid_args
5041                )
5042            }
5043            UnknownComponent(name) => write!(f, "Unknown component: \"{name}\""),
5044        }
5045    }
5046}
5047
5048impl fmt::Display for RenderDomError {
5049    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5050        use self::RenderDomError::{Component, CssError};
5051        match self {
5052            Component(c) => write!(f, "{c}"),
5053            CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
5054        }
5055    }
5056}
5057
5058/// Find the one and only `<body>` node, return error if
5059/// there is no app node or there are multiple app nodes
5060#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5061/// # Errors
5062///
5063/// Returns an error if the document has no `<html>` root node.
5064/// Every `<head><style>` block's text, concatenated in document order.
5065///
5066/// ALL of them, not the first: a document may state its stylesheet in several
5067/// blocks (a reset, then the page's own rules), and browsers apply each in
5068/// turn. Reading only the first silently dropped everything after it - and it
5069/// is silent in the worst way, because the page still renders, just with some
5070/// rules missing and nothing to say which.
5071fn head_style_text(html_node: &XmlNode) -> String {
5072    let Some(head) = find_node_by_type(html_node.children.as_ref(), "head") else {
5073        return String::new();
5074    };
5075    let mut out = String::new();
5076    for child in head.children.as_ref() {
5077        let XmlNodeChild::Element(element) = child else {
5078            continue;
5079        };
5080        if !element.node_type.as_str().eq_ignore_ascii_case("style") {
5081            continue;
5082        }
5083        let text = element.get_text_content();
5084        if !text.is_empty() {
5085            if !out.is_empty() {
5086                out.push('\n');
5087            }
5088            out.push_str(&text);
5089        }
5090    }
5091    out
5092}
5093
5094pub fn get_html_node(
5095    root_nodes: &[XmlNodeChild],
5096) -> Result<alloc::borrow::Cow<'_, XmlNode>, DomXmlParseError> {
5097    use alloc::borrow::Cow;
5098
5099    let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
5100        if let XmlNodeChild::Element(node) = child {
5101            // HTML element names are case-insensitive (ASCII). NOT normalize_casing:
5102            // that inserts '_' before each uppercase letter for component-name
5103            // canonicalisation, so "HTML" would become "h_t_m_l" and never match.
5104            if node.node_type.as_str().eq_ignore_ascii_case("html") {
5105                Some(node)
5106            } else {
5107                None
5108            }
5109        } else {
5110            None
5111        }
5112    });
5113
5114    if let Some(html_node) = html_node_iterator.next() {
5115        return if html_node_iterator.next().is_some() {
5116            Err(DomXmlParseError::MultipleHtmlRootNodes)
5117        } else {
5118            Ok(Cow::Borrowed(html_node))
5119        };
5120    }
5121
5122    // NO <html> ROOT: synthesise one, the way a browser does.
5123    //
5124    // Requiring the wrapper made a perfectly good fragment - `<svg>…</svg>`,
5125    // `<div>hi</div>`, an icon file straight off the disk - parse into the
5126    // TEXT "No <html> node found as the root of the file", which then lays
5127    // out and paints like any other text. The failure was silent and looked
5128    // exactly like a rendering bug: a caller measuring pixels saw an error
5129    // message it never asked for and no sign of what happened.
5130    //
5131    // A root `<body>`/`<head>` is ADOPTED rather than nested (again like a
5132    // browser): wrapping `<body>` inside a fresh `<body>` would give the
5133    // document two, which is its own error.
5134    let has_structural_root = root_nodes.iter().any(|child| match child {
5135        XmlNodeChild::Element(node) => {
5136            let tag = node.node_type.as_str();
5137            tag.eq_ignore_ascii_case("body") || tag.eq_ignore_ascii_case("head")
5138        }
5139        XmlNodeChild::Text(_) => false,
5140    });
5141    if has_structural_root {
5142        return Ok(Cow::Owned(
5143            XmlNode::create("html").with_children(root_nodes.to_vec()),
5144        ));
5145    }
5146
5147    // METADATA GOES TO THE HEAD, content to the body - the same split a
5148    // browser makes. A `<style>` left in the body is not looked at by
5149    // `str_to_dom_unstyled` (it reads `<head><style>`), so a fragment that
5150    // brought its own stylesheet would render unstyled and give no hint why.
5151    let (head_children, body_children): (Vec<_>, Vec<_>) =
5152        root_nodes.iter().cloned().partition(|child| match child {
5153            XmlNodeChild::Element(node) => {
5154                let tag = node.node_type.as_str();
5155                tag.eq_ignore_ascii_case("style")
5156                    || tag.eq_ignore_ascii_case("link")
5157                    || tag.eq_ignore_ascii_case("meta")
5158                    || tag.eq_ignore_ascii_case("title")
5159                    || tag.eq_ignore_ascii_case("base")
5160            }
5161            XmlNodeChild::Text(_) => false,
5162        });
5163
5164    let mut html_children = Vec::new();
5165    if !head_children.is_empty() {
5166        html_children.push(XmlNodeChild::Element(
5167            XmlNode::create("head").with_children(head_children),
5168        ));
5169    }
5170    html_children.push(XmlNodeChild::Element(
5171        XmlNode::create("body").with_children(body_children),
5172    ));
5173    Ok(Cow::Owned(XmlNode::create("html").with_children(html_children)))
5174}
5175
5176/// Find the one and only `<body>` node, return error if
5177/// there is no app node or there are multiple app nodes
5178#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5179/// # Errors
5180///
5181/// Returns an error if the document has no `<body>` node.
5182pub fn get_body_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
5183    fn find_body_recursive(nodes: &[XmlNodeChild], depth: usize) -> Option<&XmlNode> {
5184        // AUDIT 2026-07-08: bound recursion depth to avoid a stack overflow on
5185        // pathologically deep markup while hunting for the <body> element.
5186        if depth > MAX_XML_NESTING_DEPTH {
5187            return None;
5188        }
5189        for child in nodes {
5190            if let XmlNodeChild::Element(node) = child {
5191                // case-insensitive ASCII tag match; see get_html_node.
5192                if node.node_type.as_str().eq_ignore_ascii_case("body") {
5193                    return Some(node);
5194                }
5195                // Recurse into children
5196                if let Some(found) = find_body_recursive(node.children.as_ref(), depth + 1) {
5197                    return Some(found);
5198                }
5199            }
5200        }
5201        None
5202    }
5203
5204    // First try to find body as a direct child (proper HTML structure)
5205    let direct_body = root_nodes.iter().find_map(|child| {
5206        if let XmlNodeChild::Element(node) = child {
5207            // case-insensitive ASCII tag match; see get_html_node.
5208            if node.node_type.as_str().eq_ignore_ascii_case("body") {
5209                Some(node)
5210            } else {
5211                None
5212            }
5213        } else {
5214            None
5215        }
5216    });
5217
5218    if let Some(body) = direct_body {
5219        return Ok(body);
5220    }
5221
5222    // If not found as direct child, search recursively (for malformed HTML like example.com)
5223    // where <body> might be nested inside <head> due to missing </head> tag
5224    find_body_recursive(root_nodes, 0).ok_or(DomXmlParseError::NoBodyInHtml)
5225}
5226
5227/// Searches in the the `root_nodes` for a `node_type`, convenience function in order to
5228/// for example find the first <blah /> node in all these nodes.
5229/// This function searches recursively through the entire tree.
5230fn find_node_by_type<'a>(root_nodes: &'a [XmlNodeChild], node_type: &str) -> Option<&'a XmlNode> {
5231    // First check direct children
5232    for child in root_nodes {
5233        if let XmlNodeChild::Element(node) = child {
5234            // case-insensitive ASCII tag match; see get_html_node.
5235            if node.node_type.as_str().eq_ignore_ascii_case(node_type) {
5236                return Some(node);
5237            }
5238        }
5239    }
5240
5241    // If not found, search recursively (for malformed HTML)
5242    for child in root_nodes {
5243        if let XmlNodeChild::Element(node) = child {
5244            if let Some(found) = find_node_by_type(node.children.as_ref(), node_type) {
5245                return Some(found);
5246            }
5247        }
5248    }
5249
5250    None
5251}
5252
5253#[must_use]
5254pub fn find_attribute<'a>(node: &'a XmlNode, attribute: &str) -> Option<&'a AzString> {
5255    node.attributes
5256        .iter()
5257        .find(|n| normalize_casing(n.key.as_str()).as_str() == attribute)
5258        .map(|s| &s.value)
5259}
5260
5261/// Normalizes input such as `abcDef`, `AbcDef`, `abc-def` to the normalized form of `abc_def`
5262#[must_use]
5263pub fn normalize_casing(input: &str) -> String {
5264    let mut words: Vec<String> = Vec::new();
5265    let mut cur_str = Vec::new();
5266
5267    for ch in input.chars() {
5268        if ch.is_uppercase() || ch == '_' || ch == '-' {
5269            if !cur_str.is_empty() {
5270                words.push(cur_str.iter().collect());
5271                cur_str.clear();
5272            }
5273            if ch.is_uppercase() {
5274                cur_str.extend(ch.to_lowercase());
5275            }
5276        } else {
5277            cur_str.extend(ch.to_lowercase());
5278        }
5279    }
5280
5281    if !cur_str.is_empty() {
5282        words.push(cur_str.iter().collect());
5283        cur_str.clear();
5284    }
5285
5286    words.join("_")
5287}
5288
5289/// Given a root node, traverses along the hierarchy, and returns a
5290/// mutable reference to the last child node of the root node
5291#[allow(trivial_casts)]
5292pub fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> {
5293    let mut hierarchy = hierarchy.to_vec();
5294    hierarchy.reverse();
5295    let Some(item) = hierarchy.pop() else {
5296        return Some(root_node);
5297    };
5298    let child = root_node.children.as_mut().get_mut(item)?;
5299    match child {
5300        XmlNodeChild::Element(node) => get_item_internal(&mut hierarchy, node),
5301        XmlNodeChild::Text(_) => None, // Can't traverse into text nodes
5302    }
5303}
5304
5305fn get_item_internal<'a>(
5306    hierarchy: &mut Vec<usize>,
5307    root_node: &'a mut XmlNode,
5308) -> Option<&'a mut XmlNode> {
5309    if hierarchy.is_empty() {
5310        return Some(root_node);
5311    }
5312    let Some(cur_item) = hierarchy.pop() else {
5313        return Some(root_node);
5314    };
5315    let child = root_node.children.as_mut().get_mut(cur_item)?;
5316    match child {
5317        XmlNodeChild::Element(node) => get_item_internal(hierarchy, node),
5318        XmlNodeChild::Text(_) => None, // Can't traverse into text nodes
5319    }
5320}
5321
5322/// Parses an XML string and returns a `StyledDom` with the components instantiated in the
5323/// `<app></app>`
5324#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5325/// # Errors
5326///
5327/// Returns an error if the XML cannot be parsed into a DOM (malformed markup or an unknown component).
5328pub fn str_to_dom<'a>(
5329    root_nodes: &'a [XmlNodeChild],
5330    component_map: &'a ComponentMap,
5331    max_width: Option<f32>,
5332) -> Result<StyledDom, DomXmlParseError> {
5333    // Delegate to the fast path (Dom::Fast / CompactDom arena).
5334    str_to_dom_fast(root_nodes, component_map, max_width)
5335}
5336
5337/// Parse XML to `StyledDom` via arena-based `FastDom` (no tree intermediary).
5338///
5339/// **Note**: `str_to_dom()` now delegates to this function, so you can use
5340/// either one. This function is kept for backward compatibility.
5341#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5342fn str_to_dom_fast<'a>(
5343    root_nodes: &'a [XmlNodeChild],
5344    component_map: &'a ComponentMap,
5345    max_width: Option<f32>,
5346) -> Result<StyledDom, DomXmlParseError> {
5347    let html_node = get_html_node(root_nodes)?;
5348    let body_node = get_body_node(html_node.children.as_ref())?;
5349
5350    let style_text = head_style_text(&html_node);
5351    let global_style = if style_text.is_empty() {
5352        None
5353    } else {
5354        Some(Css::from_string(style_text.into()))
5355    };
5356
5357    render_dom_from_body_node_fast(body_node, global_style, component_map, max_width)
5358        .map_err(Into::into)
5359}
5360
5361/// Parses XML nodes and returns a `Dom` with CSS stylesheets attached (but not applied).
5362///
5363/// Unlike `str_to_dom` which returns a fully styled `StyledDom`, this function
5364/// returns an unstyled `Dom` whose `css` field carries the parsed `<style>` rules.
5365/// The layout framework will apply the CSS during the cascade pass.
5366///
5367/// This is the correct function for building a `Dom` from XML in layout callbacks
5368/// (which must return `Dom`, not `StyledDom`).
5369#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5370/// # Errors
5371///
5372/// Returns an error if the XML cannot be parsed into a DOM (malformed markup or an unknown component).
5373pub fn str_to_dom_unstyled<'a>(
5374    root_nodes: &'a [XmlNodeChild],
5375    component_map: &'a ComponentMap,
5376) -> Result<Dom, DomXmlParseError> {
5377    let html_node = get_html_node(root_nodes)?;
5378    let body_node = get_body_node(html_node.children.as_ref())?;
5379
5380    let style_text = head_style_text(&html_node);
5381    let global_style = if style_text.is_empty() {
5382        None
5383    } else {
5384        Some(Css::from_string(style_text.into()))
5385    };
5386
5387    // Build the DOM tree from the body node
5388    let body_dom =
5389        xml_node_to_dom_fast(body_node, component_map, false, 0).map_err(DomXmlParseError::from)?;
5390
5391    // Wrap in proper HTML structure (NodeType is imported at module top)
5392    let root_node_type = body_dom.root.node_type.clone();
5393
5394    let mut full_dom = match root_node_type {
5395        NodeType::Html => body_dom,
5396        NodeType::Body => Dom::create_html().with_child(body_dom),
5397        _ => {
5398            let body_wrapper = Dom::create_body().with_child(body_dom);
5399            Dom::create_html().with_child(body_wrapper)
5400        }
5401    };
5402
5403    // Attach CSS to the Dom's css field instead of applying it immediately
5404    if let Some(css) = global_style {
5405        full_dom.css = alloc::vec![css].into();
5406    }
5407
5408    Ok(full_dom)
5409}
5410
5411/// Parses an XML string and returns a `String`, which contains the Rust source code
5412/// (i.e. it compiles the XML to valid Rust)
5413#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
5414/// # Errors
5415///
5416/// Returns an error if the XML cannot be parsed or compiled to Rust code.
5417pub fn str_to_rust_code<'a>(
5418    root_nodes: &'a [XmlNodeChild],
5419    imports: &str,
5420    component_map: &'a ComponentMap,
5421) -> Result<String, CompileError> {
5422    let html_node = get_html_node(root_nodes)?;
5423    let body_node = get_body_node(html_node.children.as_ref())?;
5424    let style_text = head_style_text(&html_node);
5425    let mut global_style = if style_text.is_empty() {
5426        Css::empty()
5427    } else {
5428        azul_css::parser2::new_from_str(&style_text).0
5429    };
5430
5431    global_style.sort_by_specificity();
5432
5433    let mut css_blocks = BTreeMap::new();
5434    let mut extra_blocks = VecContents::default();
5435    let app_source = compile_body_node_to_rust_code(
5436        body_node,
5437        component_map,
5438        &mut extra_blocks,
5439        &mut css_blocks,
5440        &global_style,
5441        CssMatcher {
5442            path: Vec::new(),
5443            indices_in_parent: vec![0],
5444            children_length: vec![body_node.children.as_ref().len()],
5445        },
5446    )?;
5447
5448    let app_source = app_source
5449        .lines()
5450        .map(|l| format!("        {l}"))
5451        .collect::<Vec<String>>()
5452        .join("\r\n");
5453
5454    // NOTE: `css_blocks` / `extra_blocks` are no longer emitted — per-node styles
5455    // are now inlined as `.with_css("..")` strings (public API) rather than as
5456    // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` blocks (that API was
5457    // removed in 32d44ed8a). The maps stay in the signatures for compatibility.
5458    let _ = (&css_blocks, &extra_blocks);
5459
5460    let main_func = "
5461
5462use azul::{
5463    app::{App, AppConfig},
5464    dom::Dom,
5465    callbacks::{RefAny, LayoutCallbackInfo},
5466    window::WindowCreateOptions,
5467};
5468
5469struct Data { }
5470
5471extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> Dom {
5472    crate::ui::render()
5473}
5474
5475fn main() {
5476    let config = AppConfig::create();
5477    let app = App::create(RefAny::new(Data { }), config);
5478    let window = WindowCreateOptions::create(render);
5479    app.run(window);
5480}";
5481
5482    let ui_module = format!(
5483        "#[allow(unused_imports)]\r\npub mod ui {{
5484
5485    use azul::prelude::*;
5486    use azul::dom::{{NodeType, TabIndex, SmallAriaInfo}};
5487    use azul::str::String as AzString;
5488
5489    pub fn render() -> Dom {{\r\n{app_source}\r\n    }}\r\n}}"
5490    );
5491    let source_code = format!(
5492        "#![windows_subsystem = \"windows\"]\r\n//! Auto-generated UI source \
5493         code\r\n{}\r\n{}\r\n\r\n{}{}",
5494        imports,
5495        compile_components(Vec::new()), // no user-defined components to compile
5496        ui_module,
5497        main_func,
5498    );
5499
5500    Ok(source_code)
5501}
5502
5503// Compile all components to source code
5504#[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
5505fn compile_components(
5506    components: Vec<(
5507        ComponentName,
5508        CompiledComponent,
5509        ComponentArguments,
5510        BTreeMap<String, String>,
5511    )>,
5512) -> String {
5513    let cs = components
5514        .iter()
5515        .map(|(name, function_body, function_args, css_blocks)| {
5516            let name = &normalize_casing(name);
5517            let f = compile_component(name, function_args, function_body)
5518                .lines()
5519                .map(|l| format!("    {l}"))
5520                .collect::<Vec<String>>()
5521                .join("\r\n");
5522
5523            // let css_blocks = ...
5524
5525            format!(
5526                "#[allow(unused_imports)]\r\npub mod {name} {{\r\n    use azul::dom::Dom;\r\n    use \
5527                 azul::str::String as AzString;\r\n{f}\r\n}}"
5528            )
5529        })
5530        .collect::<Vec<String>>()
5531        .join("\r\n\r\n");
5532
5533    let cs = cs
5534        .lines()
5535        .map(|l| format!("    {l}"))
5536        .collect::<Vec<String>>()
5537        .join("\r\n");
5538
5539    if cs.is_empty() {
5540        cs
5541    } else {
5542        format!("pub mod components {{\r\n{cs}\r\n}}")
5543    }
5544}
5545
5546fn format_component_args(component_args: &ComponentArgumentVec) -> String {
5547    let mut args = component_args
5548        .iter()
5549        .map(|a| format!("{}: {}", a.name, a.arg_type))
5550        .collect::<Vec<String>>();
5551
5552    args.sort_by(|a, b| b.cmp(a));
5553
5554    args.join(", ")
5555}
5556
5557#[must_use]
5558pub fn compile_component(
5559    component_name: &str,
5560    component_args: &ComponentArguments,
5561    component_function_body: &str,
5562) -> String {
5563    let component_name = &normalize_casing(component_name);
5564    let function_args = format_component_args(&component_args.args);
5565    let component_function_body = component_function_body
5566        .lines()
5567        .map(|l| format!("    {l}"))
5568        .collect::<Vec<String>>()
5569        .join("\r\n");
5570    let should_inline = component_function_body.lines().count() == 1;
5571    format!(
5572        "{}pub fn render({}{}{}) -> Dom {{\r\n{}\r\n}}",
5573        if should_inline { "#[inline]\r\n" } else { "" },
5574        // pass the text content as the first
5575        if component_args.accepts_text {
5576            "text: AzString"
5577        } else {
5578            ""
5579        },
5580        if function_args.is_empty() || !component_args.accepts_text {
5581            ""
5582        } else {
5583            ", "
5584        },
5585        function_args,
5586        component_function_body,
5587    )
5588}
5589
5590/// Parse an SVG numeric attribute value to f32.
5591///
5592/// STRICT: a geometry attribute (`cx`, `r`, `x1`, ...) is a USER UNIT, and
5593/// `cx="10px"` is not valid SVG. The `<svg>` element's own `width`/`height`
5594/// are CSS lengths and a different thing entirely - see [`parse_svg_length`].
5595fn parse_svg_float(attr: Option<&AzString>) -> Option<f32> {
5596    attr?.as_str().trim().parse::<f32>().ok()
5597}
5598
5599/// Parse the `<svg>` element's own `width`/`height`, which - unlike the
5600/// geometry attributes - are CSS LENGTHS.
5601///
5602/// A bare `px` is accepted because `width="16px"` is as common in the wild as
5603/// `width="16"`. A relative unit (`%`, `em`) is REJECTED rather than guessed
5604/// at: the caller then falls back to the viewBox, which is a real answer,
5605/// instead of resolving a percentage against nothing.
5606fn parse_svg_length(attr: Option<&AzString>) -> Option<f32> {
5607    let raw = attr?.as_str().trim();
5608    let number = raw.strip_suffix("px").unwrap_or(raw).trim();
5609    number.parse::<f32>().ok()
5610}
5611
5612/// Parse an SVG `viewBox` into `(min_x, min_y, width, height)`.
5613///
5614/// Space- or comma-separated, per the spec; exactly four numbers, because
5615/// three or five is a malformed viewBox and silently taking the first four
5616/// would place the art somewhere nobody asked for.
5617fn parse_svg_view_box(value: &str) -> Option<(f32, f32, f32, f32)> {
5618    let nums: Vec<f32> = value
5619        .split(|c: char| c == ',' || c.is_ascii_whitespace())
5620        .filter(|s| !s.is_empty())
5621        .map(str::parse::<f32>)
5622        .collect::<Result<Vec<_>, _>>()
5623        .ok()?;
5624    match nums[..] {
5625        [min_x, min_y, width, height] if width > 0.0 && height > 0.0 => {
5626            Some((min_x, min_y, width, height))
5627        }
5628        _ => None,
5629    }
5630}
5631
5632/// Parse an SVG `points` attribute (used by `<polygon>` and `<polyline>`).
5633fn parse_svg_points(pts: &str, close: bool) -> Option<crate::svg::SvgMultiPolygon> {
5634    let nums: Vec<f32> = pts
5635        .split(|c: char| c == ',' || c.is_ascii_whitespace())
5636        .filter(|s| !s.is_empty())
5637        .filter_map(|s| s.parse::<f32>().ok())
5638        .collect();
5639    if nums.len() < 4 || !nums.len().is_multiple_of(2) {
5640        return None;
5641    }
5642    let mut elements = Vec::new();
5643    let points: Vec<azul_css::props::basic::SvgPoint> = nums
5644        .chunks_exact(2)
5645        .map(|c| azul_css::props::basic::SvgPoint { x: c[0], y: c[1] })
5646        .collect();
5647    for w in points.windows(2) {
5648        elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5649            w[0], w[1],
5650        )));
5651    }
5652    if close && points.len() >= 2 {
5653        let first = points[0];
5654        let last = *points.last().unwrap();
5655        if (first.x - last.x).abs() > 0.001 || (first.y - last.y).abs() > 0.001 {
5656            elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5657                last, first,
5658            )));
5659        }
5660    }
5661    Some(crate::svg::SvgMultiPolygon {
5662        rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5663            items: crate::svg::SvgPathElementVec::from_vec(elements),
5664        }]),
5665    })
5666}
5667
5668/// Fast XML to Dom conversion that builds Dom tree directly without intermediate `StyledDom`
5669/// This is O(n) instead of O(n²) for large documents
5670/// Apply the shared set of XML attributes onto a single [`NodeData`] node.
5671///
5672/// Handles `<img src>` rebuild, `id`/`class`, `focusable`, `tabindex`, inline
5673/// `style`, and SVG-shape geometry — the block that was previously duplicated
5674/// verbatim between [`xml_node_to_dom_fast`] (operating on `dom.root`) and
5675/// [`xml_node_to_fast_dom`] (operating on the arena `NodeData`). `component_name`
5676/// must already be normalized (lowercased); the caller computes `child_inside_svg`.
5677#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
5678fn apply_xml_node_attributes(
5679    node: &mut crate::dom::NodeData,
5680    xml_node: &XmlNode,
5681    component_name: &str,
5682    inside_svg: bool,
5683) {
5684    use crate::dom::{IdOrClass, NodeType, TabIndex};
5685
5686    // `<img src="...">`: rebuild the placeholder Image node so its `NullImage`
5687    // carries the `src` string (as UTF-8 bytes in `tag`). The bytes are NOT
5688    // resolved here — a downstream renderer (printpdf, the compositor, ...) uses
5689    // the tag to look up and embed the actual image. Optional `width`/`height`
5690    // attributes set the intrinsic size used for layout (CSS still overrides).
5691    if component_name == "img" {
5692        if let Some(src) = xml_node.attributes.get_key("src") {
5693            let width = xml_node
5694                .attributes
5695                .get_key("width")
5696                .and_then(|w| {
5697                    w.as_str()
5698                        .trim()
5699                        .trim_end_matches("px")
5700                        .trim()
5701                        .parse::<usize>()
5702                        .ok()
5703                })
5704                .unwrap_or(0);
5705            let height = xml_node
5706                .attributes
5707                .get_key("height")
5708                .and_then(|h| {
5709                    h.as_str()
5710                        .trim()
5711                        .trim_end_matches("px")
5712                        .trim()
5713                        .parse::<usize>()
5714                        .ok()
5715                })
5716                .unwrap_or(0);
5717            let image_ref = crate::resources::ImageRef::null_image(
5718                width,
5719                height,
5720                crate::resources::RawImageFormat::RGBA8,
5721                src.as_str().as_bytes().to_vec(),
5722            );
5723            node.set_node_type(NodeType::Image(azul_css::css::BoxOrStatic::heap(image_ref)));
5724        }
5725    }
5726
5727    // Set id and class attributes
5728    let mut ids_and_classes = Vec::new();
5729    if let Some(id_str) = xml_node.attributes.get_key("id") {
5730        for id in id_str.split_whitespace() {
5731            ids_and_classes.push(IdOrClass::Id(id.into()));
5732        }
5733    }
5734    if let Some(class_str) = xml_node.attributes.get_key("class") {
5735        for class in class_str.split_whitespace() {
5736            ids_and_classes.push(IdOrClass::Class(class.into()));
5737        }
5738    }
5739    if !ids_and_classes.is_empty() {
5740        node.set_ids_and_classes(ids_and_classes.into());
5741    }
5742
5743    // Handle focusable attribute
5744    if let Some(focusable) = xml_node
5745        .attributes
5746        .get_key("focusable")
5747        .and_then(|f| parse_bool(f.as_str()))
5748    {
5749        if focusable {
5750            node.set_tab_index(TabIndex::Auto);
5751        } else {
5752            node.set_tab_index(TabIndex::NoKeyboardFocus);
5753        }
5754    }
5755
5756    // Handle tabindex attribute
5757    if let Some(tab_index) = xml_node
5758        .attributes
5759        .get_key("tabindex")
5760        .and_then(|val| val.parse::<isize>().ok())
5761    {
5762        match tab_index {
5763            0 => node.set_tab_index(TabIndex::Auto),
5764            i if i > 0 => node.set_tab_index(TabIndex::OverrideInParent(
5765                u32::try_from(i).unwrap_or(u32::MAX),
5766            )),
5767            _ => node.set_tab_index(TabIndex::NoKeyboardFocus),
5768        }
5769    }
5770
5771    // Table cell span attributes (`colspan` / `rowspan`).
5772    apply_cell_span_attributes(node, xml_node);
5773
5774    // HTML `dir` attribute → the `direction` CSS property (dir="rtl"/"ltr"). Without
5775    // this, dir="rtl" (the common way to set RTL in HTML) had no effect. Appended
5776    // BEFORE the inline `style` below so author style still wins on equal specificity.
5777    let dir_prop = xml_node.attributes.get_key("dir").and_then(|d| {
5778        let v = d.as_str().trim();
5779        if v.eq_ignore_ascii_case("rtl") {
5780            Some(azul_css::props::style::StyleDirection::Rtl)
5781        } else if v.eq_ignore_ascii_case("ltr") {
5782            Some(azul_css::props::style::StyleDirection::Ltr)
5783        } else {
5784            None
5785        }
5786    });
5787
5788    // `<svg>`: its own viewport. Two things have to come off the element, and
5789    // both were being dropped.
5790    //
5791    //   * the `viewBox`, which is the element's USER-SPACE coordinate system.
5792    //     `SvgNodeData::ViewBox` existed as a variant but nothing ever produced
5793    //     one, so a parsed `<svg>` had no record of what coordinate space its
5794    //     children were drawn in.
5795    //   * an INTRINSIC SIZE. An `<svg>` is a replaced element: it is as big as
5796    //     `width`/`height` say, and failing that as big as its viewBox (SVG's
5797    //     own default sizing rule). Without one the element lays out 0x0 and
5798    //     takes no space at all - which is what an icon parsed straight from a
5799    //     theme file did, and why it came out blank.
5800    //
5801    // These are INTRINSIC dimensions, not a demand: they are pushed ahead of
5802    // the inline `style` below, so a call site that says how big it wants the
5803    // thing still wins.
5804    let mut intrinsic_props: Vec<azul_css::dynamic_selector::CssPropertyWithConditions> =
5805        Vec::new();
5806    if component_name == "svg" {
5807        let view_box = xml_node
5808            .attributes
5809            .get_key("viewBox")
5810            .or_else(|| xml_node.attributes.get_key("viewbox"))
5811            .and_then(|v| parse_svg_view_box(v.as_str()));
5812        if let Some((min_x, min_y, width, height)) = view_box {
5813            node.set_svg_data(crate::dom::SvgNodeData::ViewBox {
5814                min_x,
5815                min_y,
5816                width,
5817                height,
5818            });
5819        }
5820        let stated = |key: &str| parse_svg_length(xml_node.attributes.get_key(key));
5821        let usable = |v: f32| v.is_finite() && v > 0.0;
5822        if let Some(w) = stated("width")
5823            .or(view_box.map(|(_, _, w, _)| w))
5824            .filter(|w| usable(*w))
5825        {
5826            intrinsic_props.push(
5827                azul_css::dynamic_selector::CssPropertyWithConditions::simple(
5828                    azul_css::props::property::CssProperty::width(
5829                        azul_css::props::layout::LayoutWidth::px(w),
5830                    ),
5831                ),
5832            );
5833        }
5834        if let Some(h) = stated("height")
5835            .or(view_box.map(|(_, _, _, h)| h))
5836            .filter(|h| usable(*h))
5837        {
5838            intrinsic_props.push(
5839                azul_css::dynamic_selector::CssPropertyWithConditions::simple(
5840                    azul_css::props::property::CssProperty::height(
5841                        azul_css::props::layout::LayoutHeight::px(h),
5842                    ),
5843                ),
5844            );
5845        }
5846    }
5847
5848    // An SVG SHAPE is painted by filling its own box and clipping that box to
5849    // its geometry (`SvgNodeData::*`, pushed as a clip mask by the display
5850    // list). Two things make that work, and both are ordinary CSS:
5851    //
5852    //   * the box has to BE the `<svg>`'s viewport - the clip mask is
5853    //     rasterised into the node's paint rect, so a shape that laid out as
5854    //     an ordinary in-flow block would be clipped against the wrong
5855    //     rectangle (and, being empty, would be 0-high anyway);
5856    //   * `fill` has to reach the cascade. The presentation ATTRIBUTE is
5857    //     translated here; `style="fill:…"` and a stylesheet rule need nothing,
5858    //     because `fill` is an accepted spelling of `background-color`
5859    //     (`COMBINED_CSS_PROPERTIES_KEY_MAP`).
5860    //
5861    // `fill="none"` deliberately emits NOTHING rather than a transparent
5862    // background: it must not shadow a stylesheet rule that does set a fill.
5863    if inside_svg
5864        && matches!(
5865            component_name,
5866            "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline"
5867        )
5868    {
5869        use azul_css::props::{
5870            layout::{
5871                LayoutInsetBottom, LayoutLeft, LayoutPosition, LayoutRight, LayoutTop,
5872            },
5873            property::CssProperty,
5874        };
5875        let simple = azul_css::dynamic_selector::CssPropertyWithConditions::simple;
5876        intrinsic_props.push(simple(CssProperty::const_position(LayoutPosition::Absolute)));
5877        intrinsic_props.push(simple(CssProperty::const_left(LayoutLeft::const_px(0))));
5878        intrinsic_props.push(simple(CssProperty::const_top(LayoutTop::const_px(0))));
5879        intrinsic_props.push(simple(CssProperty::const_right(LayoutRight::const_px(0))));
5880        intrinsic_props.push(simple(CssProperty::const_bottom(LayoutInsetBottom::const_px(
5881            0,
5882        ))));
5883
5884        if let Some(fill) = xml_node.attributes.get_key("fill") {
5885            let fill = fill.as_str().trim();
5886            if fill != "none" {
5887                if let Ok(color) = azul_css::props::basic::color::parse_css_color(fill) {
5888                    intrinsic_props.push(simple(CssProperty::const_background_content(
5889                        azul_css::props::style::StyleBackgroundContentVec::from_vec(vec![
5890                            azul_css::props::style::StyleBackgroundContent::Color(color),
5891                        ]),
5892                    )));
5893                }
5894            }
5895        }
5896
5897        // The STROKE, as the box's border - the display list turns it into a
5898        // stroked path rather than a rectangle. Both halves are translated
5899        // here only for the presentation ATTRIBUTE; `style="stroke:…"` and a
5900        // stylesheet rule need nothing, because `stroke`/`stroke-width` are
5901        // accepted spellings of `border-color`/`border-width`.
5902        if let Some(stroke) = xml_node.attributes.get_key("stroke") {
5903            let stroke = stroke.as_str().trim();
5904            if stroke != "none" {
5905                if let Ok(color) = azul_css::props::basic::color::parse_css_color(stroke) {
5906                    use azul_css::props::style::{
5907                        StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor,
5908                        StyleBorderTopColor,
5909                    };
5910                    intrinsic_props.push(simple(CssProperty::const_border_top_color(
5911                        StyleBorderTopColor { inner: color },
5912                    )));
5913                    intrinsic_props.push(simple(CssProperty::const_border_right_color(
5914                        StyleBorderRightColor { inner: color },
5915                    )));
5916                    intrinsic_props.push(simple(CssProperty::const_border_bottom_color(
5917                        StyleBorderBottomColor { inner: color },
5918                    )));
5919                    intrinsic_props.push(simple(CssProperty::const_border_left_color(
5920                        StyleBorderLeftColor { inner: color },
5921                    )));
5922                }
5923            }
5924        }
5925        // `stroke-width` is in USER UNITS, like every other geometry
5926        // attribute - not a CSS length.
5927        if let Some(width) = parse_svg_float(xml_node.attributes.get_key("stroke-width")) {
5928            if width.is_finite() && width > 0.0 {
5929                use azul_css::props::style::{
5930                    LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth,
5931                    LayoutBorderTopWidth,
5932                };
5933                let px = azul_css::props::basic::PixelValue::px(width);
5934                intrinsic_props.push(simple(CssProperty::const_border_top_width(
5935                    LayoutBorderTopWidth { inner: px },
5936                )));
5937                intrinsic_props.push(simple(CssProperty::const_border_right_width(
5938                    LayoutBorderRightWidth { inner: px },
5939                )));
5940                intrinsic_props.push(simple(CssProperty::const_border_bottom_width(
5941                    LayoutBorderBottomWidth { inner: px },
5942                )));
5943                intrinsic_props.push(simple(CssProperty::const_border_left_width(
5944                    LayoutBorderLeftWidth { inner: px },
5945                )));
5946            }
5947        }
5948    }
5949
5950    // `<svg>` is the positioning context its shapes resolve against.
5951    if component_name == "svg" {
5952        intrinsic_props.push(
5953            azul_css::dynamic_selector::CssPropertyWithConditions::simple(
5954                azul_css::props::property::CssProperty::const_position(
5955                    azul_css::props::layout::LayoutPosition::Relative,
5956                ),
5957            ),
5958        );
5959    }
5960
5961    // Handle inline style attribute (and the mapped `dir` attribute above)
5962    let style_attr = xml_node.attributes.get_key("style");
5963    if style_attr.is_some() || dir_prop.is_some() || !intrinsic_props.is_empty() {
5964        use azul_css::dynamic_selector::CssPropertyWithConditions;
5965        let css_key_map = azul_css::props::property::get_css_key_map();
5966        let mut props: Vec<CssPropertyWithConditions> = intrinsic_props;
5967        if let Some(dir) = dir_prop {
5968            props.push(CssPropertyWithConditions::simple(
5969                azul_css::props::property::CssProperty::Direction(
5970                    azul_css::css::CssPropertyValue::Exact(dir),
5971                ),
5972            ));
5973        }
5974        if let Some(style) = style_attr {
5975            let mut attributes = Vec::new();
5976            for s in style.as_str().split(';') {
5977                let mut s = s.split(':');
5978                let Some(key) = s.next() else {
5979                    continue;
5980                };
5981                let Some(value) = s.next() else {
5982                    continue;
5983                };
5984                // Called for its side effect (writes parsed props into `attributes`);
5985                // the returned value is intentionally discarded.
5986                drop(azul_css::parser2::parse_css_declaration(
5987                    key.trim(),
5988                    value.trim(),
5989                    azul_css::parser2::ErrorLocationRange::default(),
5990                    &css_key_map,
5991                    &mut Vec::new(),
5992                    &mut attributes,
5993                ));
5994            }
5995            props.extend(attributes.into_iter().filter_map(|s| match s {
5996                CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
5997                CssDeclaration::Dynamic(_) => None,
5998            }));
5999        }
6000        if !props.is_empty() {
6001            node.set_css_props(props.into());
6002        }
6003    }
6004
6005    // Handle SVG shape elements when inside an <svg> context
6006    let tag = component_name;
6007    let is_svg_shape = inside_svg
6008        && matches!(
6009            tag,
6010            "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline"
6011        );
6012
6013    if is_svg_shape {
6014        let clip = match tag {
6015            "path" => xml_node
6016                .attributes
6017                .get_key("d")
6018                .and_then(|d| crate::path_parser::parse_svg_path_d(d.as_str()).ok()),
6019            "circle" => {
6020                let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
6021                let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
6022                let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
6023                if r > 0.0 {
6024                    Some(crate::svg::SvgMultiPolygon {
6025                        rings: crate::svg::SvgPathVec::from_vec(vec![
6026                            crate::path_parser::svg_circle_to_paths(cx, cy, r),
6027                        ]),
6028                    })
6029                } else {
6030                    None
6031                }
6032            }
6033            "rect" => {
6034                let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
6035                let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
6036                let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
6037                let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
6038                let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
6039                let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
6040                if w > 0.0 && h > 0.0 {
6041                    Some(crate::svg::SvgMultiPolygon {
6042                        rings: crate::svg::SvgPathVec::from_vec(vec![
6043                            crate::path_parser::svg_rect_to_path(x, y, w, h, rx, ry),
6044                        ]),
6045                    })
6046                } else {
6047                    None
6048                }
6049            }
6050            "ellipse" => {
6051                let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
6052                let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
6053                let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
6054                let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
6055                if rx > 0.0 && ry > 0.0 {
6056                    // Approximate ellipse with 4 cubic beziers (using rx for x-kappa, ry for y-kappa)
6057                    use azul_css::props::basic::{SvgCubicCurve, SvgPoint};
6058                    const KAPPA: f32 = 0.552_284_8;
6059                    let kx = rx * KAPPA;
6060                    let ky = ry * KAPPA;
6061                    let elements = vec![
6062                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
6063                            start: SvgPoint { x: cx, y: cy - ry },
6064                            ctrl_1: SvgPoint {
6065                                x: cx + kx,
6066                                y: cy - ry,
6067                            },
6068                            ctrl_2: SvgPoint {
6069                                x: cx + rx,
6070                                y: cy - ky,
6071                            },
6072                            end: SvgPoint { x: cx + rx, y: cy },
6073                        }),
6074                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
6075                            start: SvgPoint { x: cx + rx, y: cy },
6076                            ctrl_1: SvgPoint {
6077                                x: cx + rx,
6078                                y: cy + ky,
6079                            },
6080                            ctrl_2: SvgPoint {
6081                                x: cx + kx,
6082                                y: cy + ry,
6083                            },
6084                            end: SvgPoint { x: cx, y: cy + ry },
6085                        }),
6086                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
6087                            start: SvgPoint { x: cx, y: cy + ry },
6088                            ctrl_1: SvgPoint {
6089                                x: cx - kx,
6090                                y: cy + ry,
6091                            },
6092                            ctrl_2: SvgPoint {
6093                                x: cx - rx,
6094                                y: cy + ky,
6095                            },
6096                            end: SvgPoint { x: cx - rx, y: cy },
6097                        }),
6098                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
6099                            start: SvgPoint { x: cx - rx, y: cy },
6100                            ctrl_1: SvgPoint {
6101                                x: cx - rx,
6102                                y: cy - ky,
6103                            },
6104                            ctrl_2: SvgPoint {
6105                                x: cx - kx,
6106                                y: cy - ry,
6107                            },
6108                            end: SvgPoint { x: cx, y: cy - ry },
6109                        }),
6110                    ];
6111                    Some(crate::svg::SvgMultiPolygon {
6112                        rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
6113                            items: crate::svg::SvgPathElementVec::from_vec(elements),
6114                        }]),
6115                    })
6116                } else {
6117                    None
6118                }
6119            }
6120            "line" => {
6121                let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
6122                let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
6123                let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
6124                let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
6125                Some(crate::svg::SvgMultiPolygon {
6126                    rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
6127                        items: crate::svg::SvgPathElementVec::from_vec(vec![
6128                            crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
6129                                azul_css::props::basic::SvgPoint { x: x1, y: y1 },
6130                                azul_css::props::basic::SvgPoint { x: x2, y: y2 },
6131                            )),
6132                        ]),
6133                    }]),
6134                })
6135            }
6136            "polygon" | "polyline" => xml_node
6137                .attributes
6138                .get_key("points")
6139                .and_then(|pts| parse_svg_points(pts.as_str(), tag == "polygon")),
6140            _ => None,
6141        };
6142
6143        if let Some(mp) = clip {
6144            node.set_svg_data(crate::dom::SvgNodeData::Path(mp));
6145        }
6146    }
6147}
6148
6149/// Parse the HTML `colspan` / `rowspan` presentational attributes into
6150/// `AttributeType`s on the node. The table layout reads them back via
6151/// `get_cell_spans`. Without this the XML→DOM conversion dropped them and every
6152/// cell defaulted to span 1, so `<th colspan="2">` only covered one column.
6153/// Parsed unconditionally — non-cell elements simply don't carry these attributes.
6154fn apply_cell_span_attributes(node: &mut crate::dom::NodeData, xml_node: &XmlNode) {
6155    let mut spans = Vec::new();
6156    if let Some(n) = xml_node
6157        .attributes
6158        .get_key("colspan")
6159        .and_then(|v| v.as_str().trim().parse::<i32>().ok())
6160    {
6161        spans.push(crate::dom::AttributeType::ColSpan(n));
6162    }
6163    if let Some(n) = xml_node
6164        .attributes
6165        .get_key("rowspan")
6166        .and_then(|v| v.as_str().trim().parse::<i32>().ok())
6167    {
6168        spans.push(crate::dom::AttributeType::RowSpan(n));
6169    }
6170    if !spans.is_empty() {
6171        let mut v = node.attributes().clone().into_library_owned_vec();
6172        v.extend(spans);
6173        node.set_attributes(v.into());
6174    }
6175}
6176
6177#[allow(clippy::result_large_err)]
6178// returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
6179// component_map is threaded through the whole fast-DOM pipeline for parity with the
6180// component-expanding interpreter path (see ~xml.rs:2845); this fast path never expands
6181// components, so it only forwards the map into recursive calls. Removing it here would
6182// cascade unused-param removals up the entire pipeline.
6183#[allow(clippy::only_used_in_recursion)]
6184/// Every `<style>` element's text in this subtree, in document order.
6185///
6186/// Depth-bounded for the same reason the DOM conversion is: this reads files
6187/// nothing in this build produced.
6188fn collect_style_text(node: &XmlNode, out: &mut Vec<String>, depth: usize) {
6189    if depth >= MAX_XML_NESTING_DEPTH {
6190        return;
6191    }
6192    for child in node.children.as_ref() {
6193        let XmlNodeChild::Element(element) = child else {
6194            continue;
6195        };
6196        if normalize_casing(&element.node_type) == "style" {
6197            let text = element.get_text_content();
6198            if !text.is_empty() {
6199                out.push(text);
6200            }
6201        } else {
6202            collect_style_text(element, out, depth + 1);
6203        }
6204    }
6205}
6206
6207fn xml_node_to_dom_fast<'a>(
6208    xml_node: &'a XmlNode,
6209    component_map: &'a ComponentMap,
6210    inside_svg: bool,
6211    depth: usize,
6212) -> Result<Dom, RenderDomError> {
6213    use crate::dom::Dom;
6214
6215    let component_name = normalize_casing(&xml_node.node_type);
6216
6217    // Look up the component definition
6218    let node_type = tag_to_node_type(&component_name);
6219    let mut dom = Dom::create_node(node_type);
6220
6221    apply_xml_node_attributes(&mut dom.root, xml_node, &component_name, inside_svg);
6222
6223    let child_inside_svg = inside_svg || component_name == "svg";
6224
6225    // AUDIT 2026-07-08: bound recursion depth to avoid a native stack overflow on
6226    // pathologically deep markup. At the cap, this node is emitted without its
6227    // children (truncation) rather than crashing the process.
6228    // AUDIT-TODO: a worklist-based iterative builder would preserve deep subtrees.
6229    if depth >= MAX_XML_NESTING_DEPTH {
6230        return Ok(dom);
6231    }
6232
6233    // Recursively convert children
6234    let mut children = Vec::new();
6235    // A `<style>` found INSIDE the tree - an SVG's own `<defs><style>`, above
6236    // all - is a stylesheet, not content. In azul a stylesheet is an ATTRIBUTE
6237    // of a node (`Dom.css`, scoped to that subtree by `scope_inline_css`)
6238    // rather than a node of its own, so it has to be recognised HERE, at the
6239    // input, and hung on the element that contains it. Leaving it as a node
6240    // rendered the CSS source as visible text.
6241    //
6242    // Scoping to the subtree is exactly right for the case that motivates it:
6243    // an icon's `.ColorScheme-Text { color:… }` is meant for that icon, and
6244    // must not reach the rest of the document.
6245    let mut scoped_css: Vec<Css> = Vec::new();
6246    // An `<svg>`'s stylesheet is SVG-GLOBAL: it is nearly always written in
6247    // `<defs><style>`, and `<defs>` is a definition container that draws
6248    // nothing - attaching the sheet there would scope it to a subtree with no
6249    // shapes in it. Collected from the whole subtree and hung on the `<svg>`,
6250    // which is as global as it should ever get.
6251    if component_name == "svg" {
6252        let mut texts = Vec::new();
6253        collect_style_text(xml_node, &mut texts, 0);
6254        for text in texts {
6255            scoped_css.push(Css::from_string(text.into()));
6256        }
6257    }
6258    for child in xml_node.children.as_ref() {
6259        match child {
6260            XmlNodeChild::Element(child_node)
6261                if normalize_casing(&child_node.node_type) == "style" =>
6262            {
6263                // Never a rendered node. Inside an `<svg>` it was already
6264                // hoisted above; elsewhere it scopes to THIS element.
6265                if component_name != "svg" {
6266                    let text = child_node.get_text_content();
6267                    if !text.is_empty() {
6268                        scoped_css.push(Css::from_string(text.into()));
6269                    }
6270                }
6271            }
6272            XmlNodeChild::Element(child_node) => {
6273                let child_dom =
6274                    xml_node_to_dom_fast(child_node, component_map, child_inside_svg, depth + 1)?;
6275                children.push(child_dom);
6276            }
6277            XmlNodeChild::Text(text) => {
6278                let text_dom = Dom::create_text_do_not_use_without_block_level_wrapper(
6279                    AzString::from(text.as_str()),
6280                );
6281                children.push(text_dom);
6282            }
6283        }
6284    }
6285
6286    if !children.is_empty() {
6287        dom = dom.with_children(children.into());
6288    }
6289
6290    for css in scoped_css {
6291        dom.add_component_css(css);
6292    }
6293
6294    Ok(dom)
6295}
6296
6297/// Builder for arena-based DOM construction (`FastDom`).
6298/// Builds two parallel Vecs (hierarchy + `node_data`) in a single DFS pass.
6299#[derive(Debug)]
6300pub struct CompactDomBuilder {
6301    hierarchy: Vec<crate::styled_dom::NodeHierarchyItem>,
6302    node_data: Vec<crate::dom::NodeData>,
6303    css: Vec<crate::dom::CssWithNodeId>,
6304    /// Stack of (`node_index`, `previous_child_index`) for open elements
6305    stack: Vec<(usize, Option<usize>)>,
6306}
6307
6308impl Default for CompactDomBuilder {
6309    fn default() -> Self {
6310        Self::new()
6311    }
6312}
6313
6314impl CompactDomBuilder {
6315    #[must_use]
6316    pub const fn new() -> Self {
6317        Self {
6318            hierarchy: Vec::new(),
6319            node_data: Vec::new(),
6320            css: Vec::new(),
6321            stack: Vec::new(),
6322        }
6323    }
6324
6325    #[must_use]
6326    pub fn with_capacity(cap: usize) -> Self {
6327        Self {
6328            hierarchy: Vec::with_capacity(cap),
6329            node_data: Vec::with_capacity(cap),
6330            css: Vec::new(),
6331            stack: Vec::new(),
6332        }
6333    }
6334
6335    /// Open a new element node. Must be paired with `close_node()`.
6336    pub fn open_node(&mut self, node_data: crate::dom::NodeData) {
6337        use crate::id::NodeId;
6338        use crate::styled_dom::NodeHierarchyItem;
6339
6340        let idx = self.hierarchy.len();
6341
6342        // Determine parent from stack
6343        let parent_raw = if let Some(&(parent_idx, _)) = self.stack.last() {
6344            NodeId::into_raw(&Some(NodeId::new(parent_idx)))
6345        } else {
6346            0 // No parent (root)
6347        };
6348
6349        // Determine previous sibling from parent's last child tracking
6350        let prev_sibling_raw = if let Some(&(_, prev_child)) = self.stack.last() {
6351            prev_child.map_or(0, |pi| NodeId::into_raw(&Some(NodeId::new(pi))))
6352        } else {
6353            0
6354        };
6355
6356        // If there's a previous sibling, set its next_sibling to us
6357        if let Some(&(_, Some(prev_idx))) = self.stack.last() {
6358            self.hierarchy[prev_idx].next_sibling = NodeId::into_raw(&Some(NodeId::new(idx)));
6359        }
6360
6361        // Update parent's "last seen child" to us
6362        if let Some(parent) = self.stack.last_mut() {
6363            parent.1 = Some(idx);
6364        }
6365
6366        // Push the hierarchy item (last_child will be set in close_node)
6367        self.hierarchy.push(NodeHierarchyItem {
6368            parent: parent_raw,
6369            previous_sibling: prev_sibling_raw,
6370            next_sibling: 0, // Will be set by next sibling's open_node
6371            last_child: 0,   // Will be set in close_node
6372        });
6373        self.node_data.push(node_data);
6374
6375        // Push onto stack: this node is now the "open" element, no children yet
6376        self.stack.push((idx, None));
6377    }
6378
6379    /// Close the current element. Sets the `last_child` pointer.
6380    pub fn close_node(&mut self) {
6381        use crate::id::NodeId;
6382
6383        if let Some((idx, last_child_idx)) = self.stack.pop() {
6384            // Set last_child on this node's hierarchy item
6385            self.hierarchy[idx].last_child =
6386                last_child_idx.map_or(0, |lc| NodeId::into_raw(&Some(NodeId::new(lc))));
6387        }
6388    }
6389
6390    /// Add a leaf node (text, br, hr, etc.) that has no children.
6391    pub fn add_leaf(&mut self, node_data: crate::dom::NodeData) {
6392        self.open_node(node_data);
6393        self.close_node();
6394    }
6395
6396    /// Add a CSS stylesheet scoped to a node ID.
6397    pub fn add_css(&mut self, node_id: usize, css: Css) {
6398        self.css.push(crate::dom::CssWithNodeId { node_id, css });
6399    }
6400
6401    /// Finish building and produce a `FastDom`.
6402    #[must_use]
6403    pub fn finish(self) -> crate::dom::FastDom {
6404        crate::dom::FastDom {
6405            node_hierarchy: self.hierarchy.into(),
6406            node_data: self.node_data.into(),
6407            css: self.css.into(),
6408        }
6409    }
6410}
6411
6412/// Convert an XML node tree into a `FastDom` (arena-based) in a single DFS pass.
6413/// This is the fast path equivalent of `xml_node_to_dom_fast`.
6414#[allow(clippy::result_large_err)]
6415// returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
6416// See xml_node_to_dom_fast: component_map is forwarded for pipeline parity, not read here.
6417#[allow(clippy::only_used_in_recursion)]
6418fn xml_node_to_fast_dom<'a>(
6419    xml_node: &'a XmlNode,
6420    component_map: &'a ComponentMap,
6421    inside_svg: bool,
6422    builder: &mut CompactDomBuilder,
6423    depth: usize,
6424) -> Result<(), RenderDomError> {
6425    use crate::dom::NodeData;
6426
6427    let component_name = normalize_casing(&xml_node.node_type);
6428    let node_type = tag_to_node_type(&component_name);
6429    let mut node_data = NodeData::create_node(node_type);
6430
6431    apply_xml_node_attributes(&mut node_data, xml_node, &component_name, inside_svg);
6432
6433    let child_inside_svg = inside_svg || component_name == "svg";
6434
6435    // Open this node in the builder
6436    builder.open_node(node_data);
6437
6438    // AUDIT 2026-07-08: bound recursion depth to avoid a native stack overflow on
6439    // pathologically deep markup. At the cap, children are dropped (the node is
6440    // still opened+closed) rather than crashing the process.
6441    // AUDIT-TODO: a worklist-based iterative builder would preserve deep subtrees.
6442    if depth < MAX_XML_NESTING_DEPTH {
6443        // Recursively convert children
6444        for child in xml_node.children.as_ref() {
6445            match child {
6446                XmlNodeChild::Element(child_node) => {
6447                    xml_node_to_fast_dom(
6448                        child_node,
6449                        component_map,
6450                        child_inside_svg,
6451                        builder,
6452                        depth + 1,
6453                    )?;
6454                }
6455                XmlNodeChild::Text(text) => {
6456                    builder.add_leaf(
6457                        NodeData::create_text_do_not_use_without_block_level_wrapper(
6458                            AzString::from(text.as_str()),
6459                        ),
6460                    );
6461                }
6462            }
6463        }
6464    }
6465
6466    // Close this node
6467    builder.close_node();
6468
6469    Ok(())
6470}
6471
6472/// Render a DOM from an XML body node using the fast arena-based path.
6473/// Builds a `FastDom` directly (no tree intermediary), then creates `StyledDom`.
6474#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
6475fn render_dom_from_body_node_fast<'a>(
6476    body_node: &'a XmlNode,
6477    mut global_css: Option<Css>,
6478    component_map: &'a ComponentMap,
6479    max_width: Option<f32>,
6480) -> Result<StyledDom, RenderDomError> {
6481    use crate::dom::{NodeData, NodeType};
6482
6483    let mut builder = CompactDomBuilder::new();
6484
6485    // Build the HTML > Body wrapper + body content in one pass
6486    // Open <html>
6487    builder.open_node(NodeData::create_node(NodeType::Html));
6488    // Open <body> (the body_node content goes inside)
6489    xml_node_to_fast_dom(body_node, component_map, false, &mut builder, 0)?;
6490    // Close <html>
6491    builder.close_node();
6492
6493    // Collect CSS rules from each source.
6494    let mut combined_rules: Vec<CssRuleBlock> = Vec::new();
6495    if let Some(max_width) = max_width {
6496        let max_width_css =
6497            Css::from_string(format!("html {{ max-width: {max_width}px; }}").into());
6498        combined_rules.extend(max_width_css.rules.into_library_owned_vec());
6499    }
6500    let mut combined_keyframes = Vec::new();
6501    if let Some(css) = global_css.take() {
6502        combined_rules.extend(css.rules.into_library_owned_vec());
6503        combined_keyframes.extend(css.keyframes.into_library_owned_vec());
6504    }
6505    let mut combined_css = Css::new(combined_rules);
6506    combined_css.keyframes = combined_keyframes.into();
6507
6508    // Add CSS to the FastDom
6509    let mut fast_dom = builder.finish();
6510    fast_dom.css = vec![crate::dom::CssWithNodeId {
6511        node_id: 0, // Global scope (root)
6512        css: combined_css,
6513    }]
6514    .into();
6515
6516    // Create StyledDom via the fast path (no tree→arena conversion)
6517    let styled = StyledDom::create_from_fast_dom(fast_dom);
6518    Ok(styled)
6519}
6520
6521// render_dom_from_body_node() removed — use render_dom_from_body_node_fast() or str_to_dom()
6522
6523fn set_stringified_attributes(
6524    dom_string: &mut String,
6525    xml_attributes: &XmlAttributeMap,
6526    filtered_xml_attributes: &ComponentArgumentVec,
6527    tabs: usize,
6528) {
6529    let t0 = String::from("    ").repeat(tabs);
6530    let t = String::from("    ").repeat(tabs + 1);
6531
6532    // push ids and classes as chained `.with_id("..")` / `.with_class("..")`
6533    // calls (public builder API; both take `Into<AzString>`, so bare &str works).
6534    let _ = &t;
6535    for id in xml_attributes
6536        .get_key("id")
6537        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6538        .unwrap_or_default()
6539    {
6540        let _ = write!(
6541            dom_string,
6542            "\r\n{}.with_id(\"{}\")",
6543            t0,
6544            format_args_dynamic(id, filtered_xml_attributes)
6545        );
6546    }
6547
6548    for class in xml_attributes
6549        .get_key("class")
6550        .map(|s| s.split_whitespace().collect::<Vec<_>>())
6551        .unwrap_or_default()
6552    {
6553        let _ = write!(
6554            dom_string,
6555            "\r\n{}.with_class(\"{}\")",
6556            t0,
6557            format_args_dynamic(class, filtered_xml_attributes)
6558        );
6559    }
6560
6561    if let Some(focusable) = xml_attributes
6562        .get_key("focusable")
6563        .map(|f| format_args_dynamic(f, filtered_xml_attributes))
6564        .and_then(|f| parse_bool(&f))
6565    {
6566        if focusable {
6567            let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)");
6568        } else {
6569            let _ = write!(
6570                dom_string,
6571                "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6572            );
6573        }
6574    }
6575
6576    if let Some(tab_index) = xml_attributes
6577        .get_key("tabindex")
6578        .map(|val| format_args_dynamic(val, filtered_xml_attributes))
6579        .and_then(|val| val.parse::<isize>().ok())
6580    {
6581        match tab_index {
6582            0 => {
6583                let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)");
6584            }
6585            i if i > 0 => {
6586                let _ = write!(
6587                    dom_string,
6588                    "\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
6589                    t,
6590                    usize::try_from(i).unwrap_or(0)
6591                );
6592            }
6593            _ => {
6594                let _ = write!(
6595                    dom_string,
6596                    "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6597                );
6598            }
6599        }
6600    }
6601}
6602
6603/// Item of a split string - either a variable name (with optional format spec) or a string
6604#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6605pub enum DynamicItem {
6606    /// A variable reference, e.g. {counter} or {counter:?} or {price:.2}
6607    Var {
6608        name: String,
6609        /// Optional format specifier after the colon: "?" for debug, ".2" for precision, etc.
6610        format_spec: Option<String>,
6611    },
6612    Str(String),
6613}
6614
6615/// Splits a string into formatting arguments, supporting format specifiers like `{var:?}`
6616/// ```rust
6617/// # use azul_core::xml::DynamicItem::*;
6618/// # use azul_core::xml::split_dynamic_string;
6619/// let s = "hello {a}, {b}{{ {c} }}";
6620/// let split = split_dynamic_string(s);
6621/// let output = vec![
6622///     Str("hello ".to_string()),
6623///     Var { name: "a".to_string(), format_spec: None },
6624///     Str(", ".to_string()),
6625///     Var { name: "b".to_string(), format_spec: None },
6626///     Str("{ ".to_string()),
6627///     Var { name: "c".to_string(), format_spec: None },
6628///     Str(" }".to_string()),
6629/// ];
6630/// assert_eq!(output, split);
6631/// ```
6632#[must_use]
6633pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
6634    use self::DynamicItem::{Str, Var};
6635
6636    let input: Vec<char> = input.chars().collect();
6637    let input_chars_len = input.len();
6638
6639    let mut items = Vec::new();
6640    let mut current_idx = 0;
6641    let mut last_idx = 0;
6642
6643    while current_idx < input_chars_len {
6644        let c = input[current_idx];
6645        match c {
6646            '{' if input.get(current_idx + 1).copied() != Some('{') => {
6647                // variable start, search until next closing brace or whitespace or end of string
6648                let mut start_offset = 1;
6649                let mut has_found_variable = false;
6650                while let Some(c) = input.get(current_idx + start_offset) {
6651                    if c.is_whitespace() {
6652                        break;
6653                    }
6654                    if *c == '}' && input.get(current_idx + start_offset + 1).copied() != Some('}')
6655                    {
6656                        start_offset += 1;
6657                        has_found_variable = true;
6658                        break;
6659                    }
6660                    start_offset += 1;
6661                }
6662
6663                // advance current_idx accordingly
6664                // on fail, set cursor to end
6665                // set last_idx accordingly
6666                if has_found_variable {
6667                    if last_idx != current_idx {
6668                        items.push(Str(input[last_idx..current_idx].iter().collect()));
6669                    }
6670
6671                    // subtract 1 from start for opening brace, one from end for closing brace
6672                    let var_content: String = input
6673                        [(current_idx + 1)..(current_idx + start_offset - 1)]
6674                        .iter()
6675                        .collect();
6676                    // Split on first ':' to separate variable name from format specifier
6677                    let (var_name, format_spec) = if let Some(colon_pos) = var_content.find(':') {
6678                        let name = var_content[..colon_pos].to_string();
6679                        let spec = var_content[(colon_pos + 1)..].to_string();
6680                        (name, Some(spec))
6681                    } else {
6682                        (var_content, None)
6683                    };
6684                    items.push(Var {
6685                        name: var_name,
6686                        format_spec,
6687                    });
6688                    current_idx += start_offset;
6689                    last_idx = current_idx;
6690                } else {
6691                    current_idx += start_offset;
6692                }
6693            }
6694            _ => {
6695                current_idx += 1;
6696            }
6697        }
6698    }
6699
6700    if current_idx != last_idx {
6701        items.push(Str(input[last_idx..].iter().collect()));
6702    }
6703
6704    for item in &mut items {
6705        // replace {{ with { in strings
6706        if let Str(s) = item {
6707            *s = s.replace("{{", "{").replace("}}", "}");
6708        }
6709    }
6710
6711    items
6712}
6713
6714/// Combines the split string back into its original form while replacing the variables with their
6715/// values
6716///
6717/// let variables = btreemap!{ "a" => "value1", "b" => "value2" };
6718/// [Str("hello "), Var("a"), Str(", "), Var("b"), Str("{ "), Var("c"), Str(" }}")]
6719/// => "hello value1, valuec{ {c} }"
6720fn combine_and_replace_dynamic_items(
6721    input: &[DynamicItem],
6722    variables: &ComponentArgumentVec,
6723) -> String {
6724    let mut s = String::new();
6725
6726    for item in input {
6727        match item {
6728            DynamicItem::Var { name, format_spec } => {
6729                let variable_name = normalize_casing(name.trim());
6730                if let Some(resolved_var) = variables
6731                    .iter()
6732                    .find(|s| s.name.as_str() == variable_name)
6733                    .map(|q| &q.arg_type)
6734                {
6735                    // Format specifiers are applied at compile time, not at runtime replacement
6736                    s.push_str(resolved_var);
6737                } else {
6738                    s.push('{');
6739                    s.push_str(name);
6740                    if let Some(spec) = format_spec {
6741                        s.push(':');
6742                        s.push_str(spec);
6743                    }
6744                    s.push('}');
6745                }
6746            }
6747            DynamicItem::Str(dynamic_str) => {
6748                s.push_str(dynamic_str);
6749            }
6750        }
6751    }
6752
6753    s
6754}
6755
6756/// Given a string and a key => value mapping, replaces parts of the string with the value, i.e.:
6757///
6758/// ```rust
6759/// # use azul_core::xml::{format_args_dynamic, ComponentArgument, ComponentArgumentVec};
6760/// # use azul_css::AzString;
6761/// let variables: ComponentArgumentVec = vec![
6762///     ComponentArgument { name: AzString::from("a"), arg_type: AzString::from("value1") },
6763///     ComponentArgument { name: AzString::from("b"), arg_type: AzString::from("value2") },
6764/// ].into();
6765///
6766/// let initial = "hello {a}, {b}{{ {c} }}";
6767/// let expected = "hello value1, value2{ {c} }".to_string();
6768/// assert_eq!(format_args_dynamic(initial, &variables), expected);
6769/// ```
6770///
6771/// Note: the number (0, 1, etc.) is the order of the argument, it is irrelevant for
6772/// runtime formatting, only important for keeping the component / function arguments
6773/// in order when compiling the arguments to Rust code
6774#[must_use]
6775pub fn format_args_dynamic(input: &str, variables: &ComponentArgumentVec) -> String {
6776    let dynamic_str_items = split_dynamic_string(input);
6777    combine_and_replace_dynamic_items(&dynamic_str_items, variables)
6778}
6779
6780/// Decode a numeric character reference body (the part between `&` and `;`),
6781/// e.g. `"#65"` -> `'A'`, `"#x41"` -> `'A'`. Returns `None` if it is not a valid
6782/// numeric reference.
6783fn decode_numeric_entity(entity: &str) -> Option<char> {
6784    let num = entity.strip_prefix('#')?;
6785    let code = if let Some(hex) = num.strip_prefix(['x', 'X']) {
6786        u32::from_str_radix(hex, 16).ok()?
6787    } else {
6788        num.parse::<u32>().ok()?
6789    };
6790    char::from_u32(code)
6791}
6792
6793/// Decode the common HTML/XML entities in a single left-to-right pass.
6794///
6795/// Handles `&lt;` `&gt;` `&amp;` `&quot;` `&apos;` and numeric references
6796/// (`&#NN;` / `&#xHH;`). `&nbsp;` and any unrecognized `&...;` sequence are left
6797/// verbatim. The single pass guarantees `&amp;` never double-decodes a following
6798/// entity. See [`prepare_string`] for why `&nbsp;` is deliberately preserved.
6799fn decode_entities(input: &str) -> String {
6800    // Longest handled entity body is a hex numeric ref like `#x10FFFF` (8 bytes);
6801    // cap the `;` search window so a stray `&` far from a `;` stays cheap.
6802    const MAX_ENTITY_BODY: usize = 12;
6803
6804    let mut out = String::with_capacity(input.len());
6805    let bytes = input.as_bytes();
6806    let mut i = 0;
6807    while i < input.len() {
6808        if bytes[i] == b'&' {
6809            if let Some(semi_rel) = input[i + 1..].find(';') {
6810                if semi_rel <= MAX_ENTITY_BODY {
6811                    let body = &input[i + 1..i + 1 + semi_rel];
6812                    let end = i + 1 + semi_rel; // index of ';'
6813                                                // Leave &nbsp; for the per-line pass in prepare_string.
6814                    if body.eq_ignore_ascii_case("nbsp") {
6815                        out.push_str(&input[i..=end]);
6816                        i = end + 1;
6817                        continue;
6818                    }
6819                    let decoded = match body {
6820                        "lt" => Some('<'),
6821                        "gt" => Some('>'),
6822                        "amp" => Some('&'),
6823                        "quot" => Some('"'),
6824                        "apos" => Some('\''),
6825                        _ => decode_numeric_entity(body),
6826                    };
6827                    if let Some(c) = decoded {
6828                        out.push(c);
6829                        i = end + 1;
6830                        continue;
6831                    }
6832                }
6833            }
6834            // Not a recognized entity: emit the '&' literally.
6835            out.push('&');
6836            i += 1;
6837        } else {
6838            // Copy one whole UTF-8 char (i is always on a char boundary here).
6839            let ch = input[i..].chars().next().unwrap_or('\u{FFFD}');
6840            out.push(ch);
6841            i += ch.len_utf8();
6842        }
6843    }
6844    out
6845}
6846
6847// NOTE: Two sequential returns count as a single return, while single returns get ignored.
6848#[must_use]
6849pub fn prepare_string(input: &str) -> String {
6850    const SPACE: &str = " ";
6851    const RETURN: &str = "\n";
6852
6853    let input = input.trim();
6854
6855    if input.is_empty() {
6856        return String::new();
6857    }
6858
6859    // AUDIT 2026-07-08: previously only `&lt;`/`&gt;` were decoded. Decode the full
6860    // common named-entity set (`&lt;` `&gt;` `&amp;` `&quot;` `&apos;`) plus numeric
6861    // references (`&#NN;` decimal and `&#xHH;` hex) in a single left-to-right pass.
6862    // A single pass is used deliberately so `&amp;` cannot double-decode a following
6863    // entity (e.g. "&amp;lt;" -> literal "&lt;", not "<"). `&nbsp;` is intentionally
6864    // left untouched here so the per-line pass below (which runs AFTER trimming) can
6865    // still turn it into a space that survives leading/trailing trim.
6866    let input = decode_entities(input);
6867
6868    let input_len = input.len();
6869    let mut final_lines: Vec<String> = Vec::new();
6870    let mut last_line_was_empty = false;
6871
6872    for line in input.lines() {
6873        let line = line.trim();
6874        let line = line.replace("&nbsp;", " ");
6875        let current_line_is_empty = line.is_empty();
6876
6877        if !current_line_is_empty {
6878            if last_line_was_empty {
6879                final_lines.push(format!("{RETURN}{line}"));
6880            } else {
6881                final_lines.push(line.to_string());
6882            }
6883        }
6884
6885        last_line_was_empty = current_line_is_empty;
6886    }
6887
6888    let mut target = String::with_capacity(input_len);
6889    for (line_idx, line) in final_lines.iter().enumerate() {
6890        // A joining space goes before every line EXCEPT the first (idx 0) and a
6891        // paragraph break (RETURN-prefixed). The old code also skipped the LAST line,
6892        // which dropped the word boundary for a soft-wrapped final line
6893        // ("Hello\nworld" -> "Helloworld").
6894        if !(line.starts_with(RETURN) || line_idx == 0) {
6895            target.push_str(SPACE);
6896        }
6897        target.push_str(line);
6898    }
6899    target
6900}
6901
6902/// Parses a string ("true" or "false")
6903#[must_use]
6904pub fn parse_bool(input: &str) -> Option<bool> {
6905    match input {
6906        "true" => Some(true),
6907        "false" => Some(false),
6908        _ => None,
6909    }
6910}
6911
6912#[derive(Debug, Clone)]
6913pub struct CssMatcher {
6914    path: Vec<CssPathSelector>,
6915    indices_in_parent: Vec<usize>,
6916    children_length: Vec<usize>,
6917}
6918
6919impl CssMatcher {
6920    fn get_hash(&self) -> u64 {
6921        use core::hash::Hash;
6922
6923        use core::hash::Hasher;
6924
6925        let mut hasher = crate::hash::DefaultHasher::new();
6926        for p in &self.path {
6927            p.hash(&mut hasher);
6928        }
6929        hasher.finish()
6930    }
6931}
6932
6933impl CssMatcher {
6934    fn matches(&self, path: &CssPath) -> bool {
6935        use azul_css::css::CssPathSelector::*;
6936
6937        use crate::style::{CssGroupIterator, CssGroupSplitReason};
6938
6939        if self.path.is_empty() {
6940            return false;
6941        }
6942        if path.selectors.as_ref().is_empty() {
6943            return false;
6944        }
6945
6946        // self_matcher is only ever going to contain "Children" selectors, never "DirectChildren"
6947        let mut path_groups = CssGroupIterator::new(path.selectors.as_ref()).collect::<Vec<_>>();
6948        path_groups.reverse();
6949
6950        if path_groups.is_empty() {
6951            return false;
6952        }
6953        let mut self_groups = CssGroupIterator::new(self.path.as_ref()).collect::<Vec<_>>();
6954        self_groups.reverse();
6955        if self_groups.is_empty() {
6956            return false;
6957        }
6958
6959        if self.indices_in_parent.len() != self_groups.len() {
6960            return false;
6961        }
6962        if self.children_length.len() != self_groups.len() {
6963            return false;
6964        }
6965
6966        // self_groups = [ // HTML
6967        //     "body",
6968        //     "div.__azul_native-ribbon-container"
6969        //     "div.__azul_native-ribbon-tabs"
6970        //     "p.home"
6971        // ]
6972        //
6973        // path_groups = [ // CSS
6974        //     ".__azul_native-ribbon-tabs"
6975        //     "div.after-tabs"
6976        // ]
6977
6978        // get the first path group and see if it matches anywhere in the self group
6979        let mut cur_selfgroup_scan = 0;
6980        let mut cur_pathgroup_scan = 0;
6981        let mut valid = false;
6982        let mut path_group = path_groups[cur_pathgroup_scan].clone();
6983
6984        while cur_selfgroup_scan < self_groups.len() {
6985            let mut advance = None;
6986
6987            // scan all remaining path groups
6988            for (id, cg) in self_groups[cur_selfgroup_scan..].iter().enumerate() {
6989                let gm = group_matches(
6990                    &path_group.0,
6991                    &self_groups[cur_selfgroup_scan + id].0,
6992                    self.indices_in_parent[cur_selfgroup_scan + id],
6993                    self.children_length[cur_selfgroup_scan + id],
6994                );
6995
6996                if gm {
6997                    // ok: ".__azul_native-ribbon-tabs" was found within self_groups
6998                    // advance the self_groups by n
6999                    advance = Some(id);
7000                    break;
7001                }
7002            }
7003
7004            match advance {
7005                Some(n) => {
7006                    // group was found in remaining items
7007                    // advance cur_pathgroup_scan by 1 and cur_selfgroup_scan by n
7008                    if cur_pathgroup_scan == path_groups.len() - 1 {
7009                        // last path group
7010                        return cur_selfgroup_scan + n == self_groups.len() - 1;
7011                    }
7012                    cur_pathgroup_scan += 1;
7013                    cur_selfgroup_scan += n;
7014                    path_group = path_groups[cur_pathgroup_scan].clone();
7015                }
7016                None => return false, // group was not found in remaining items
7017            }
7018        }
7019
7020        // only return true if all path_groups matched
7021        cur_pathgroup_scan == path_groups.len() - 1
7022    }
7023}
7024
7025// does p.home match div.after-tabs?
7026// a: div.after-tabs
7027fn group_matches(
7028    a: &[&CssPathSelector],
7029    b: &[&CssPathSelector],
7030    idx_in_parent: usize,
7031    parent_children: usize,
7032) -> bool {
7033    use azul_css::css::{
7034        CssNthChildSelector, CssPathPseudoSelector,
7035        CssPathSelector::{Class, Global, Id, PseudoSelector, Type},
7036    };
7037
7038    for selector in a {
7039        match selector {
7040            // always matches
7041            Global
7042            | PseudoSelector(
7043                CssPathPseudoSelector::Hover
7044                | CssPathPseudoSelector::Active
7045                | CssPathPseudoSelector::Focus
7046                | CssPathPseudoSelector::SeatFocus,
7047            ) => {}
7048
7049            Type(tag) => {
7050                if !b.iter().any(|t| **t == Type(*tag)) {
7051                    return false;
7052                }
7053            }
7054            Class(class) => {
7055                if !b.iter().any(|t| **t == Class(class.clone())) {
7056                    return false;
7057                }
7058            }
7059            Id(id) => {
7060                if !b.iter().any(|t| **t == Id(id.clone())) {
7061                    return false;
7062                }
7063            }
7064            PseudoSelector(CssPathPseudoSelector::First) => {
7065                if idx_in_parent != 0 {
7066                    return false;
7067                }
7068            }
7069            PseudoSelector(CssPathPseudoSelector::Last) => {
7070                if idx_in_parent != parent_children.saturating_sub(1) {
7071                    return false;
7072                }
7073            }
7074            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(i))) => {
7075                if idx_in_parent != *i as usize {
7076                    return false;
7077                }
7078            }
7079            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even)) => {
7080                if !idx_in_parent.is_multiple_of(2) {
7081                    return false;
7082                }
7083            }
7084            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
7085                if idx_in_parent.is_multiple_of(2) {
7086                    return false;
7087                }
7088            }
7089            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
7090                if !idx_in_parent
7091                    .saturating_sub(p.offset as usize)
7092                    .is_multiple_of(p.pattern_repeat as usize)
7093                {
7094                    return false;
7095                }
7096            }
7097
7098            _ => return false, // can't happen
7099        }
7100    }
7101
7102    true
7103}
7104
7105struct CssBlock {
7106    ending: Option<CssPathPseudoSelector>,
7107    block: CssRuleBlock,
7108}
7109
7110#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7111/// # Errors
7112///
7113/// Returns an error if the body node cannot be compiled to Rust code.
7114pub fn compile_body_node_to_rust_code<'a>(
7115    body_node: &'a XmlNode,
7116    component_map: &'a ComponentMap,
7117    extra_blocks: &mut VecContents,
7118    css_blocks: &mut BTreeMap<String, String>,
7119    css: &Css,
7120    mut matcher: CssMatcher,
7121) -> Result<String, CompileError> {
7122    use azul_css::css::CssDeclaration;
7123
7124    let t = "";
7125    let t2 = "    ";
7126    let mut dom_string = String::from("Dom::create_body()");
7127    let node_type = CssPathSelector::Type(NodeTypeTag::Body);
7128    matcher.path.push(node_type);
7129
7130    let ids = body_node
7131        .attributes
7132        .get_key("id")
7133        .map(|s| s.split_whitespace().collect::<Vec<_>>())
7134        .unwrap_or_default();
7135    matcher.path.extend(
7136        ids.into_iter()
7137            .map(|id| CssPathSelector::Id(id.to_string().into())),
7138    );
7139    let classes = body_node
7140        .attributes
7141        .get_key("class")
7142        .map(|s| s.split_whitespace().collect::<Vec<_>>())
7143        .unwrap_or_default();
7144    matcher.path.extend(
7145        classes
7146            .into_iter()
7147            .map(|class| CssPathSelector::Class(class.to_string().into())),
7148    );
7149
7150    let matcher_hash = matcher.get_hash();
7151    let css_blocks_for_this_node = get_css_blocks(css, &matcher);
7152    if !css_blocks_for_this_node.is_empty() {
7153        // Track property types for the helper-const machinery, then emit the
7154        // matched declarations as an inline CSS string. (The old path emitted a
7155        // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` + `.with_inline_css_props`,
7156        // but that API was removed in 32d44ed8a; `.with_css(<str>)` is the
7157        // current equivalent and parses pseudo blocks too.)
7158        for css_block in &css_blocks_for_this_node {
7159            for declaration in css_block.block.declarations.as_ref() {
7160                let prop = match declaration {
7161                    CssDeclaration::Static(s) => s,
7162                    CssDeclaration::Dynamic(d) => &d.default_value,
7163                };
7164                extra_blocks.insert_from_css_property(prop);
7165            }
7166        }
7167
7168        let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
7169        if !inline_css.is_empty() {
7170            let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7171            let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
7172        }
7173        let _ = (&mut *css_blocks, matcher_hash); // retained for signature compat
7174    }
7175
7176    if !body_node.children.as_ref().is_empty() {
7177        use azul_css::codegen::format::GetHash;
7178        let children_hash = body_node.children.as_ref().get_hash();
7179        dom_string.push_str("\r\n.with_children(vec![\r\n");
7180
7181        for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
7182            match child {
7183                XmlNodeChild::Element(child_node) => {
7184                    let mut matcher = matcher.clone();
7185                    matcher.path.push(CssPathSelector::Children);
7186                    matcher.indices_in_parent.push(child_idx);
7187                    matcher.children_length.push(body_node.children.len());
7188
7189                    let _ = write!(
7190                        dom_string,
7191                        "{}{},\r\n",
7192                        t,
7193                        compile_node_to_rust_code_inner(
7194                            child_node,
7195                            component_map,
7196                            1,
7197                            extra_blocks,
7198                            css_blocks,
7199                            css,
7200                            matcher,
7201                        )?
7202                    );
7203                }
7204                XmlNodeChild::Text(text) => {
7205                    let text = text.trim();
7206                    if !text.is_empty() {
7207                        let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
7208                        let _ = write!(dom_string,
7209                            "{t}Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"),\r\n"
7210                        );
7211                    }
7212                }
7213            }
7214        }
7215        let _ = write!(dom_string, "\r\n{t}])");
7216    }
7217
7218    let dom_string = dom_string.trim();
7219    Ok(dom_string.to_string())
7220}
7221
7222/// Serialize the CSS blocks matched for a node into one inline CSS string for
7223/// `Dom::with_css(...)`. `with_css` parses via `Css::parse_inline`, which runs
7224/// the full selector+nesting machinery, so `:hover`/`:active`/`:focus` are
7225/// emitted as nested pseudo blocks and round-trip faithfully; plain rules are
7226/// emitted flat as `key: value;` (via `CssProperty::key()` / `value()`).
7227fn css_blocks_to_inline_string(blocks: &[CssBlock]) -> String {
7228    fn decls_of(block: &CssBlock) -> Vec<String> {
7229        block
7230            .block
7231            .declarations
7232            .as_ref()
7233            .iter()
7234            .map(|d| {
7235                let prop = match d {
7236                    CssDeclaration::Static(s) => s,
7237                    CssDeclaration::Dynamic(dy) => &dy.default_value,
7238                };
7239                format!("{}: {};", prop.key(), prop.value())
7240            })
7241            .collect()
7242    }
7243
7244    let mut normal: Vec<String> = Vec::new();
7245    let mut pseudo: Vec<String> = Vec::new();
7246    for block in blocks {
7247        let pseudo_sel = match block.ending {
7248            Some(CssPathPseudoSelector::Hover) => Some(":hover"),
7249            Some(CssPathPseudoSelector::Active) => Some(":active"),
7250            Some(CssPathPseudoSelector::Focus) => Some(":focus"),
7251            Some(CssPathPseudoSelector::SeatFocus) => Some(":seat-focus"),
7252            _ => None,
7253        };
7254        match pseudo_sel {
7255            None => normal.extend(decls_of(block)),
7256            Some(sel) => pseudo.push(format!("{} {{ {} }}", sel, decls_of(block).join(" "))),
7257        }
7258    }
7259
7260    let mut parts = normal;
7261    parts.extend(pseudo);
7262    parts.join(" ")
7263}
7264
7265fn get_css_blocks(css: &Css, matcher: &CssMatcher) -> Vec<CssBlock> {
7266    let mut blocks = Vec::new();
7267
7268    for css_block in css.rules.as_ref() {
7269        if matcher.matches(&css_block.path) {
7270            let ending = match css_block.path.selectors.as_ref().last() {
7271                Some(CssPathSelector::PseudoSelector(p)) => Some(p.clone()),
7272                _ => None,
7273            };
7274
7275            blocks.push(CssBlock {
7276                ending,
7277                block: css_block.clone(),
7278            });
7279        }
7280    }
7281
7282    blocks
7283}
7284
7285fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
7286    use self::DynamicItem::{Str, Var};
7287    if input.is_empty() {
7288        String::from("AzString::from_const_str(\"\")")
7289    } else if input.len() == 1 {
7290        // common: there is only one "dynamic item" - skip the "format!()" macro
7291        match &input[0] {
7292            Var { name, format_spec } => {
7293                let var_name = normalize_casing(name.trim());
7294                if let Some(spec) = format_spec {
7295                    format!("format!(\"{{:{spec}}}\", {var_name}).into()")
7296                } else {
7297                    var_name
7298                }
7299            }
7300            Str(s) => format!("AzString::from_const_str(\"{s}\")"),
7301        }
7302    } else {
7303        // build a "format!("{var}, blah", var)" string
7304        let mut formatted_str = String::from("format!(\"");
7305        let mut variables = Vec::new();
7306        for item in input {
7307            match item {
7308                Var { name, format_spec } => {
7309                    let variable_name = normalize_casing(name.trim());
7310                    if let Some(spec) = format_spec {
7311                        let _ = write!(formatted_str, "{{{variable_name}:{spec}}}");
7312                    } else {
7313                        let _ = write!(formatted_str, "{{{variable_name}}}");
7314                    }
7315                    variables.push(variable_name.clone());
7316                }
7317                Str(s) => {
7318                    let s = s.replace('"', "\\\"");
7319                    formatted_str.push_str(&s);
7320                }
7321            }
7322        }
7323
7324        formatted_str.push('\"');
7325        if !variables.is_empty() {
7326            formatted_str.push_str(", ");
7327        }
7328
7329        formatted_str.push_str(&variables.join(", "));
7330        formatted_str.push_str(").into()");
7331        formatted_str
7332    }
7333}
7334
7335fn format_args_for_rust_code(input: &str) -> String {
7336    let dynamic_str_items = split_dynamic_string(input);
7337    compile_and_format_dynamic_items(&dynamic_str_items)
7338}
7339
7340#[allow(clippy::result_large_err)]
7341// returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
7342// component_map is forwarded through the codegen recursion for parity with the
7343// component-expanding path; this Rust-codegen path only threads it into recursive calls.
7344#[allow(clippy::only_used_in_recursion)]
7345#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
7346fn compile_node_to_rust_code_inner(
7347    node: &XmlNode,
7348    component_map: &ComponentMap,
7349    tabs: usize,
7350    extra_blocks: &mut VecContents,
7351    css_blocks: &mut BTreeMap<String, String>,
7352    css: &Css,
7353    mut matcher: CssMatcher,
7354) -> Result<String, CompileError> {
7355    use azul_css::css::CssDeclaration;
7356
7357    let t = String::from("    ").repeat(tabs - 1);
7358    let t2 = String::from("    ").repeat(tabs);
7359
7360    let component_name = normalize_casing(&node.node_type);
7361
7362    // Look up the CSS NodeTypeTag
7363    let node_type_tag = tag_to_node_type_tag(&component_name);
7364    let node_type = CssPathSelector::Type(node_type_tag);
7365
7366    // Emit a plain `create_node(<Tag>)` for the base node. Do NOT route through
7367    // the component `compile_fn`: its Rust arm bakes inline text into a
7368    // `.with_children(..)`, which the child-walk below would then OVERWRITE with
7369    // a second `.with_children(..)` — silently dropping the text on any node
7370    // that has BOTH text and element children. The child-walk handles ALL
7371    // children (text + elements) in order, so the base node must stay childless.
7372    // Interactive/data tags (Button/Input/…) whose NodeType carries data fall
7373    // back to `div`, matching the C/C++/Python walkers (`safe_container_tag`).
7374    let ctor = analyze_node_ctor(&component_name, node);
7375    let mut dom_string = ctor.render_rust().map_or_else(
7376        || {
7377            let tag = safe_container_tag(&format!("{:?}", tag_to_node_type(&component_name)));
7378            format!("{t2}Dom::create_node(NodeType::{tag})")
7379        },
7380        |expr| format!("{t2}{expr}"),
7381    );
7382
7383    matcher.path.push(node_type);
7384    let ids = node
7385        .attributes
7386        .get_key("id")
7387        .map(|s| s.split_whitespace().collect::<Vec<_>>())
7388        .unwrap_or_default();
7389
7390    matcher.path.extend(
7391        ids.into_iter()
7392            .map(|id| CssPathSelector::Id(id.to_string().into())),
7393    );
7394
7395    let classes = node
7396        .attributes
7397        .get_key("class")
7398        .map(|s| s.split_whitespace().collect::<Vec<_>>())
7399        .unwrap_or_default();
7400
7401    matcher.path.extend(
7402        classes
7403            .into_iter()
7404            .map(|class| CssPathSelector::Class(class.to_string().into())),
7405    );
7406
7407    let matcher_hash = matcher.get_hash();
7408    let css_blocks_for_this_node = get_css_blocks(css, &matcher);
7409    if !css_blocks_for_this_node.is_empty() {
7410        // Track property types for the helper-const machinery, then emit the
7411        // matched declarations as an inline CSS string. (The old path emitted a
7412        // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` + `.with_inline_css_props`,
7413        // but that API was removed in 32d44ed8a; `.with_css(<str>)` is the
7414        // current equivalent and parses pseudo blocks too.)
7415        for css_block in &css_blocks_for_this_node {
7416            for declaration in css_block.block.declarations.as_ref() {
7417                let prop = match declaration {
7418                    CssDeclaration::Static(s) => s,
7419                    CssDeclaration::Dynamic(d) => &d.default_value,
7420                };
7421                extra_blocks.insert_from_css_property(prop);
7422            }
7423        }
7424
7425        let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
7426        if !inline_css.is_empty() {
7427            let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7428            let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
7429        }
7430        let _ = (&mut *css_blocks, matcher_hash); // retained for signature compat
7431    }
7432
7433    set_stringified_attributes(
7434        &mut dom_string,
7435        &node.attributes,
7436        &ComponentArgumentVec::new(),
7437        tabs,
7438    );
7439
7440    // Text folded into the ctor (Tier A/C) is skipped, as is a `<caption>`
7441    // already injected by `create_table`.
7442    let mut caption_skipped = false;
7443    let mut children_string = node
7444        .children
7445        .as_ref()
7446        .iter()
7447        .enumerate()
7448        .filter_map(|(child_idx, c)| match c {
7449            XmlNodeChild::Element(child_node) => {
7450                if ctor.skip_caption()
7451                    && !caption_skipped
7452                    && child_node
7453                        .node_type
7454                        .as_str()
7455                        .eq_ignore_ascii_case("caption")
7456                {
7457                    caption_skipped = true;
7458                    return None;
7459                }
7460                let mut matcher = matcher.clone();
7461                matcher.path.push(CssPathSelector::Children);
7462                matcher.indices_in_parent.push(child_idx);
7463                matcher.children_length.push(node.children.len());
7464
7465                Some(compile_node_to_rust_code_inner(
7466                    child_node,
7467                    component_map,
7468                    tabs + 1,
7469                    extra_blocks,
7470                    css_blocks,
7471                    css,
7472                    matcher,
7473                ))
7474            }
7475            XmlNodeChild::Text(text) => {
7476                if ctor.consumes_text() {
7477                    return None;
7478                }
7479                let text = text.trim();
7480                if text.is_empty() {
7481                    None
7482                } else {
7483                    let t2 = String::from("    ").repeat(tabs);
7484                    let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
7485                    Some(Ok(format!(
7486                        "{t2}Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\")"
7487                    )))
7488                }
7489            }
7490        })
7491        .collect::<Result<Vec<_>, _>>()?
7492        .join(",\r\n");
7493
7494    if !children_string.is_empty() {
7495        let _ = write!(
7496            dom_string,
7497            "\r\n{t2}.with_children(vec![\r\n{children_string}\r\n{t2}])"
7498        );
7499    }
7500
7501    Ok(dom_string)
7502}
7503
7504// ───────────────────────────────────────────────────────────────────────────
7505// Generic FLUENT DOM-builder emitter (C++ / Python).
7506//
7507// Rust has its own dedicated walker above (`compile_*_to_rust_code`). C++ and
7508// Python share this generic walker because their builder APIs are also fluent
7509// (`Dom::create_*().with_css(..).with_child(..)`); only the surface tokens
7510// differ, captured in `FluentSyntax`. Plain C is imperative and has its own
7511// walker (`compile_*_to_c_code`).
7512// ───────────────────────────────────────────────────────────────────────────
7513
7514/// Tags with a zero-arg per-tag creator (`create_<tag>()` / `AzDom_create<Tag>()`
7515/// / `create_node(NodeType::<Tag>)`). Interactive / data elements (Button, Input,
7516/// Img, Select, Textarea, Label, A, Table, …) take constructor arguments, so an
7517/// exported page maps them to a plain `div` container (structure preserved; the
7518/// user re-wires behavior). Keep these CamelCase to match `NodeTypeTag` debug names.
7519const SAFE_CONTAINER_TAGS: &[&str] = &[
7520    // These must match the real `NodeType` Debug names exactly (the lookup below is a
7521    // string compare against `{:?}`). Six used to be mis-cased — "Blockquote",
7522    // "Colgroup", "Figcaption", "Tbody", "Tfoot", "Thead" — so those tags silently
7523    // degraded to "Div".
7524    "Abbr",
7525    "Acronym",
7526    "Address",
7527    "Article",
7528    "Aside",
7529    "B",
7530    "Bdi",
7531    "Bdo",
7532    "Big",
7533    "BlockQuote",
7534    "Body",
7535    "Br",
7536    "Caption",
7537    "Cite",
7538    "Code",
7539    "ColGroup",
7540    "Dd",
7541    "Del",
7542    "Dfn",
7543    "Dir",
7544    "Div",
7545    "Dl",
7546    "Dt",
7547    "Em",
7548    "Embed",
7549    "FigCaption",
7550    "Figure",
7551    "Footer",
7552    "H1",
7553    "H2",
7554    "H3",
7555    "H4",
7556    "H5",
7557    "H6",
7558    "Head",
7559    "Header",
7560    "Hr",
7561    "Html",
7562    "I",
7563    "Ins",
7564    "Kbd",
7565    "Li",
7566    "Link",
7567    "Main",
7568    "Map",
7569    "Mark",
7570    "Meta",
7571    "Nav",
7572    "Object",
7573    "Ol",
7574    "P",
7575    "Pre",
7576    "Q",
7577    "Rp",
7578    "Rt",
7579    "Rtc",
7580    "Ruby",
7581    "S",
7582    "Samp",
7583    "Script",
7584    "Section",
7585    "Small",
7586    "Span",
7587    "Strong",
7588    "Style",
7589    "Sub",
7590    "Sup",
7591    "Svg",
7592    "TBody",
7593    "Td",
7594    "TFoot",
7595    "Th",
7596    "THead",
7597    "Title",
7598    "Tr",
7599    "U",
7600    "Ul",
7601    "Var",
7602    "Wbr",
7603];
7604
7605/// The CamelCase tag to actually emit a creator for: the tag itself if it has a
7606/// zero-arg creator, else `"Div"`.
7607fn safe_container_tag(tag_dbg: &str) -> &'static str {
7608    SAFE_CONTAINER_TAGS
7609        .iter()
7610        .copied()
7611        .find(|t| *t == tag_dbg)
7612        .unwrap_or("Div")
7613}
7614
7615// ───────────────────────────────────────────────────────────────────────────
7616// Semantic / accessibility-aware constructor selection.
7617//
7618// Instead of mapping every element to a plain `div`, an exported live page
7619// picks the *most specific* Azul constructor so the generated app keeps the
7620// page's semantics + accessibility tree:
7621//
7622//   • Tier A  `create_<tag>_with_text(text)` — a tag with a single text child
7623//             and no element children (P, Span, H1-H6, Li, Td, Code, …).
7624//   • Tier B  aria-only / void widgets (Details, Summary, Form, Canvas, Area,
7625//             …) — `create_<tag>(SmallAriaInfo::label(..))` when `aria-label`
7626//             is present, else `create_<tag>_no_a11y()`.
7627//   • Tier C  multi-arg widgets (Button, A, Label, Input, Select, Option,
7628//             Optgroup, Textarea, Table) — args pulled from HTML attributes.
7629//   • Tier D  scalar-driven widgets (Progress, Meter, Dialog) — the `*_no_a11y`
7630//             form with extracted numeric args (the full aria structs are
7631//             complex; the NoA11y form is simplest + correct).
7632//
7633// Every symbol emitted here is verified to exist in `target/codegen/azul.h` (C)
7634// and `azul20.hpp` (C++); anything else falls back to `safe_container_tag`
7635// (`div`). The four walkers share `analyze_node_ctor` and each renders the
7636// result with its own surface tokens.
7637// ───────────────────────────────────────────────────────────────────────────
7638
7639/// A single positional argument of a semantic constructor. String payloads are
7640/// RAW — escaping happens at render time (matching the walkers).
7641#[derive(Debug, Clone)]
7642enum CtorArg {
7643    /// Plain string literal (`AzString` / `String` / `"…"`).
7644    Str(String),
7645    /// `SmallAriaInfo` built from an accessible label.
7646    Aria(String),
7647    /// `f32` numeric literal.
7648    Float(f32),
7649    /// `OptionString::Some(text)`.
7650    OptSome(String),
7651    /// `OptionString::None`.
7652    OptNone,
7653}
7654
7655/// The constructor chosen for an element node.
7656enum NodeCtor {
7657    /// Plain container — keep each walker's existing `create_<tag>()` path.
7658    Plain,
7659    /// A specific semantic constructor.
7660    Semantic {
7661        /// Canonical CamelCase suffix after `create` / `AzDom_create`
7662        /// (e.g. `Button`, `ButtonNoA11y`, `PWithText`, `A`, `ANoA11y`).
7663        suffix: String,
7664        args: Vec<CtorArg>,
7665        /// The node's direct text is folded into the ctor — skip text children
7666        /// in the walk so it isn't emitted twice.
7667        consumes_text: bool,
7668        /// The table aria form injects its own `<caption>` child — drop the
7669        /// first literal `<caption>` element so it isn't duplicated.
7670        skip_caption: bool,
7671    },
7672}
7673
7674/// Uppercase the first character (`button` → `Button`, `h1` → `H1`). HTML tags
7675/// are single lowercase tokens, so this yields the exact `AzDom_create<Suffix>`
7676/// spelling.
7677fn cap_first(tag: &str) -> String {
7678    let mut c = tag.chars();
7679    c.next().map_or_else(String::new, |f| {
7680        f.to_uppercase().collect::<String>() + c.as_str()
7681    })
7682}
7683
7684/// CamelCase → `snake_case` for the C++/Python/Rust method names
7685/// (`ButtonNoA11y` → `button_no_a11y`, `PWithText` → `p_with_text`,
7686/// `ANoA11y` → `a_no_a11y`, `H1WithText` → `h1_with_text`).
7687fn camel_to_snake(s: &str) -> String {
7688    let chars: Vec<char> = s.chars().collect();
7689    let mut out = String::new();
7690    for (i, &ch) in chars.iter().enumerate() {
7691        if ch.is_ascii_uppercase() && i > 0 {
7692            let prev = chars[i - 1];
7693            let next_lower = chars.get(i + 1).is_some_and(char::is_ascii_lowercase);
7694            if prev.is_ascii_lowercase()
7695                || prev.is_ascii_digit()
7696                || (prev.is_ascii_uppercase() && next_lower)
7697            {
7698                out.push('_');
7699            }
7700        }
7701        out.extend(ch.to_lowercase());
7702    }
7703    out
7704}
7705
7706/// Escape `\` and `"` for a double-quoted string literal.
7707fn esc_lit(s: &str) -> String {
7708    s.replace('\\', "\\\\").replace('"', "\\\"")
7709}
7710
7711/// Format an `f32` as a valid float literal with a decimal point (`1` → `1.0`).
7712fn fmt_f32_lit(f: f32) -> String {
7713    let s = format!("{f}");
7714    if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") {
7715        s
7716    } else {
7717        format!("{s}.0")
7718    }
7719}
7720
7721/// Joined, trimmed text of a node's *direct* text children (`"  Go  "` → `"Go"`).
7722fn node_direct_text(node: &XmlNode) -> String {
7723    node.children
7724        .as_ref()
7725        .iter()
7726        .filter_map(|c| match c {
7727            XmlNodeChild::Text(t) => {
7728                let t = t.trim();
7729                if t.is_empty() {
7730                    None
7731                } else {
7732                    Some(t.to_string())
7733                }
7734            }
7735            XmlNodeChild::Element(_) => None,
7736        })
7737        .collect::<Vec<_>>()
7738        .join(" ")
7739}
7740
7741/// Non-empty `aria-label` attribute value, if present.
7742fn node_aria_label(node: &XmlNode) -> Option<String> {
7743    node.attributes.get_key("aria-label").and_then(|v| {
7744        let v = v.as_str().trim();
7745        if v.is_empty() {
7746            None
7747        } else {
7748            Some(v.to_string())
7749        }
7750    })
7751}
7752
7753/// Attribute value, or `default` when absent.
7754fn node_attr_or(node: &XmlNode, key: &str, default: &str) -> String {
7755    node.attributes
7756        .get_key(key)
7757        .map_or_else(|| default.to_string(), |v| v.as_str().to_string())
7758}
7759
7760/// Attribute parsed as `f32`, or `default` when absent / unparsable.
7761fn node_attr_f32(node: &XmlNode, key: &str, default: f32) -> f32 {
7762    node.attributes
7763        .get_key(key)
7764        .and_then(|v| v.as_str().trim().parse::<f32>().ok())
7765        .unwrap_or(default)
7766}
7767
7768/// Text of the node's first `<caption>` element child, if any (non-empty).
7769fn first_caption_text(node: &XmlNode) -> Option<String> {
7770    node.children.as_ref().iter().find_map(|c| match c {
7771        XmlNodeChild::Element(e) if e.node_type.as_str().eq_ignore_ascii_case("caption") => {
7772            let t = e.get_text_content();
7773            let t = t.trim();
7774            if t.is_empty() {
7775                None
7776            } else {
7777                Some(t.to_string())
7778            }
7779        }
7780        _ => None,
7781    })
7782}
7783
7784/// Tags with a single-arg `create_<tag>_with_text(text)` constructor (Tier A).
7785const WITH_TEXT_TAGS: &[&str] = &[
7786    "acronym",
7787    "b",
7788    "bdi",
7789    "bdo",
7790    "big",
7791    "blockquote",
7792    "cite",
7793    "code",
7794    "del",
7795    "dfn",
7796    "em",
7797    "h1",
7798    "h2",
7799    "h3",
7800    "h4",
7801    "h5",
7802    "h6",
7803    "i",
7804    "ins",
7805    "kbd",
7806    "li",
7807    "mark",
7808    "p",
7809    "pre",
7810    "rp",
7811    "rt",
7812    "s",
7813    "samp",
7814    "small",
7815    "span",
7816    "strong",
7817    "style",
7818    "sub",
7819    "sup",
7820    "td",
7821    "th",
7822    "title",
7823    "u",
7824    "var",
7825];
7826
7827/// Pick the semantic constructor for `tag` (lowercase HTML tag) + `node`.
7828#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
7829fn analyze_node_ctor(tag: &str, node: &XmlNode) -> NodeCtor {
7830    // Helper for the common "no caption skip" case.
7831    fn sem(suffix: impl Into<String>, args: Vec<CtorArg>, consumes_text: bool) -> NodeCtor {
7832        NodeCtor::Semantic {
7833            suffix: suffix.into(),
7834            args,
7835            consumes_text,
7836            skip_caption: false,
7837        }
7838    }
7839
7840    let aria = node_aria_label(node);
7841    let has_aria = aria.is_some();
7842    let label = aria.unwrap_or_default();
7843    // `has_only_text_children()` is also true for childless nodes; pair it with
7844    // `has_text` so empty elements stay plain containers.
7845    let pure_text = node.has_only_text_children();
7846    let text = node_direct_text(node);
7847    let has_text = !text.is_empty();
7848    let cap = cap_first(tag);
7849
7850    // Tier A — *_with_text (single text child, no element children).
7851    if WITH_TEXT_TAGS.contains(&tag) {
7852        if pure_text && has_text {
7853            return sem(format!("{cap}WithText"), vec![CtorArg::Str(text)], true);
7854        }
7855        return NodeCtor::Plain;
7856    }
7857
7858    match tag {
7859        // Tier B — aria-only / void widgets.
7860        "details" | "form" | "fieldset" | "legend" | "menu" | "output" | "datalist" | "canvas"
7861        | "audio" | "video" | "area" => {
7862            if has_aria {
7863                sem(cap, vec![CtorArg::Aria(label)], false)
7864            } else {
7865                sem(format!("{cap}NoA11y"), vec![], false)
7866            }
7867        }
7868        // Summary is Tier B but also has a WithText form for a single text child.
7869        "summary" => {
7870            if pure_text && has_text {
7871                if has_aria {
7872                    sem(
7873                        "SummaryWithText",
7874                        vec![CtorArg::Str(text), CtorArg::Aria(label)],
7875                        true,
7876                    )
7877                } else {
7878                    sem("SummaryWithTextNoA11y", vec![CtorArg::Str(text)], true)
7879                }
7880            } else if has_aria {
7881                sem("Summary", vec![CtorArg::Aria(label)], false)
7882            } else {
7883                sem("SummaryNoA11y", vec![], false)
7884            }
7885        }
7886
7887        // Tier C — multi-arg widgets (args from HTML attributes).
7888        "button" => {
7889            if has_aria {
7890                sem(
7891                    "Button",
7892                    vec![CtorArg::Str(text), CtorArg::Aria(label)],
7893                    true,
7894                )
7895            } else {
7896                sem("ButtonNoA11y", vec![CtorArg::Str(text)], true)
7897            }
7898        }
7899        "a" => {
7900            let href = node_attr_or(node, "href", "");
7901            if has_aria {
7902                sem(
7903                    "A",
7904                    vec![CtorArg::Str(href), CtorArg::Str(text), CtorArg::Aria(label)],
7905                    true,
7906                )
7907            } else {
7908                let lbl = if has_text {
7909                    CtorArg::OptSome(text)
7910                } else {
7911                    CtorArg::OptNone
7912                };
7913                sem("ANoA11y", vec![CtorArg::Str(href), lbl], true)
7914            }
7915        }
7916        "label" => {
7917            let for_id = node_attr_or(node, "for", "");
7918            if has_aria {
7919                sem(
7920                    "Label",
7921                    vec![
7922                        CtorArg::Str(for_id),
7923                        CtorArg::Str(text),
7924                        CtorArg::Aria(label),
7925                    ],
7926                    true,
7927                )
7928            } else {
7929                sem(
7930                    "LabelNoA11y",
7931                    vec![CtorArg::Str(for_id), CtorArg::Str(text)],
7932                    true,
7933                )
7934            }
7935        }
7936        "input" => {
7937            let ty = node_attr_or(node, "type", "text");
7938            let name = node_attr_or(node, "name", "");
7939            if has_aria {
7940                sem(
7941                    "Input",
7942                    vec![
7943                        CtorArg::Str(ty),
7944                        CtorArg::Str(name),
7945                        CtorArg::Str(label.clone()),
7946                        CtorArg::Aria(label),
7947                    ],
7948                    false,
7949                )
7950            } else {
7951                sem(
7952                    "InputNoA11y",
7953                    vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label)],
7954                    false,
7955                )
7956            }
7957        }
7958        "textarea" => {
7959            let name = node_attr_or(node, "name", "");
7960            if has_aria {
7961                sem(
7962                    "Textarea",
7963                    vec![
7964                        CtorArg::Str(name),
7965                        CtorArg::Str(label.clone()),
7966                        CtorArg::Aria(label),
7967                    ],
7968                    false,
7969                )
7970            } else {
7971                sem(
7972                    "TextareaNoA11y",
7973                    vec![CtorArg::Str(name), CtorArg::Str(label)],
7974                    false,
7975                )
7976            }
7977        }
7978        "select" => {
7979            let name = node_attr_or(node, "name", "");
7980            if has_aria {
7981                sem(
7982                    "Select",
7983                    vec![
7984                        CtorArg::Str(name),
7985                        CtorArg::Str(label.clone()),
7986                        CtorArg::Aria(label),
7987                    ],
7988                    false,
7989                )
7990            } else {
7991                sem(
7992                    "SelectNoA11y",
7993                    vec![CtorArg::Str(name), CtorArg::Str(label)],
7994                    false,
7995                )
7996            }
7997        }
7998        "option" => {
7999            let value = node_attr_or(node, "value", "");
8000            if has_aria {
8001                sem(
8002                    "Option",
8003                    vec![
8004                        CtorArg::Str(value),
8005                        CtorArg::Str(text),
8006                        CtorArg::Aria(label),
8007                    ],
8008                    true,
8009                )
8010            } else {
8011                sem(
8012                    "OptionNoA11y",
8013                    vec![CtorArg::Str(value), CtorArg::Str(text)],
8014                    true,
8015                )
8016            }
8017        }
8018        "optgroup" => {
8019            let lbl = node_attr_or(node, "label", "");
8020            if has_aria {
8021                sem(
8022                    "Optgroup",
8023                    vec![CtorArg::Str(lbl), CtorArg::Aria(label)],
8024                    false,
8025                )
8026            } else {
8027                sem("OptgroupNoA11y", vec![CtorArg::Str(lbl)], false)
8028            }
8029        }
8030        "table" => {
8031            if has_aria {
8032                // The aria form injects a caption child, so take the caption from
8033                // the literal <caption> (or the aria label) and drop the literal.
8034                let caption = first_caption_text(node).unwrap_or_else(|| label.clone());
8035                NodeCtor::Semantic {
8036                    suffix: "Table".to_string(),
8037                    args: vec![CtorArg::Str(caption), CtorArg::Aria(label)],
8038                    consumes_text: false,
8039                    skip_caption: true,
8040                }
8041            } else {
8042                sem("TableNoA11y", vec![], false)
8043            }
8044        }
8045
8046        // Tier D — scalar-driven widgets (NoA11y form with extracted numbers).
8047        "progress" => sem(
8048            "ProgressNoA11y",
8049            vec![
8050                CtorArg::Float(node_attr_f32(node, "value", 0.0)),
8051                CtorArg::Float(node_attr_f32(node, "max", 1.0)),
8052            ],
8053            false,
8054        ),
8055        "meter" => sem(
8056            "MeterNoA11y",
8057            vec![
8058                CtorArg::Float(node_attr_f32(node, "value", 0.0)),
8059                CtorArg::Float(node_attr_f32(node, "min", 0.0)),
8060                CtorArg::Float(node_attr_f32(node, "max", 1.0)),
8061            ],
8062            false,
8063        ),
8064        "dialog" => sem("DialogNoA11y", vec![], false),
8065
8066        _ => NodeCtor::Plain,
8067    }
8068}
8069
8070impl CtorArg {
8071    /// Rust expression for this argument (`AzString::from(..)` works for both the
8072    /// `Into<AzString>` and the concrete `AzString` parameter forms).
8073    fn render_rust(&self) -> String {
8074        match self {
8075            Self::Str(s) => format!("AzString::from(\"{}\")", esc_lit(s)),
8076            Self::Aria(s) => format!("SmallAriaInfo::label(AzString::from(\"{}\"))", esc_lit(s)),
8077            Self::Float(f) => fmt_f32_lit(*f),
8078            Self::OptSome(s) => format!("OptionString::Some(AzString::from(\"{}\"))", esc_lit(s)),
8079            Self::OptNone => "OptionString::None".to_string(),
8080        }
8081    }
8082    fn render_c(&self) -> String {
8083        match self {
8084            Self::Str(s) => format!("AZ_STR(\"{}\")", esc_lit(s)),
8085            Self::Aria(s) => format!("AzSmallAriaInfo_label(AZ_STR(\"{}\"))", esc_lit(s)),
8086            Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
8087            Self::OptSome(s) => format!("AzOptionString_some(AZ_STR(\"{}\"))", esc_lit(s)),
8088            Self::OptNone => "AzOptionString_none()".to_string(),
8089        }
8090    }
8091    fn render_cpp(&self) -> String {
8092        match self {
8093            Self::Str(s) => format!("String(\"{}\")", esc_lit(s)),
8094            Self::Aria(s) => format!("SmallAriaInfo::label(String(\"{}\"))", esc_lit(s)),
8095            Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
8096            Self::OptSome(s) => format!("OptionString::some(String(\"{}\"))", esc_lit(s)),
8097            Self::OptNone => "OptionString::none()".to_string(),
8098        }
8099    }
8100    fn render_python(&self) -> String {
8101        match self {
8102            Self::Str(s) => format!("\"{}\"", esc_lit(s)),
8103            Self::Aria(s) => format!("azul.SmallAriaInfo.label(\"{}\")", esc_lit(s)),
8104            Self::Float(f) => fmt_f32_lit(*f),
8105            Self::OptSome(s) => format!("azul.OptionString.some(\"{}\")", esc_lit(s)),
8106            Self::OptNone => "azul.OptionString.none()".to_string(),
8107        }
8108    }
8109}
8110
8111impl NodeCtor {
8112    const fn consumes_text(&self) -> bool {
8113        matches!(
8114            self,
8115            Self::Semantic {
8116                consumes_text: true,
8117                ..
8118            }
8119        )
8120    }
8121    const fn skip_caption(&self) -> bool {
8122        matches!(
8123            self,
8124            Self::Semantic {
8125                skip_caption: true,
8126                ..
8127            }
8128        )
8129    }
8130    /// `Dom::create_…(args)` for Rust, or `None` for a plain container.
8131    fn render_rust(&self) -> Option<String> {
8132        match self {
8133            Self::Plain => None,
8134            Self::Semantic { suffix, args, .. } => Some(format!(
8135                "Dom::create_{}({})",
8136                camel_to_snake(suffix),
8137                args.iter()
8138                    .map(CtorArg::render_rust)
8139                    .collect::<Vec<_>>()
8140                    .join(", ")
8141            )),
8142        }
8143    }
8144    /// `AzDom_create…(args)` for C, or `None` for a plain container.
8145    fn render_c(&self) -> Option<String> {
8146        match self {
8147            Self::Plain => None,
8148            Self::Semantic { suffix, args, .. } => Some(format!(
8149                "AzDom_create{}({})",
8150                suffix,
8151                args.iter()
8152                    .map(CtorArg::render_c)
8153                    .collect::<Vec<_>>()
8154                    .join(", ")
8155            )),
8156        }
8157    }
8158    /// Fluent `Dom::create_…` (C++) / `azul.Dom.create_…` (Python), or `None`.
8159    fn render_fluent(&self, target: &CompileTarget) -> Option<String> {
8160        match self {
8161            Self::Plain => None,
8162            Self::Semantic { suffix, args, .. } => {
8163                let snake = camel_to_snake(suffix);
8164                let (prefix, rendered) = match target {
8165                    CompileTarget::Cpp => (
8166                        format!("Dom::create_{snake}"),
8167                        args.iter().map(CtorArg::render_cpp).collect::<Vec<_>>(),
8168                    ),
8169                    CompileTarget::Python => (
8170                        format!("azul.Dom.create_{snake}"),
8171                        args.iter().map(CtorArg::render_python).collect::<Vec<_>>(),
8172                    ),
8173                    _ => return None,
8174                };
8175                Some(format!("{}({})", prefix, rendered.join(", ")))
8176            }
8177        }
8178    }
8179}
8180
8181/// Per-language token hooks for the fluent walker. The `&str` args are already
8182/// escaped for a double-quoted string literal.
8183struct FluentSyntax {
8184    target: CompileTarget,
8185    /// tag debug-name (e.g. "Div") -> full create expression
8186    create_node: fn(&str) -> String,
8187    /// escaped text -> create-text expression
8188    create_text: fn(&str) -> String,
8189    /// escaped css -> `.with_css(..)` call
8190    with_css: fn(&str) -> String,
8191    /// escaped class -> `.with_class(..)` call
8192    with_class: fn(&str) -> String,
8193    /// escaped id -> `.with_id(..)` call
8194    with_id: fn(&str) -> String,
8195    /// escaped child expression -> `.with_child(..)` call (children are chained)
8196    with_child: fn(&str) -> String,
8197}
8198
8199const CPP_SYNTAX: FluentSyntax = FluentSyntax {
8200    target: CompileTarget::Cpp,
8201    // Use per-tag creators (Dom::create_div(), create_p(), create_body(), …)
8202    // — `NodeType` is a tagged union, so `create_node` would need union
8203    // construction; the per-tag creators exist for every common HTML element.
8204    create_node: |tag| alloc::format!("Dom::create_{}()", tag.to_lowercase()),
8205    create_text: |s| {
8206        alloc::format!("Dom::create_text_do_not_use_without_block_level_wrapper(String(\"{s}\"))")
8207    },
8208    with_css: |s| alloc::format!(".with_css(String(\"{s}\"))"),
8209    with_class: |s| alloc::format!(".with_class(String(\"{s}\"))"),
8210    with_id: |s| alloc::format!(".with_id(String(\"{s}\"))"),
8211    with_child: |c| alloc::format!(".with_child({c})"),
8212};
8213
8214const PYTHON_SYNTAX: FluentSyntax = FluentSyntax {
8215    target: CompileTarget::Python,
8216    // Per-tag creators (azul.Dom.create_div(), …) — see CPP_SYNTAX note.
8217    create_node: |tag| alloc::format!("azul.Dom.create_{}()", tag.to_lowercase()),
8218    create_text: |s| {
8219        alloc::format!("azul.Dom.create_text_do_not_use_without_block_level_wrapper(\"{s}\")")
8220    },
8221    with_css: |s| alloc::format!(".with_css(\"{s}\")"),
8222    with_class: |s| alloc::format!(".with_class(\"{s}\")"),
8223    with_id: |s| alloc::format!(".with_id(\"{s}\")"),
8224    with_child: |c| alloc::format!(".with_child({c})"),
8225};
8226
8227/// Walk one element node, emitting a fluent create-expression for `syntax`'s
8228/// language. Mirrors `compile_node_to_rust_code_inner` but token-parameterized.
8229#[allow(clippy::result_large_err)]
8230// returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
8231// See compile_node_to_rust_code_inner: component_map is forwarded for codegen-path parity.
8232#[allow(clippy::only_used_in_recursion)]
8233fn compile_node_fluent(
8234    node: &XmlNode,
8235    syntax: &FluentSyntax,
8236    component_map: &ComponentMap,
8237    css: &Css,
8238    mut matcher: CssMatcher,
8239) -> Result<String, CompileError> {
8240    use azul_css::css::CssDeclaration;
8241
8242    let component_name = normalize_casing(&node.node_type);
8243    let node_type_tag = tag_to_node_type_tag(&component_name);
8244    let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
8245
8246    // Base create-expression. For an exported live page every node is a plain
8247    // HTML element, so emit a per-tag creator directly via the language hooks
8248    // (universal + verified) rather than the per-component `compile_fn`, whose
8249    // C++/Python arms emit stale placeholder syntax (`Dom.div()` etc.).
8250    // Interactive/data tags (whose creators need args) fall back to `div`. Any
8251    // element text shows up as a Text child below and is handled there.
8252    let ctor = analyze_node_ctor(&component_name, node);
8253    let mut s = ctor.render_fluent(&syntax.target).map_or_else(
8254        || (syntax.create_node)(safe_container_tag(&tag_dbg)),
8255        |expr| expr,
8256    );
8257
8258    matcher.path.push(CssPathSelector::Type(node_type_tag));
8259    let ids: Vec<String> = node
8260        .attributes
8261        .get_key("id")
8262        .map(|v| {
8263            v.split_whitespace()
8264                .map(alloc::string::ToString::to_string)
8265                .collect()
8266        })
8267        .unwrap_or_default();
8268    matcher
8269        .path
8270        .extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
8271    let classes: Vec<String> = node
8272        .attributes
8273        .get_key("class")
8274        .map(|v| {
8275            v.split_whitespace()
8276                .map(alloc::string::ToString::to_string)
8277                .collect()
8278        })
8279        .unwrap_or_default();
8280    matcher.path.extend(
8281        classes
8282            .iter()
8283            .map(|c| CssPathSelector::Class(c.clone().into())),
8284    );
8285
8286    // Inline CSS (matched rules -> `.with_css("..")`, pseudo blocks included).
8287    let blocks = get_css_blocks(css, &matcher);
8288    if !blocks.is_empty() {
8289        let inline_css = css_blocks_to_inline_string(&blocks);
8290        if !inline_css.is_empty() {
8291            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
8292            s.push_str(&(syntax.with_css)(&esc));
8293        }
8294    }
8295    for id in &ids {
8296        s.push_str(&(syntax.with_id)(
8297            &id.replace('\\', "\\\\").replace('"', "\\\""),
8298        ));
8299    }
8300    for class in &classes {
8301        s.push_str(&(syntax.with_class)(
8302            &class.replace('\\', "\\\\").replace('"', "\\\""),
8303        ));
8304    }
8305
8306    // Children (chained `.with_child(..)`). Text folded into the ctor (Tier A/C)
8307    // is skipped here, as is a `<caption>` already injected by `create_table`.
8308    let mut caption_skipped = false;
8309    for (child_idx, child) in node.children.as_ref().iter().enumerate() {
8310        match child {
8311            XmlNodeChild::Element(child_node) => {
8312                if ctor.skip_caption()
8313                    && !caption_skipped
8314                    && child_node
8315                        .node_type
8316                        .as_str()
8317                        .eq_ignore_ascii_case("caption")
8318                {
8319                    caption_skipped = true;
8320                    continue;
8321                }
8322                let mut m = matcher.clone();
8323                m.path.push(CssPathSelector::Children);
8324                m.indices_in_parent.push(child_idx);
8325                m.children_length.push(node.children.len());
8326                let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
8327                s.push_str(&(syntax.with_child)(&child_src));
8328            }
8329            XmlNodeChild::Text(text) => {
8330                if ctor.consumes_text() {
8331                    continue;
8332                }
8333                let text = text.trim();
8334                if !text.is_empty() {
8335                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
8336                    s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
8337                }
8338            }
8339        }
8340    }
8341
8342    Ok(s)
8343}
8344
8345/// Build the `<body>` render-expression for `syntax`'s language.
8346#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
8347fn compile_body_fluent<'a>(
8348    body_node: &'a XmlNode,
8349    syntax: &FluentSyntax,
8350    component_map: &'a ComponentMap,
8351    css: &Css,
8352    mut matcher: CssMatcher,
8353) -> Result<String, CompileError> {
8354    let mut s = (syntax.create_node)("Body");
8355    matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
8356    let classes: Vec<String> = body_node
8357        .attributes
8358        .get_key("class")
8359        .map(|v| {
8360            v.split_whitespace()
8361                .map(alloc::string::ToString::to_string)
8362                .collect()
8363        })
8364        .unwrap_or_default();
8365    matcher.path.extend(
8366        classes
8367            .iter()
8368            .map(|c| CssPathSelector::Class(c.clone().into())),
8369    );
8370
8371    let blocks = get_css_blocks(css, &matcher);
8372    if !blocks.is_empty() {
8373        let inline_css = css_blocks_to_inline_string(&blocks);
8374        if !inline_css.is_empty() {
8375            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
8376            s.push_str(&(syntax.with_css)(&esc));
8377        }
8378    }
8379    for class in &classes {
8380        s.push_str(&(syntax.with_class)(
8381            &class.replace('\\', "\\\\").replace('"', "\\\""),
8382        ));
8383    }
8384
8385    for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
8386        match child {
8387            XmlNodeChild::Element(child_node) => {
8388                let mut m = matcher.clone();
8389                m.path.push(CssPathSelector::Children);
8390                m.indices_in_parent.push(child_idx);
8391                m.children_length.push(body_node.children.len());
8392                let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
8393                s.push_str(&(syntax.with_child)(&child_src));
8394            }
8395            XmlNodeChild::Text(text) => {
8396                let text = text.trim();
8397                if !text.is_empty() {
8398                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
8399                    s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
8400                }
8401            }
8402        }
8403    }
8404    Ok(s)
8405}
8406
8407/// Parse the page's `<style>` and seed a matcher rooted at `<body>`. Shared by
8408/// the C++/Python/C entry points (mirrors the head of `str_to_rust_code`).
8409#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
8410/// Returns the body by VALUE: `get_html_node` may have synthesised the `<html>`
8411/// wrapper (a fragment with no root of its own), and a reference into a node
8412/// this function owns cannot outlive it. Codegen, not a hot path - one clone
8413/// of the body subtree per compile.
8414fn parse_page_style_and_body(root_nodes: &[XmlNodeChild]) -> Result<(Css, XmlNode), CompileError> {
8415    let html_node = get_html_node(root_nodes)?;
8416    let body_node = get_body_node(html_node.children.as_ref())?.clone();
8417    let mut global_style = Css::empty();
8418    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
8419        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
8420            let text = style_node.get_text_content();
8421            if !text.is_empty() {
8422                global_style = azul_css::parser2::new_from_str(&text).0;
8423            }
8424        }
8425    }
8426    global_style.sort_by_specificity();
8427    Ok((global_style, body_node))
8428}
8429
8430fn body_matcher(body_node: &XmlNode) -> CssMatcher {
8431    CssMatcher {
8432        path: Vec::new(),
8433        indices_in_parent: vec![0],
8434        children_length: vec![body_node.children.as_ref().len()],
8435    }
8436}
8437
8438/// Compile a full HTML page to a compilable **C++** Azul app.
8439#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
8440/// # Errors
8441///
8442/// Returns an error if the XML cannot be parsed or compiled to C++ code.
8443pub fn str_to_cpp_code<'a>(
8444    root_nodes: &'a [XmlNodeChild],
8445    component_map: &'a ComponentMap,
8446) -> Result<String, CompileError> {
8447    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
8448    let body_node = &body_node;
8449    let render = compile_body_fluent(
8450        body_node,
8451        &CPP_SYNTAX,
8452        component_map,
8453        &global_style,
8454        body_matcher(body_node),
8455    )?;
8456    Ok(alloc::format!(
8457        "// Auto-generated UI source code (C++). Build:\n\
8458         //   clang++ -std=c++20 -I <azul>/target/codegen main.cpp -lazul\n\
8459         #include \"azul20.hpp\"\n\
8460         using namespace azul;\n\n\
8461         struct Data {{}};\n\n\
8462         AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n    \
8463         return {render};\n}}\n\n\
8464         int main() {{\n    \
8465         RefAny data = RefAny::create(Data{{}});\n    \
8466         WindowCreateOptions window = WindowCreateOptions::create(render);\n    \
8467         App app = App::create(std::move(data), AppConfig::default_());\n    \
8468         app.run(std::move(window));\n    \
8469         return 0;\n}}\n"
8470    ))
8471}
8472
8473/// Compile a full HTML page to a compilable **Python** Azul app.
8474#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
8475/// # Errors
8476///
8477/// Returns an error if the XML cannot be parsed or compiled to Python code.
8478pub fn str_to_python_code<'a>(
8479    root_nodes: &'a [XmlNodeChild],
8480    component_map: &'a ComponentMap,
8481) -> Result<String, CompileError> {
8482    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
8483    let body_node = &body_node;
8484    let render = compile_body_fluent(
8485        body_node,
8486        &PYTHON_SYNTAX,
8487        component_map,
8488        &global_style,
8489        body_matcher(body_node),
8490    )?;
8491    Ok(alloc::format!(
8492        "# Auto-generated UI source code (Python). Run: python3 main.py\n\
8493         import azul\n\n\
8494         class Data:\n    pass\n\n\
8495         def render(data, info):\n    return (\n        {}\n    )\n\n\
8496         def main():\n    \
8497         app = azul.App.create(Data(), azul.AppConfig.create())\n    \
8498         window = azul.WindowCreateOptions.create(render)\n    \
8499         app.run(window)\n\n\
8500         if __name__ == \"__main__\":\n    main()\n",
8501        render.replace("\r\n", "\n        ")
8502    ))
8503}
8504
8505// ───────────────────────────────────────────────────────────────────────────
8506// Imperative C emitter. C has no fluent builder: each node is a statement that
8507// creates an `AzDom` local, applies css/class (by-value, returns), and pushes
8508// children via `AzDom_addChild(&parent, child)`. A recursive walk emits the
8509// statements bottom-up and returns the variable name holding each node.
8510// ───────────────────────────────────────────────────────────────────────────
8511
8512/// C per-tag creator suffix: `NodeTypeTag` debug name with first char kept and
8513/// the rest lowercased (`Div`->`Div`, `BlockQuote`->`Blockquote`, `H1`->`H1`),
8514/// matching `AzDom_create<Suffix>` in azul.h.
8515fn c_creator_suffix(tag_dbg: &str) -> String {
8516    let mut chars = tag_dbg.chars();
8517    chars.next().map_or_else(
8518        || "Div".to_string(),
8519        |first| {
8520            let rest: String = chars.as_str().to_lowercase();
8521            alloc::format!("{first}{rest}")
8522        },
8523    )
8524}
8525
8526#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
8527#[allow(clippy::too_many_lines)] // large but cohesive: one branch per node kind
8528fn compile_node_c(
8529    node: &XmlNode,
8530    component_map: &ComponentMap,
8531    css: &Css,
8532    mut matcher: CssMatcher,
8533    counter: &mut usize,
8534    out: &mut String,
8535) -> Result<String, CompileError> {
8536    let _ = component_map;
8537    let component_name = normalize_casing(&node.node_type);
8538    let node_type_tag = tag_to_node_type_tag(&component_name);
8539    let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
8540
8541    let var = alloc::format!("n{}", *counter);
8542    *counter += 1;
8543    let ctor = analyze_node_ctor(&component_name, node);
8544    match ctor.render_c() {
8545        Some(expr) => {
8546            let _ = writeln!(out, "    AzDom {var} = {expr};");
8547        }
8548        None => {
8549            let _ = writeln!(
8550                out,
8551                "    AzDom {} = AzDom_create{}();",
8552                var,
8553                c_creator_suffix(safe_container_tag(&tag_dbg))
8554            );
8555        }
8556    }
8557
8558    matcher.path.push(CssPathSelector::Type(node_type_tag));
8559    let ids: Vec<String> = node
8560        .attributes
8561        .get_key("id")
8562        .map(|v| {
8563            v.split_whitespace()
8564                .map(alloc::string::ToString::to_string)
8565                .collect()
8566        })
8567        .unwrap_or_default();
8568    matcher
8569        .path
8570        .extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
8571    let classes: Vec<String> = node
8572        .attributes
8573        .get_key("class")
8574        .map(|v| {
8575            v.split_whitespace()
8576                .map(alloc::string::ToString::to_string)
8577                .collect()
8578        })
8579        .unwrap_or_default();
8580    matcher.path.extend(
8581        classes
8582            .iter()
8583            .map(|c| CssPathSelector::Class(c.clone().into())),
8584    );
8585
8586    let blocks = get_css_blocks(css, &matcher);
8587    if !blocks.is_empty() {
8588        let inline_css = css_blocks_to_inline_string(&blocks);
8589        if !inline_css.is_empty() {
8590            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
8591            let _ = writeln!(out, "    {var} = AzDom_withCss({var}, AZ_STR(\"{esc}\"));");
8592        }
8593    }
8594    for id in &ids {
8595        let esc = id.replace('\\', "\\\\").replace('"', "\\\"");
8596        let _ = writeln!(out, "    {var} = AzDom_withId({var}, AZ_STR(\"{esc}\"));");
8597    }
8598    for class in &classes {
8599        let esc = class.replace('\\', "\\\\").replace('"', "\\\"");
8600        let _ = writeln!(
8601            out,
8602            "    {var} = AzDom_withClass({var}, AZ_STR(\"{esc}\"));"
8603        );
8604    }
8605
8606    let mut caption_skipped = false;
8607    for (child_idx, child) in node.children.as_ref().iter().enumerate() {
8608        match child {
8609            XmlNodeChild::Element(child_node) => {
8610                if ctor.skip_caption()
8611                    && !caption_skipped
8612                    && child_node
8613                        .node_type
8614                        .as_str()
8615                        .eq_ignore_ascii_case("caption")
8616                {
8617                    caption_skipped = true;
8618                    continue;
8619                }
8620                let mut m = matcher.clone();
8621                m.path.push(CssPathSelector::Children);
8622                m.indices_in_parent.push(child_idx);
8623                m.children_length.push(node.children.len());
8624                let child_var = compile_node_c(child_node, component_map, css, m, counter, out)?;
8625                let _ = writeln!(out, "    AzDom_addChild(&{var}, {child_var});");
8626            }
8627            XmlNodeChild::Text(text) => {
8628                if ctor.consumes_text() {
8629                    continue;
8630                }
8631                let text = text.trim();
8632                if !text.is_empty() {
8633                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
8634                    let _ = writeln!(out,
8635                        "    AzDom_addChild(&{var}, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{esc}\")));"
8636                    );
8637                }
8638            }
8639        }
8640    }
8641    Ok(var)
8642}
8643
8644/// Compile a full HTML page to a compilable **C** Azul app.
8645#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
8646/// # Errors
8647///
8648/// Returns an error if the XML cannot be parsed or compiled to C code.
8649pub fn str_to_c_code<'a>(
8650    root_nodes: &'a [XmlNodeChild],
8651    component_map: &'a ComponentMap,
8652) -> Result<String, CompileError> {
8653    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
8654    let body_node = &body_node;
8655    let mut body = String::new();
8656    let mut counter = 0usize;
8657
8658    // Emit the body as the root node, then its children.
8659    let root = alloc::format!("n{counter}");
8660    counter += 1;
8661    let _ = writeln!(body, "    AzDom {root} = AzDom_createBody();");
8662
8663    let mut matcher = body_matcher(body_node);
8664    matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
8665    let classes: Vec<String> = body_node
8666        .attributes
8667        .get_key("class")
8668        .map(|v| {
8669            v.split_whitespace()
8670                .map(alloc::string::ToString::to_string)
8671                .collect()
8672        })
8673        .unwrap_or_default();
8674    matcher.path.extend(
8675        classes
8676            .iter()
8677            .map(|c| CssPathSelector::Class(c.clone().into())),
8678    );
8679    let blocks = get_css_blocks(&global_style, &matcher);
8680    if !blocks.is_empty() {
8681        let inline_css = css_blocks_to_inline_string(&blocks);
8682        if !inline_css.is_empty() {
8683            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
8684            let _ = writeln!(
8685                body,
8686                "    {root} = AzDom_withCss({root}, AZ_STR(\"{esc}\"));"
8687            );
8688        }
8689    }
8690    for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
8691        match child {
8692            XmlNodeChild::Element(child_node) => {
8693                let mut m = matcher.clone();
8694                m.path.push(CssPathSelector::Children);
8695                m.indices_in_parent.push(child_idx);
8696                m.children_length.push(body_node.children.len());
8697                let child_var = compile_node_c(
8698                    child_node,
8699                    component_map,
8700                    &global_style,
8701                    m,
8702                    &mut counter,
8703                    &mut body,
8704                )?;
8705                let _ = writeln!(body, "    AzDom_addChild(&{root}, {child_var});");
8706            }
8707            XmlNodeChild::Text(text) => {
8708                let text = text.trim();
8709                if !text.is_empty() {
8710                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
8711                    let _ = writeln!(body,
8712                        "    AzDom_addChild(&{root}, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{esc}\")));"
8713                    );
8714                }
8715            }
8716        }
8717    }
8718
8719    Ok(alloc::format!(
8720        "/* Auto-generated UI source code (C). Build:\n\
8721         *   clang -I <azul>/target/codegen main.c -lazul\n */\n\
8722         #include \"azul.h\"\n\
8723         #include <string.h>\n\
8724         #define AZ_STR(s) AzString_copyFromBytes((const uint8_t*)(s), 0, strlen(s))\n\n\
8725         AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n\
8726         {body}    return {root};\n}}\n\n\
8727         int main(void) {{\n    \
8728         AzString data_type = AZ_STR(\"Data\");\n    \
8729         AzRefAny data = AzRefAny_newC((AzGlVoidPtrConst){{ .ptr = NULL }}, 0, 1, 0, data_type, NULL, 0, 0);\n    \
8730         AzApp app = AzApp_create(data, AzAppConfig_create());\n    \
8731         AzWindowCreateOptions window = AzWindowCreateOptions_create(render);\n    \
8732         AzApp_run(&app, window);\n    \
8733         AzApp_delete(&app);\n    \
8734         return 0;\n}}\n"
8735    ))
8736}
8737
8738#[cfg(test)]
8739#[path = "xml_test.rs"]
8740mod xml_test;