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

            
21
use alloc::{
22
    boxed::Box,
23
    collections::BTreeMap,
24
    string::{String, ToString},
25
    vec::Vec,
26
};
27
use core::{fmt, fmt::Write, hash::Hash};
28

            
29
use azul_css::{
30
    css::{
31
        Css, CssDeclaration, CssPath, CssPathPseudoSelector, CssPathSelector, CssRuleBlock,
32
        NodeTypeTag,
33
    },
34
    codegen::format::VecContents,
35
    parser2::{CssParseErrorOwned, ErrorLocation},
36
    props::{
37
        basic::{ColorU, StyleFontFamilyVec},
38
        property::CssProperty,
39
        style::{
40
            NormalizedLinearColorStopVec, NormalizedRadialColorStopVec, StyleBackgroundContentVec,
41
            StyleBackgroundPositionVec, StyleBackgroundRepeatVec, StyleBackgroundSizeVec,
42
            StyleTransformVec,
43
        },
44
    },
45
    AzString, OptionString, StringVec, U8Vec,
46
};
47

            
48
use 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.
57
pub 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)]
62
pub struct XmlTagName {
63
    pub inner: AzString,
64
}
65

            
66
impl From<AzString> for XmlTagName {
67
    fn from(s: AzString) -> Self {
68
        Self { inner: s }
69
    }
70
}
71

            
72
impl From<String> for XmlTagName {
73
734600
    fn from(s: String) -> Self {
74
734600
        Self { inner: s.into() }
75
734600
    }
76
}
77

            
78
impl From<&str> for XmlTagName {
79
115975
    fn from(s: &str) -> Self {
80
115975
        Self { inner: s.into() }
81
115975
    }
82
}
83

            
84
impl core::ops::Deref for XmlTagName {
85
    type Target = AzString;
86
1349403
    fn deref(&self) -> &Self::Target {
87
1349403
        &self.inner
88
1349403
    }
89
}
90

            
91
/// (Unparsed) text content of an XML node, such as the "Hello" in `<button>Hello</button>`.
92
pub 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)]
97
pub struct XmlAttributeMap {
98
    pub inner: StringPairVec,
99
}
100

            
101
impl From<StringPairVec> for XmlAttributeMap {
102
739641
    fn from(v: StringPairVec) -> Self {
103
739641
        Self { inner: v }
104
739641
    }
105
}
106

            
107
impl core::ops::Deref for XmlAttributeMap {
108
    type Target = StringPairVec;
109
1466821
    fn deref(&self) -> &Self::Target {
110
1466821
        &self.inner
111
1466821
    }
112
}
113

            
114
impl core::ops::DerefMut for XmlAttributeMap {
115
18360
    fn deref_mut(&mut self) -> &mut Self::Target {
116
18360
        &mut self.inner
117
18360
    }
118
}
119

            
120
/// Name of a component argument (e.g. `"text"`, `"href"`).
121
type ComponentArgumentName = String;
122
/// Type of a component argument as a string (e.g. `"String"`, `"bool"`).
123
type ComponentArgumentType = String;
124
/// Zero-based position of an argument in the component's argument list.
125
type ComponentArgumentOrder = usize;
126

            
127
/// FFI-safe replacement for `(ComponentArgumentName, ComponentArgumentType)` tuple.
128
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
129
#[repr(C)]
130
pub struct ComponentArgument {
131
    pub name: AzString,
132
    pub arg_type: AzString,
133
}
134

            
135
impl_vec!(
136
    ComponentArgument,
137
    ComponentArgumentVec,
138
    ComponentArgumentVecDestructor,
139
    ComponentArgumentVecDestructorType,
140
    ComponentArgumentVecSlice,
141
    OptionComponentArgument
142
);
143
impl_option!(
144
    ComponentArgument,
145
    OptionComponentArgument,
146
    copy = false,
147
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
148
);
149
impl_vec_debug!(ComponentArgument, ComponentArgumentVec);
150
impl_vec_partialeq!(ComponentArgument, ComponentArgumentVec);
151
impl_vec_eq!(ComponentArgument, ComponentArgumentVec);
152
impl_vec_partialord!(ComponentArgument, ComponentArgumentVec);
153
impl_vec_ord!(ComponentArgument, ComponentArgumentVec);
154
impl_vec_hash!(ComponentArgument, ComponentArgumentVec);
155
impl_vec_clone!(
156
    ComponentArgument,
157
    ComponentArgumentVec,
158
    ComponentArgumentVecDestructor
159
);
160
impl_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)]
165
pub 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"`).
171
type ComponentName = String;
172
/// Compiled source code string for a component.
173
type 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.
177
const 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)]
192
pub enum c_void {}
193

            
194
/// Type of an XML node in the parsed tree.
195
#[repr(C)]
196
#[derive(Debug, Copy, Clone)]
197
pub 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)]
208
pub 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)]
216
pub 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)]
238
pub struct MimeTypeHint {
239
    pub inner: AzString,
240
}
241

            
242
impl MimeTypeHint {
243
11
    #[must_use] pub fn new(s: &str) -> Self {
244
11
        Self {
245
11
            inner: AzString::from(s),
246
11
        }
247
11
    }
248

            
249
2022
    #[must_use] pub fn from_extension(ext: &str) -> Self {
250
2022
        let mime = match ext.to_lowercase().as_str() {
251
            // Images
252
2022
            "png" => "image/png",
253
11
            "jpg" | "jpeg" => "image/jpeg",
254
9
            "gif" => "image/gif",
255
8
            "webp" => "image/webp",
256
8
            "svg" => "image/svg+xml",
257
8
            "ico" => "image/x-icon",
258
8
            "bmp" => "image/bmp",
259
8
            "avif" => "image/avif",
260
            // Fonts
261
8
            "ttf" => "font/ttf",
262
8
            "otf" => "font/otf",
263
8
            "woff" => "font/woff",
264
8
            "woff2" => "font/woff2",
265
7
            "eot" => "application/vnd.ms-fontobject",
266
            // Stylesheets
267
7
            "css" => "text/css",
268
            // Scripts
269
5
            "js" | "mjs" => "application/javascript",
270
            // Video
271
5
            "mp4" => "video/mp4",
272
4
            "webm" => "video/webm",
273
4
            "ogg" => "video/ogg",
274
            // Audio
275
4
            "mp3" => "audio/mpeg",
276
3
            "wav" => "audio/wav",
277
3
            "flac" => "audio/flac",
278
            // Default
279
3
            _ => "application/octet-stream",
280
        };
281
2022
        Self {
282
2022
            inner: AzString::from(mime),
283
2022
        }
284
2022
    }
285
}
286

            
287
impl_option!(
288
    MimeTypeHint,
289
    OptionMimeTypeHint,
290
    copy = false,
291
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
292
);
293

            
294
/// An external resource URL found in an XML/HTML document
295
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
296
#[repr(C)]
297
pub struct ExternalResource {
298
    /// The URL as found in the document (may be relative or absolute)
299
    pub url: AzString,
300
    /// Classification of the resource type
301
    pub kind: ExternalResourceKind,
302
    /// MIME type hint (from type attribute, file extension, or heuristics)
303
    pub mime_type: OptionMimeTypeHint,
304
    /// The HTML element that referenced this resource (e.g., "img", "link", "script")
305
    pub source_element: AzString,
306
    /// The attribute that contained the URL (e.g., "src", "href")
307
    pub source_attribute: AzString,
308
}
309

            
310
impl_option!(
311
    ExternalResource,
312
    OptionExternalResource,
313
    copy = false,
314
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
315
);
316

            
317
impl_vec!(
318
    ExternalResource,
319
    ExternalResourceVec,
320
    ExternalResourceVecDestructor,
321
    ExternalResourceVecDestructorType,
322
    ExternalResourceVecSlice,
323
    OptionExternalResource
324
);
325
impl_vec_mut!(ExternalResource, ExternalResourceVec);
326
impl_vec_debug!(ExternalResource, ExternalResourceVec);
327
impl_vec_partialeq!(ExternalResource, ExternalResourceVec);
328
impl_vec_eq!(ExternalResource, ExternalResourceVec);
329
impl_vec_partialord!(ExternalResource, ExternalResourceVec);
330
impl_vec_ord!(ExternalResource, ExternalResourceVec);
331
impl_vec_hash!(ExternalResource, ExternalResourceVec);
332
impl_vec_clone!(
333
    ExternalResource,
334
    ExternalResourceVec,
335
    ExternalResourceVecDestructor
336
);
337

            
338
/// AUDIT 2026-07-08: maximum XML/HTML nesting depth handled by the recursive
339
/// DOM-build (`xml_node_to_dom_fast`, `xml_node_to_fast_dom`), resource-scan
340
/// (iterative worklist in `scan_external_resources`) and `<body>`-lookup
341
/// (`find_body_recursive`) passes. These bound descent per nesting level, so a pathologically deep
342
/// document (e.g. tens of thousands of nested `<div>`s) would overflow the native
343
/// stack. Beyond this depth, deeper children are ignored rather than crashing.
344
/// 512 is far past any realistic hand-authored markup while staying comfortably
345
/// inside the default thread stack.
346
const MAX_XML_NESTING_DEPTH: usize = 512;
347

            
348
/// AUDIT 2026-07-08: maximum recursion depth for [`ComponentFieldType::parse`],
349
/// which recurses through `Option<..>` / `Vec<..>` wrappers. Caps attacker
350
/// strings such as `"Option<".repeat(100_000)` that would otherwise overflow the
351
/// stack. 64 nested type wrappers is far beyond any real field type.
352
const MAX_TYPE_PARSE_DEPTH: usize = 64;
353

            
354
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
355
#[repr(C)]
356
pub struct Xml {
357
    pub root: XmlNodeChildVec,
358
}
359

            
360
impl Xml {
361
    /// Scan the XML/HTML document for external resource URLs.
362
    ///
363
    /// This function traverses the entire document tree and extracts URLs from:
364
    /// - `<img src="...">` - Images
365
    /// - `<link href="...">` - Stylesheets, icons, fonts
366
    /// - `<script src="...">` - Scripts
367
    /// - `<video src="...">`, `<source src="...">` - Video
368
    /// - `<audio src="...">` - Audio
369
    /// - `<a href="...">` - Links (classified as Unknown)
370
    /// - CSS `url()` in style attributes
371
    /// - `<style>` blocks with @import or `url()`
372
3
    #[must_use] pub fn scan_external_resources(&self) -> ExternalResourceVec {
373
3
        let mut resources = Vec::new();
374

            
375
        // AUDIT 2026-07-08: iterative DFS with an explicit worklist. The old
376
        // per-node recursion overflowed the stack on pathologically deep markup
377
        // (a single-purpose scan frame is large: string lowercasing + closure +
378
        // wide match). An explicit stack keeps memory on the heap; `depth` still
379
        // bounds how deep we descend so unbounded input can't grow the worklist
380
        // without limit.
381
3
        let mut stack: Vec<(&XmlNodeChild, usize)> = Vec::new();
382
8
        for child in self.root.as_ref() {
383
8
            stack.push((child, 0));
384
8
        }
385
524
        while let Some((child, depth)) = stack.pop() {
386
521
            match child {
387
                XmlNodeChild::Text(text) => {
388
                    // CSS @import / url() in text content (inside <style> tags).
389
                    Self::extract_css_urls(text.as_str(), &mut resources);
390
                }
391
521
                XmlNodeChild::Element(node) => {
392
521
                    if depth > MAX_XML_NESTING_DEPTH {
393
                        // Deeper subtrees are simply not scanned.
394
1
                        continue;
395
520
                    }
396
520
                    Self::scan_node(node, &mut resources);
397
520
                    for c in node.children.as_ref() {
398
513
                        stack.push((c, depth + 1));
399
513
                    }
400
                }
401
            }
402
        }
403

            
404
3
        resources.into()
405
3
    }
406

            
407
    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
408
524
    fn scan_node(node: &XmlNode, resources: &mut Vec<ExternalResource>) {
409
524
        let tag_name = node.node_type.inner.as_str().to_lowercase();
410

            
411
        // Get attribute lookup helper
412
1063
        let get_attr = |name: &str| -> Option<String> {
413
1063
            node.attributes
414
1063
                .inner
415
1063
                .as_ref()
416
1063
                .iter()
417
1063
                .find(|pair| pair.key.as_str().eq_ignore_ascii_case(name))
418
1063
                .map(|pair| pair.value.as_str().to_string())
419
1063
        };
420

            
421
524
        match tag_name.as_str() {
422
524
            "img" => {
423
2
                if let Some(src) = get_attr("src") {
424
2
                    let mime = Self::guess_mime_from_url(&src, "image");
425
2
                    resources.push(ExternalResource {
426
2
                        url: AzString::from(src),
427
2
                        kind: ExternalResourceKind::Image,
428
2
                        mime_type: mime.into(),
429
2
                        source_element: AzString::from("img"),
430
2
                        source_attribute: AzString::from("src"),
431
2
                    });
432
2
                }
433
                // Also check srcset
434
2
                if let Some(srcset) = get_attr("srcset") {
435
2
                    for src in Self::parse_srcset(&srcset) {
436
2
                        let mime = Self::guess_mime_from_url(&src, "image");
437
2
                        resources.push(ExternalResource {
438
2
                            url: AzString::from(src),
439
2
                            kind: ExternalResourceKind::Image,
440
2
                            mime_type: mime.into(),
441
2
                            source_element: AzString::from("img"),
442
2
                            source_attribute: AzString::from("srcset"),
443
2
                        });
444
2
                    }
445
1
                }
446
            }
447
522
            "link" => {
448
1
                if let Some(href) = get_attr("href") {
449
1
                    let rel = get_attr("rel").unwrap_or_default().to_lowercase();
450
1
                    let type_attr = get_attr("type");
451
1
                    let as_attr = get_attr("as").unwrap_or_default().to_lowercase();
452

            
453
1
                    let (kind, category) = if rel.contains("stylesheet") {
454
1
                        (ExternalResourceKind::Stylesheet, "stylesheet")
455
                    } else if rel.contains("icon") || rel.contains("apple-touch-icon") {
456
                        (ExternalResourceKind::Icon, "image")
457
                    } else if as_attr == "font" {
458
                        (ExternalResourceKind::Font, "font")
459
                    } else if as_attr == "script" {
460
                        (ExternalResourceKind::Script, "script")
461
                    } else if as_attr == "image" {
462
                        (ExternalResourceKind::Image, "image")
463
                    } else {
464
                        (ExternalResourceKind::Unknown, "")
465
                    };
466

            
467
1
                    let mime = type_attr
468
1
                        .map(|t| MimeTypeHint::new(&t))
469
1
                        .or_else(|| Self::guess_mime_from_url(&href, category));
470

            
471
1
                    resources.push(ExternalResource {
472
1
                        url: AzString::from(href),
473
1
                        kind,
474
1
                        mime_type: mime.into(),
475
1
                        source_element: AzString::from("link"),
476
1
                        source_attribute: AzString::from("href"),
477
1
                    });
478
                }
479
            }
480
521
            "script" => {
481
1
                if let Some(src) = get_attr("src") {
482
1
                    let type_attr = get_attr("type");
483
1
                    let mime = type_attr
484
1
                        .map(|t| MimeTypeHint::new(&t))
485
1
                        .or_else(|| Some(MimeTypeHint::new("application/javascript")));
486

            
487
1
                    resources.push(ExternalResource {
488
1
                        url: AzString::from(src),
489
1
                        kind: ExternalResourceKind::Script,
490
1
                        mime_type: mime.into(),
491
1
                        source_element: AzString::from("script"),
492
1
                        source_attribute: AzString::from("src"),
493
1
                    });
494
                }
495
            }
496
520
            "video" => {
497
1
                if let Some(src) = get_attr("src") {
498
1
                    let mime = Self::guess_mime_from_url(&src, "video");
499
1
                    resources.push(ExternalResource {
500
1
                        url: AzString::from(src),
501
1
                        kind: ExternalResourceKind::Video,
502
1
                        mime_type: mime.into(),
503
1
                        source_element: AzString::from("video"),
504
1
                        source_attribute: AzString::from("src"),
505
1
                    });
506
1
                }
507
1
                if let Some(poster) = get_attr("poster") {
508
1
                    let mime = Self::guess_mime_from_url(&poster, "image");
509
1
                    resources.push(ExternalResource {
510
1
                        url: AzString::from(poster),
511
1
                        kind: ExternalResourceKind::Image,
512
1
                        mime_type: mime.into(),
513
1
                        source_element: AzString::from("video"),
514
1
                        source_attribute: AzString::from("poster"),
515
1
                    });
516
1
                }
517
            }
518
519
            "audio" => {
519
1
                if let Some(src) = get_attr("src") {
520
1
                    let mime = Self::guess_mime_from_url(&src, "audio");
521
1
                    resources.push(ExternalResource {
522
1
                        url: AzString::from(src),
523
1
                        kind: ExternalResourceKind::Audio,
524
1
                        mime_type: mime.into(),
525
1
                        source_element: AzString::from("audio"),
526
1
                        source_attribute: AzString::from("src"),
527
1
                    });
528
1
                }
529
            }
530
518
            "source" => {
531
                if let Some(src) = get_attr("src") {
532
                    let type_attr = get_attr("type");
533
                    // Determine kind based on type or parent (heuristic: assume video)
534
                    let kind = if type_attr
535
                        .as_ref()
536
                        .is_some_and(|t| t.starts_with("audio"))
537
                    {
538
                        ExternalResourceKind::Audio
539
                    } else {
540
                        ExternalResourceKind::Video
541
                    };
542
                    let mime = type_attr.map(|t| MimeTypeHint::new(&t)).or_else(|| {
543
                        Self::guess_mime_from_url(
544
                            &src,
545
                            if kind == ExternalResourceKind::Audio {
546
                                "audio"
547
                            } else {
548
                                "video"
549
                            },
550
                        )
551
                    });
552

            
553
                    resources.push(ExternalResource {
554
                        url: AzString::from(src),
555
                        kind,
556
                        mime_type: mime.into(),
557
                        source_element: AzString::from("source"),
558
                        source_attribute: AzString::from("src"),
559
                    });
560
                }
561
                // Also handle srcset for picture elements
562
                if let Some(srcset) = get_attr("srcset") {
563
                    for src in Self::parse_srcset(&srcset) {
564
                        let mime = Self::guess_mime_from_url(&src, "image");
565
                        resources.push(ExternalResource {
566
                            url: AzString::from(src),
567
                            kind: ExternalResourceKind::Image,
568
                            mime_type: mime.into(),
569
                            source_element: AzString::from("source"),
570
                            source_attribute: AzString::from("srcset"),
571
                        });
572
                    }
573
                }
574
            }
575
518
            "a" => {
576
2
                if let Some(href) = get_attr("href") {
577
                    // Only include if it looks like a resource, not a page link
578
2
                    if Self::looks_like_resource(&href) {
579
1
                        let mime = Self::guess_mime_from_url(&href, "");
580
1
                        resources.push(ExternalResource {
581
1
                            url: AzString::from(href),
582
1
                            kind: ExternalResourceKind::Unknown,
583
1
                            mime_type: mime.into(),
584
1
                            source_element: AzString::from("a"),
585
1
                            source_attribute: AzString::from("href"),
586
1
                        });
587
1
                    }
588
                }
589
            }
590
516
            "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
516
            "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
516
            _ => {}
611
        }
612

            
613
        // Check inline style attribute for url()
614
524
        if let Some(style) = get_attr("style") {
615
1
            Self::extract_css_urls(&style, resources);
616
523
        }
617

            
618
        // Check for background attribute (deprecated but still used)
619
524
        if let Some(bg) = get_attr("background") {
620
1
            let mime = Self::guess_mime_from_url(&bg, "image");
621
1
            resources.push(ExternalResource {
622
1
                url: AzString::from(bg),
623
1
                kind: ExternalResourceKind::Image,
624
1
                mime_type: mime.into(),
625
1
                source_element: AzString::from(tag_name),
626
1
                source_attribute: AzString::from("background"),
627
1
            });
628
523
        }
629

            
630
        // Children are walked by the iterative driver in `scan_external_resources`.
631
524
    }
632

            
633
    /// Extract URLs from CSS content (handles `url()` and @import)
634
15
    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
15
        let lower = css.to_ascii_lowercase();
646

            
647
        // url(...) scan (case-insensitive)
648
15
        let mut search_from = 0;
649
4022
        while let Some(rel) = lower[search_from..].find("url(") {
650
4007
            let url_start = search_from + rel;
651
4007
            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
4007
            if lower[..url_start].trim_end().ends_with("@import") {
656
2
                search_from = after;
657
2
                continue;
658
4005
            }
659
4005
            let after_url = &css[after..];
660
4005
            if let Some(url) = Self::extract_url_value(after_url) {
661
2004
                let mime = Self::guess_mime_from_url(&url, "");
662
2004
                let kind = Self::guess_kind_from_url(&url);
663
2004
                resources.push(ExternalResource {
664
2004
                    url: AzString::from(url),
665
2004
                    kind,
666
2004
                    mime_type: mime.into(),
667
2004
                    source_element: AzString::from("style"),
668
2004
                    source_attribute: AzString::from("url()"),
669
2004
                });
670
2004
            }
671
4005
            search_from = after;
672
        }
673

            
674
        // Handle @import "url" or @import url(...) (case-insensitive)
675
15
        let mut search_from = 0;
676
20
        while let Some(rel) = lower[search_from..].find("@import") {
677
5
            let after = search_from + rel + 7;
678
5
            let after_import = &css[after..];
679
5
            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
5
            let import_url = if trimmed.get(..4).is_some_and(|p| p.eq_ignore_ascii_case("url(")) {
685
2
                Self::extract_url_value(&trimmed[4..])
686
            } else {
687
3
                Self::extract_quoted_string(trimmed)
688
            };
689

            
690
5
            if let Some(url) = import_url {
691
3
                resources.push(ExternalResource {
692
3
                    url: AzString::from(url),
693
3
                    kind: ExternalResourceKind::Stylesheet,
694
3
                    mime_type: Some(MimeTypeHint::new("text/css")).into(),
695
3
                    source_element: AzString::from("style"),
696
3
                    source_attribute: AzString::from("@import"),
697
3
                });
698
3
            }
699

            
700
5
            search_from = after;
701
        }
702
15
    }
703

            
704
    /// Extract value from url(...) - handles quoted and unquoted URLs
705
4033
    fn extract_url_value(s: &str) -> Option<String> {
706
4033
        let trimmed = s.trim_start();
707
4033
        if trimmed.starts_with('"') {
708
4
            Self::extract_quoted_string(trimmed)
709
4029
        } else if let Some(rest) = trimmed.strip_prefix('\'') {
710
3
            let end = rest.find('\'')?;
711
2
            Some(rest[..end].to_string())
712
        } else {
713
4026
            let end = trimmed.find(')')?;
714
2016
            Some(trimmed[..end].trim().to_string())
715
        }
716
4033
    }
717

            
718
    /// Extract a quoted string value
719
25
    fn extract_quoted_string(s: &str) -> Option<String> {
720
25
        if let Some(rest) = s.strip_prefix('"') {
721
15
            let end = rest.find('"')?;
722
11
            Some(rest[..end].to_string())
723
10
        } else if let Some(rest) = s.strip_prefix('\'') {
724
4
            let end = rest.find('\'')?;
725
3
            Some(rest[..end].to_string())
726
        } else {
727
6
            None
728
        }
729
25
    }
730

            
731
    /// Parse srcset attribute into individual URLs
732
12
    fn parse_srcset(srcset: &str) -> Vec<String> {
733
12
        srcset
734
12
            .split(',')
735
20020
            .filter_map(|entry| {
736
20020
                let trimmed = entry.trim();
737
                // srcset format: "url 1x" or "url 100w"
738
20020
                trimmed.split_whitespace().next().map(alloc::string::ToString::to_string)
739
20020
            })
740
20012
            .filter(|url| !url.is_empty())
741
12
            .collect()
742
12
    }
743

            
744
    /// Check if a URL looks like a downloadable resource (not a page)
745
9
    fn looks_like_resource(url: &str) -> bool {
746
9
        let lower = url.to_lowercase();
747
        // Check for common resource extensions
748
9
        let resource_exts = [
749
9
            ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp", ".ttf", ".otf",
750
9
            ".woff", ".woff2", ".eot", ".css", ".js", ".mp4", ".webm", ".ogg", ".mp3", ".wav",
751
9
            ".pdf", ".zip", ".tar", ".gz",
752
9
        ];
753
187
        resource_exts.iter().any(|ext| lower.ends_with(ext))
754
9
    }
755

            
756
    /// Guess the resource kind from URL based on file extension.
757
    // `url` is lowercased into `path` below, so these literal `.ext` checks are
758
    // already case-insensitive — the lint can't see the runtime lowercasing.
759
    #[allow(clippy::case_sensitive_file_extension_comparisons)]
760
2014
    fn guess_kind_from_url(url: &str) -> ExternalResourceKind {
761
2014
        let lower = url.to_lowercase();
762
        // Strip query string before checking extension
763
2014
        let path = lower.split('?').next().unwrap_or(&lower);
764
2014
        if path.ends_with(".png")
765
9
            || path.ends_with(".jpg")
766
9
            || path.ends_with(".jpeg")
767
9
            || path.ends_with(".gif")
768
9
            || path.ends_with(".webp")
769
9
            || path.ends_with(".svg")
770
9
            || path.ends_with(".bmp")
771
9
            || path.ends_with(".avif")
772
        {
773
2005
            ExternalResourceKind::Image
774
9
        } else if path.ends_with(".ttf")
775
9
            || path.ends_with(".otf")
776
9
            || path.ends_with(".woff")
777
9
            || path.ends_with(".woff2")
778
8
            || path.ends_with(".eot")
779
        {
780
1
            ExternalResourceKind::Font
781
8
        } else if path.ends_with(".css") {
782
2
            ExternalResourceKind::Stylesheet
783
6
        } else if path.ends_with(".js") || path.ends_with(".mjs") {
784
1
            ExternalResourceKind::Script
785
5
        } else if path.ends_with(".mp4") || path.ends_with(".webm") || path.ends_with(".ogg") {
786
1
            ExternalResourceKind::Video
787
4
        } else if path.ends_with(".mp3") || path.ends_with(".wav") || path.ends_with(".flac") {
788
1
            ExternalResourceKind::Audio
789
3
        } else if path.ends_with(".ico") {
790
1
            ExternalResourceKind::Icon
791
        } else {
792
2
            ExternalResourceKind::Unknown
793
        }
794
2014
    }
795

            
796
    /// Guess MIME type from URL based on extension
797
2027
    fn guess_mime_from_url(url: &str, category: &str) -> Option<MimeTypeHint> {
798
2027
        let lower = url.to_lowercase();
799
        // Find extension
800
2027
        let ext = lower.rsplit('.').next()?;
801
        // Remove query string if present
802
2027
        let ext = ext.split('?').next()?;
803

            
804
        // Check if it's a valid extension
805
2027
        let valid_exts = [
806
2027
            "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "avif", "ttf", "otf", "woff",
807
2027
            "woff2", "eot", "css", "js", "mjs", "mp4", "webm", "ogg", "mp3", "wav", "flac",
808
2027
        ];
809

            
810
2027
        if valid_exts.contains(&ext) {
811
2016
            Some(MimeTypeHint::from_extension(ext))
812
11
        } else if !category.is_empty() {
813
            // Use category hint for default
814
2
            match category {
815
2
                "image" => Some(MimeTypeHint::new("image/*")),
816
1
                "font" => Some(MimeTypeHint::new("font/*")),
817
1
                "stylesheet" => Some(MimeTypeHint::new("text/css")),
818
1
                "script" => Some(MimeTypeHint::new("application/javascript")),
819
1
                "video" => Some(MimeTypeHint::new("video/*")),
820
1
                "audio" => Some(MimeTypeHint::new("audio/*")),
821
1
                _ => None,
822
            }
823
        } else {
824
9
            None
825
        }
826
2027
    }
827
}
828

            
829
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
830
#[repr(C)]
831
pub struct NonXmlCharError {
832
    pub ch: u32, /* u32 = char, but ABI stable */
833
    pub pos: XmlTextPos,
834
}
835

            
836
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
837
#[repr(C)]
838
pub struct InvalidCharError {
839
    pub expected: u8,
840
    pub got: u8,
841
    pub pos: XmlTextPos,
842
}
843

            
844
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
845
#[repr(C)]
846
pub struct InvalidCharMultipleError {
847
    pub expected: u8,
848
    pub got: U8Vec,
849
    pub pos: XmlTextPos,
850
}
851

            
852
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
853
#[repr(C)]
854
pub struct InvalidQuoteError {
855
    pub got: u8,
856
    pub pos: XmlTextPos,
857
}
858

            
859
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
860
#[repr(C)]
861
pub struct InvalidSpaceError {
862
    pub got: u8,
863
    pub pos: XmlTextPos,
864
}
865

            
866
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
867
#[repr(C)]
868
pub struct InvalidStringError {
869
    pub got: AzString,
870
    pub pos: XmlTextPos,
871
}
872

            
873
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
874
#[repr(C, u8)]
875
pub enum XmlStreamError {
876
    UnexpectedEndOfStream,
877
    InvalidName,
878
    NonXmlChar(NonXmlCharError),
879
    InvalidChar(InvalidCharError),
880
    InvalidCharMultiple(InvalidCharMultipleError),
881
    InvalidQuote(InvalidQuoteError),
882
    InvalidSpace(InvalidSpaceError),
883
    InvalidString(InvalidStringError),
884
    InvalidReference,
885
    InvalidExternalID,
886
    InvalidCommentData,
887
    InvalidCommentEnd,
888
    InvalidCharacterData,
889
}
890

            
891
impl fmt::Display for XmlStreamError {
892
386
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
893
        use self::XmlStreamError::{UnexpectedEndOfStream, InvalidName, NonXmlChar, InvalidChar, InvalidCharMultiple, InvalidQuote, InvalidSpace, InvalidString, InvalidReference, InvalidExternalID, InvalidCommentData, InvalidCommentEnd, InvalidCharacterData};
894
386
        match self {
895
12
            UnexpectedEndOfStream => write!(f, "Unexpected end of stream"),
896
285
            InvalidName => write!(f, "Invalid name"),
897
35
            NonXmlChar(nx) => write!(
898
35
                f,
899
35
                "Non-XML character: {:?} at {}",
900
35
                core::char::from_u32(nx.ch),
901
                nx.pos
902
            ),
903
1
            InvalidChar(ic) => write!(
904
1
                f,
905
1
                "Invalid character: expected: {}, got: {} at {}",
906
1
                ic.expected as char, ic.got as char, ic.pos
907
            ),
908
1
            InvalidCharMultiple(imc) => write!(
909
1
                f,
910
1
                "Multiple invalid characters: expected: {}, got: {:?} at {}",
911
                imc.expected,
912
1
                imc.got.as_ref(),
913
                imc.pos
914
            ),
915
12
            InvalidQuote(iq) => write!(f, "Invalid quote: got {} at {}", iq.got as char, iq.pos),
916
1
            InvalidSpace(is) => write!(f, "Invalid space: got {} at {}", is.got as char, is.pos),
917
23
            InvalidString(ise) => write!(
918
23
                f,
919
23
                "Invalid string: got \"{}\" at {}",
920
23
                ise.got.as_str(),
921
                ise.pos
922
            ),
923
1
            InvalidReference => write!(f, "Invalid reference"),
924
1
            InvalidExternalID => write!(f, "Invalid external ID"),
925
1
            InvalidCommentData => write!(f, "Invalid comment data"),
926
1
            InvalidCommentEnd => write!(f, "Invalid comment end"),
927
12
            InvalidCharacterData => write!(f, "Invalid character data"),
928
        }
929
386
    }
930
}
931

            
932
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Ord, Hash, Eq)]
933
#[repr(C)]
934
pub struct XmlTextPos {
935
    pub row: u32,
936
    pub col: u32,
937
}
938

            
939
impl fmt::Display for XmlTextPos {
940
474
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
941
474
        write!(f, "line {}:{}", self.row, self.col)
942
474
    }
943
}
944

            
945
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
946
#[repr(C)]
947
pub struct XmlTextError {
948
    pub stream_error: XmlStreamError,
949
    pub pos: XmlTextPos,
950
}
951

            
952
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
953
#[repr(C, u8)]
954
pub enum XmlParseError {
955
    InvalidDeclaration(XmlTextError),
956
    InvalidComment(XmlTextError),
957
    InvalidPI(XmlTextError),
958
    InvalidDoctype(XmlTextError),
959
    InvalidEntity(XmlTextError),
960
    InvalidElement(XmlTextError),
961
    InvalidAttribute(XmlTextError),
962
    InvalidCdata(XmlTextError),
963
    InvalidCharData(XmlTextError),
964
    UnknownToken(XmlTextPos),
965
}
966

            
967
impl fmt::Display for XmlParseError {
968
385
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
969
        use self::XmlParseError::{InvalidDeclaration, InvalidComment, InvalidPI, InvalidDoctype, InvalidEntity, InvalidElement, InvalidAttribute, InvalidCdata, InvalidCharData, UnknownToken};
970
385
        match self {
971
1
            InvalidDeclaration(e) => {
972
1
                write!(f, "Invalid declaration: {} at {}", e.stream_error, e.pos)
973
            }
974
12
            InvalidComment(e) => write!(f, "Invalid comment: {} at {}", e.stream_error, e.pos),
975
1
            InvalidPI(e) => write!(
976
1
                f,
977
1
                "Invalid processing instruction: {} at {}",
978
                e.stream_error, e.pos
979
            ),
980
1
            InvalidDoctype(e) => write!(f, "Invalid doctype: {} at {}", e.stream_error, e.pos),
981
1
            InvalidEntity(e) => write!(f, "Invalid entity: {} at {}", e.stream_error, e.pos),
982
287
            InvalidElement(e) => write!(f, "Invalid element: {} at {}", e.stream_error, e.pos),
983
12
            InvalidAttribute(e) => write!(f, "Invalid attribute: {} at {}", e.stream_error, e.pos),
984
12
            InvalidCdata(e) => write!(f, "Invalid CDATA: {} at {}", e.stream_error, e.pos),
985
45
            InvalidCharData(e) => write!(f, "Invalid char data: {} at {}", e.stream_error, e.pos),
986
13
            UnknownToken(e) => write!(f, "Unknown token at {e}"),
987
        }
988
385
    }
989
}
990

            
991
impl_result!(
992
    Xml,
993
    XmlError,
994
    ResultXmlXmlError,
995
    copy = false,
996
    [Debug, PartialEq, Eq, PartialOrd, Clone]
997
);
998

            
999
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct DuplicatedNamespaceError {
    pub ns: AzString,
    pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnknownNamespaceError {
    pub ns: AzString,
    pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnexpectedCloseTagError {
    pub expected: AzString,
    pub actual: AzString,
    pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnknownEntityReferenceError {
    pub entity: AzString,
    pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct DuplicatedAttributeError {
    pub attribute: AzString,
    pub pos: XmlTextPos,
}
/// Error for mismatched open/close tags in XML hierarchy
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct MalformedHierarchyError {
    /// The tag that was expected (from the opening tag)
    pub expected: AzString,
    /// The tag that was actually found (the closing tag)
    pub got: AzString,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C, u8)]
pub enum XmlError {
    NoParserAvailable,
    InvalidXmlPrefixUri(XmlTextPos),
    UnexpectedXmlUri(XmlTextPos),
    UnexpectedXmlnsUri(XmlTextPos),
    InvalidElementNamePrefix(XmlTextPos),
    DuplicatedNamespace(DuplicatedNamespaceError),
    UnknownNamespace(UnknownNamespaceError),
    UnexpectedCloseTag(UnexpectedCloseTagError),
    UnexpectedEntityCloseTag(XmlTextPos),
    UnknownEntityReference(UnknownEntityReferenceError),
    MalformedEntityReference(XmlTextPos),
    EntityReferenceLoop(XmlTextPos),
    InvalidAttributeValue(XmlTextPos),
    DuplicatedAttribute(DuplicatedAttributeError),
    NoRootNode,
    SizeLimit,
    DtdDetected,
    /// Invalid hierarchy close tags, i.e `<app></p></app>`
    MalformedHierarchy(MalformedHierarchyError),
    ParserError(XmlParseError),
    UnclosedRootNode,
    UnexpectedDeclaration(XmlTextPos),
    NodesLimitReached,
    AttributesLimitReached,
    NamespacesLimitReached,
    InvalidName(XmlTextPos),
    NonXmlChar(XmlTextPos),
    InvalidChar(XmlTextPos),
    InvalidChar2(XmlTextPos),
    InvalidString(XmlTextPos),
    InvalidExternalID(XmlTextPos),
    InvalidComment(XmlTextPos),
    InvalidCharacterData(XmlTextPos),
    UnknownToken(XmlTextPos),
    UnexpectedEndOfStream,
}
impl fmt::Display for XmlError {
489
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::XmlError::{NoParserAvailable, InvalidXmlPrefixUri, UnexpectedXmlUri, UnexpectedXmlnsUri, InvalidElementNamePrefix, DuplicatedNamespace, UnknownNamespace, UnexpectedCloseTag, UnexpectedEntityCloseTag, UnknownEntityReference, MalformedEntityReference, EntityReferenceLoop, InvalidAttributeValue, DuplicatedAttribute, NoRootNode, SizeLimit, DtdDetected, MalformedHierarchy, ParserError, UnclosedRootNode, UnexpectedDeclaration, NodesLimitReached, AttributesLimitReached, NamespacesLimitReached, InvalidName, NonXmlChar, InvalidChar, InvalidChar2, InvalidString, InvalidExternalID, InvalidComment, InvalidCharacterData, UnknownToken, UnexpectedEndOfStream};
489
        match self {
1
            NoParserAvailable => write!(
1
                f,
1
                "Library was compiled without XML parser (XML parser not available)"
            ),
1
            InvalidXmlPrefixUri(pos) => {
1
                write!(f, "Invalid XML Prefix URI at line {}:{}", pos.row, pos.col)
            }
1
            UnexpectedXmlUri(pos) => {
1
                write!(f, "Unexpected XML URI at line {}:{}", pos.row, pos.col)
            }
1
            UnexpectedXmlnsUri(pos) => write!(
1
                f,
1
                "Unexpected XML namespace URI at line {}:{}",
                pos.row, pos.col
            ),
1
            InvalidElementNamePrefix(pos) => write!(
1
                f,
1
                "Invalid element name prefix at line {}:{}",
                pos.row, pos.col
            ),
1
            DuplicatedNamespace(ns) => write!(
1
                f,
1
                "Duplicated namespace: \"{}\" at {}",
1
                ns.ns.as_str(),
                ns.pos
            ),
1
            UnknownNamespace(uns) => write!(
1
                f,
1
                "Unknown namespace: \"{}\" at {}",
1
                uns.ns.as_str(),
                uns.pos
            ),
1
            UnexpectedCloseTag(ct) => write!(
1
                f,
1
                "Unexpected close tag: expected \"{}\", got \"{}\" at {}",
1
                ct.expected.as_str(),
1
                ct.actual.as_str(),
                ct.pos
            ),
1
            UnexpectedEntityCloseTag(pos) => write!(
1
                f,
1
                "Unexpected entity close tag at line {}:{}",
                pos.row, pos.col
            ),
1
            UnknownEntityReference(uer) => write!(
1
                f,
1
                "Unexpected entity reference: \"{}\" at {}",
                uer.entity, uer.pos
            ),
1
            MalformedEntityReference(pos) => write!(
1
                f,
1
                "Malformed entity reference at line {}:{}",
                pos.row, pos.col
            ),
1
            EntityReferenceLoop(pos) => write!(
1
                f,
1
                "Entity reference loop (recursive entity reference) at line {}:{}",
                pos.row, pos.col
            ),
1
            InvalidAttributeValue(pos) => {
1
                write!(f, "Invalid attribute value at line {}:{}", pos.row, pos.col)
            }
1
            DuplicatedAttribute(ae) => write!(
1
                f,
1
                "Duplicated attribute \"{}\" at line {}:{}",
1
                ae.attribute.as_str(),
                ae.pos.row,
                ae.pos.col
            ),
5
            NoRootNode => write!(f, "No root node found"),
1
            SizeLimit => write!(f, "XML file too large (size limit reached)"),
1
            DtdDetected => write!(f, "Document type descriptor detected"),
23
            MalformedHierarchy(e) => write!(
23
                f,
23
                "Malformed hierarchy: expected <{}/> closing tag, got <{}/>",
23
                e.expected.as_str(),
23
                e.got.as_str()
            ),
375
            ParserError(p) => write!(f, "{p}"),
56
            UnclosedRootNode => write!(f, "unclosed root node"),
1
            UnexpectedDeclaration(tp) => write!(f, "unexpected declaration at {tp}"),
1
            NodesLimitReached => write!(f, "nodes limit reached"),
1
            AttributesLimitReached => write!(f, "attributes limit reached"),
1
            NamespacesLimitReached => write!(f, "namespaces limit reached"),
1
            InvalidName(tp) => write!(f, "invalid name at {tp}"),
1
            NonXmlChar(tp) => write!(f, "non xml char at {tp}"),
1
            InvalidChar(tp) => write!(f, "invalid char at {tp}"),
1
            InvalidChar2(tp) => write!(f, "invalid char2 at {tp}"),
1
            InvalidString(tp) => write!(f, "invalid string at {tp}"),
1
            InvalidExternalID(tp) => write!(f, "invalid externalid at {tp}"),
1
            InvalidComment(tp) => write!(f, "invalid comment at {tp}"),
1
            InvalidCharacterData(tp) => write!(f, "invalid character data at {tp}"),
1
            UnknownToken(tp) => write!(f, "unknown token at {tp}"),
1
            UnexpectedEndOfStream => write!(f, "unexpected end of stream"),
        }
489
    }
}
// ============================================================================
// New repr(C) component system
// ============================================================================
/// Identifies a component within a library collection.
/// e.g. collection="builtin", name="div" for the `<div>` element,
/// or collection="shadcn", name="avatar" for a custom component.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ComponentId {
    /// Library / collection name: "builtin", "shadcn", "myproject"
    pub collection: AzString,
    /// Component name within the collection: "div", "avatar", "card"
    pub name: AzString,
}
impl ComponentId {
321801
    #[must_use] pub fn builtin(name: &str) -> Self {
321801
        Self {
321801
            collection: AzString::from_const_str("builtin"),
321801
            name: AzString::from(name),
321801
        }
321801
    }
12
    #[must_use] pub fn new(collection: &str, name: &str) -> Self {
12
        Self {
12
            collection: AzString::from(collection),
12
            name: AzString::from(name),
12
        }
12
    }
    /// Returns "collection:name" format string
5
    #[must_use] pub fn qualified_name(&self) -> String {
5
        format!("{}:{}", self.collection.as_str(), self.name.as_str())
5
    }
}
// ============================================================================
// Component type system — rich type descriptors for component fields
// ============================================================================
/// A single argument in a callback signature.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ComponentCallbackArg {
    /// Argument name, e.g. "`button_id`"
    pub name: AzString,
    /// Argument type
    pub arg_type: ComponentFieldType,
}
impl_vec!(
    ComponentCallbackArg,
    ComponentCallbackArgVec,
    ComponentCallbackArgVecDestructor,
    ComponentCallbackArgVecDestructorType,
    ComponentCallbackArgVecSlice,
    OptionComponentCallbackArg
);
impl_option!(
    ComponentCallbackArg,
    OptionComponentCallbackArg,
    copy = false,
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl_vec_debug!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_partialeq!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_eq!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_partialord!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_ord!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_hash!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_clone!(
    ComponentCallbackArg,
    ComponentCallbackArgVec,
    ComponentCallbackArgVecDestructor
);
/// Callback signature: return type + argument list.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ComponentCallbackSignature {
    /// Return type name, e.g. "Update"
    pub return_type: AzString,
    /// Callback arguments (excluding the implicit `&mut RefAny` and `&mut CallbackInfo`)
    pub args: ComponentCallbackArgVec,
}
/// Heap-allocated box for recursive `ComponentFieldType` (e.g. `Option<String>`).
/// Uses raw pointer indirection to break the infinite size.
#[repr(C)]
pub struct ComponentFieldTypeBox {
    pub ptr: *mut ComponentFieldType,
}
impl ComponentFieldTypeBox {
151
    #[must_use] pub fn new(t: ComponentFieldType) -> Self {
151
        Self {
151
            ptr: Box::into_raw(Box::new(t)),
151
        }
151
    }
74
    #[must_use] pub fn as_ref(&self) -> &ComponentFieldType {
74
        unsafe { &*self.ptr }
74
    }
}
impl Clone for ComponentFieldTypeBox {
5
    fn clone(&self) -> Self {
5
        Self::new(unsafe { (*self.ptr).clone() })
5
    }
}
impl Drop for ComponentFieldTypeBox {
151
    fn drop(&mut self) {
        // Null the pointer as we free it, so a *second* drop is a no-op instead
        // of a double free. This type is a by-value payload of the
        // `ComponentFieldType` enum, whose codegen FFI mirror gets
        // `impl Drop { _delete }` (= drop_in_place of the real type) AND Rust
        // field drop-glue — dropping each by-value field twice. Without this
        // take-and-null the second drop would `Box::from_raw` a dangling pointer.
151
        let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
151
        if !ptr.is_null() {
151
            unsafe {
151
                drop(Box::from_raw(ptr));
151
            }
        }
151
    }
}
impl fmt::Debug for ComponentFieldTypeBox {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.ptr.is_null() {
            write!(f, "ComponentFieldTypeBox(null)")
        } else {
            write!(f, "ComponentFieldTypeBox({:?})", unsafe { &*self.ptr })
        }
    }
}
impl PartialEq for ComponentFieldTypeBox {
5
    fn eq(&self, other: &Self) -> bool {
5
        if self.ptr.is_null() && other.ptr.is_null() {
            return true;
5
        }
5
        if self.ptr.is_null() || other.ptr.is_null() {
            return false;
5
        }
5
        unsafe { *self.ptr == *other.ptr }
5
    }
}
impl Eq for ComponentFieldTypeBox {}
impl PartialOrd for ComponentFieldTypeBox {
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
        Some(self.cmp(other))
    }
}
impl Ord for ComponentFieldTypeBox {
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
        match (self.ptr.is_null(), other.ptr.is_null()) {
            (true, true) => core::cmp::Ordering::Equal,
            (true, false) => core::cmp::Ordering::Less,
            (false, true) => core::cmp::Ordering::Greater,
            (false, false) => unsafe { (*self.ptr).cmp(&*other.ptr) },
        }
    }
}
impl Hash for ComponentFieldTypeBox {
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
        if !self.ptr.is_null() {
            unsafe {
                (*self.ptr).hash(state);
            }
        }
    }
}
/// Heap-allocated box for recursive `ComponentFieldValue` (e.g. `Some(value)`).
/// Uses raw pointer indirection to break the infinite size.
#[repr(C)]
pub struct ComponentFieldValueBox {
    pub ptr: *mut ComponentFieldValue,
}
impl ComponentFieldValueBox {
2
    #[must_use] pub fn new(v: ComponentFieldValue) -> Self {
2
        Self {
2
            ptr: Box::into_raw(Box::new(v)),
2
        }
2
    }
1
    #[must_use] pub fn as_ref(&self) -> &ComponentFieldValue {
1
        unsafe { &*self.ptr }
1
    }
}
impl Clone for ComponentFieldValueBox {
1
    fn clone(&self) -> Self {
1
        Self::new(unsafe { (*self.ptr).clone() })
1
    }
}
impl Drop for ComponentFieldValueBox {
2
    fn drop(&mut self) {
        // Take-and-null so a second drop (codegen FFI double-drop of a by-value
        // field, see `ComponentFieldTypeBox`) is a no-op, not a double free.
2
        let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
2
        if !ptr.is_null() {
2
            unsafe {
2
                drop(Box::from_raw(ptr));
2
            }
        }
2
    }
}
impl fmt::Debug for ComponentFieldValueBox {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.ptr.is_null() {
            write!(f, "ComponentFieldValueBox(null)")
        } else {
            write!(f, "ComponentFieldValueBox({:?})", unsafe { &*self.ptr })
        }
    }
}
impl PartialEq for ComponentFieldValueBox {
1
    fn eq(&self, other: &Self) -> bool {
1
        if self.ptr.is_null() && other.ptr.is_null() {
            return true;
1
        }
1
        if self.ptr.is_null() || other.ptr.is_null() {
            return false;
1
        }
1
        unsafe { *self.ptr == *other.ptr }
1
    }
}
/// Rich type descriptor for a component field.
/// Replaces the old `AzString` type names ("String", "bool", etc.) with
/// a structured enum that the debugger can use for type-aware editing.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum ComponentFieldType {
    String,
    Bool,
    I32,
    I64,
    U32,
    U64,
    Usize,
    F32,
    F64,
    ColorU,
    CssProperty,
    ImageRef,
    FontRef,
    /// `StyledDom` slot — field name = slot name
    StyledDom,
    /// Callback with typed signature
    Callback(ComponentCallbackSignature),
    /// `RefAny` data binding with type hint
    RefAny(AzString),
    /// Optional value (recursive via Box)
    OptionType(ComponentFieldTypeBox),
    /// Vec of values (recursive via Box)
    VecType(ComponentFieldTypeBox),
    /// Reference to a struct defined in the same library
    StructRef(AzString),
    /// Reference to an enum defined in the same library
    EnumRef(AzString),
}
impl ComponentFieldType {
    /// Parse a field type string like "String", "Option<Bool>", "Vec<I32>",
    /// "Callback(fn(LayoutCallbackInfo) -> Dom)", "StructRef(MyStruct)" etc.
    /// Returns `None` if the string cannot be parsed.
56
    #[must_use] pub fn parse(s: &str) -> Option<Self> {
56
        Self::parse_depth(s, 0)
56
    }
    /// Depth-bounded implementation of [`parse`](Self::parse).
    ///
    /// AUDIT 2026-07-08: `Option<..>` / `Vec<..>` wrappers recurse once per level,
    /// so an attacker string like `"Option<".repeat(100_000)` (with matching `>`)
    /// overflowed the stack. Recursion is capped at [`MAX_TYPE_PARSE_DEPTH`];
    /// beyond it, parsing fails (`None`) instead of crashing.
333
    fn parse_depth(s: &str, depth: usize) -> Option<Self> {
333
        if depth > MAX_TYPE_PARSE_DEPTH {
5
            return None;
328
        }
328
        let s = s.trim();
328
        match s {
328
            "String" | "string" => return Some(Self::String),
325
            "Bool" | "bool" => return Some(Self::Bool),
319
            "I32" | "i32" => return Some(Self::I32),
317
            "I64" | "i64" => return Some(Self::I64),
316
            "U32" | "u32" => return Some(Self::U32),
315
            "U64" | "u64" => return Some(Self::U64),
314
            "Usize" | "usize" => return Some(Self::Usize),
312
            "F32" | "f32" => return Some(Self::F32),
311
            "F64" | "f64" => return Some(Self::F64),
310
            "ColorU" => return Some(Self::ColorU),
309
            "CssProperty" => return Some(Self::CssProperty),
308
            "ImageRef" => return Some(Self::ImageRef),
307
            "FontRef" => return Some(Self::FontRef),
306
            "StyledDom" => return Some(Self::StyledDom),
305
            "RefAny" => return Some(Self::RefAny(AzString::from(""))),
303
            _ => {}
        }
        // Option<T>
303
        if let Some(inner) = s.strip_prefix("Option<").and_then(|r| r.strip_suffix('>')) {
206
            let inner_type = Self::parse_depth(inner, depth + 1)?;
74
            return Some(Self::OptionType(ComponentFieldTypeBox::new(
74
                inner_type,
74
            )));
97
        }
        // Vec<T>
97
        if let Some(inner) = s.strip_prefix("Vec<").and_then(|r| r.strip_suffix('>')) {
68
            let inner_type = Self::parse_depth(inner, depth + 1)?;
2
            return Some(Self::VecType(ComponentFieldTypeBox::new(
2
                inner_type,
2
            )));
29
        }
        // Callback(signature)
29
        if let Some(sig) = s
29
            .strip_prefix("Callback(")
29
            .and_then(|r| r.strip_suffix(')'))
        {
2
            return Some(Self::Callback(ComponentCallbackSignature {
2
                return_type: AzString::from(sig),
2
                args: Vec::new().into(),
2
            }));
27
        }
        // RefAny(TypeHint)
27
        if let Some(hint) = s.strip_prefix("RefAny(").and_then(|r| r.strip_suffix(')')) {
1
            return Some(Self::RefAny(AzString::from(hint)));
26
        }
        // EnumRef(Name) — explicit
26
        if let Some(name) = s.strip_prefix("EnumRef(").and_then(|r| r.strip_suffix(')')) {
1
            return Some(Self::EnumRef(AzString::from(name)));
25
        }
        // StructRef(Name) — explicit
25
        if let Some(name) = s
25
            .strip_prefix("StructRef(")
25
            .and_then(|r| r.strip_suffix(')'))
        {
1
            return Some(Self::StructRef(AzString::from(name)));
24
        }
        // If starts with uppercase, treat as StructRef
24
        if s.chars().next().is_some_and(char::is_uppercase) {
8
            return Some(Self::StructRef(AzString::from(s)));
16
        }
16
        None
333
    }
    /// Format this field type to its canonical string representation.
    /// This is the inverse of `parse`.
100
    #[must_use] pub fn format(&self) -> String {
100
        match self {
1
            Self::String => "String".to_string(),
3
            Self::Bool => "Bool".to_string(),
2
            Self::I32 => "I32".to_string(),
1
            Self::I64 => "I64".to_string(),
1
            Self::U32 => "U32".to_string(),
1
            Self::U64 => "U64".to_string(),
1
            Self::Usize => "Usize".to_string(),
1
            Self::F32 => "F32".to_string(),
4
            Self::F64 => "F64".to_string(),
1
            Self::ColorU => "ColorU".to_string(),
1
            Self::CssProperty => "CssProperty".to_string(),
1
            Self::ImageRef => "ImageRef".to_string(),
1
            Self::FontRef => "FontRef".to_string(),
1
            Self::StyledDom => "StyledDom".to_string(),
2
            Self::Callback(sig) => format!("Callback({})", sig.return_type.as_str()),
2
            Self::RefAny(hint) => {
2
                if hint.as_str().is_empty() {
1
                    "RefAny".to_string()
                } else {
1
                    format!("RefAny({})", hint.as_str())
                }
            }
69
            Self::OptionType(inner) => format!("Option<{}>", inner.as_ref().format()),
2
            Self::VecType(inner) => format!("Vec<{}>", inner.as_ref().format()),
5
            Self::StructRef(name) | Self::EnumRef(name) => name.as_str().to_string(),
        }
100
    }
}
impl fmt::Display for ComponentFieldType {
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2
        f.write_str(&self.format())
2
    }
}
/// A single variant in a component enum model.
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentEnumVariant {
    /// Variant name, e.g. "Admin", "Editor", "Viewer"
    pub name: AzString,
    /// Human-readable description for this variant
    pub description: AzString,
    /// Optional associated fields for this variant
    pub fields: ComponentDataFieldVec,
}
impl_vec!(
    ComponentEnumVariant,
    ComponentEnumVariantVec,
    ComponentEnumVariantVecDestructor,
    ComponentEnumVariantVecDestructorType,
    ComponentEnumVariantVecSlice,
    OptionComponentEnumVariant
);
impl_option!(
    ComponentEnumVariant,
    OptionComponentEnumVariant,
    copy = false,
    [Debug, Clone, PartialEq]
);
impl_vec_debug!(ComponentEnumVariant, ComponentEnumVariantVec);
impl_vec_partialeq!(ComponentEnumVariant, ComponentEnumVariantVec);
impl_vec_clone!(
    ComponentEnumVariant,
    ComponentEnumVariantVec,
    ComponentEnumVariantVecDestructor
);
/// A named enum model for code generation.
/// Stored in `ComponentLibrary::enum_models`.
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentEnumModel {
    /// Enum name, e.g. "`UserRole`"
    pub name: AzString,
    /// Human-readable description
    pub description: AzString,
    /// Variants
    pub variants: ComponentEnumVariantVec,
}
impl_vec!(
    ComponentEnumModel,
    ComponentEnumModelVec,
    ComponentEnumModelVecDestructor,
    ComponentEnumModelVecDestructorType,
    ComponentEnumModelVecSlice,
    OptionComponentEnumModel
);
impl_option!(
    ComponentEnumModel,
    OptionComponentEnumModel,
    copy = false,
    [Debug, Clone, PartialEq]
);
impl_vec_debug!(ComponentEnumModel, ComponentEnumModelVec);
impl_vec_partialeq!(ComponentEnumModel, ComponentEnumModelVec);
impl_vec_clone!(
    ComponentEnumModel,
    ComponentEnumModelVec,
    ComponentEnumModelVecDestructor
);
/// Default value for a component field.
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum ComponentDefaultValue {
    /// No default value (field is required)
    None,
    /// String literal default
    String(AzString),
    /// Boolean default
    Bool(bool),
    /// i32 default
    I32(i32),
    /// i64 default
    I64(i64),
    /// u32 default
    U32(u32),
    /// u64 default
    U64(u64),
    /// usize default
    Usize(usize),
    /// f32 default
    F32(f32),
    /// f64 default
    F64(f64),
    /// `ColorU` default
    ColorU(ColorU),
    /// Default is an instance of another component
    ComponentInstance(ComponentInstanceDefault),
    /// Default callback function pointer name
    CallbackFnPointer(AzString),
    /// JSON string representing a complex default value
    Json(AzString),
}
impl_option!(
    ComponentDefaultValue,
    OptionComponentDefaultValue,
    copy = false,
    [Debug, Clone, PartialEq]
);
/// Default component instance for a `StyledDom` slot.
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentInstanceDefault {
    /// Library name, e.g. "builtin"
    pub library: AzString,
    /// Component tag, e.g. "a"
    pub component: AzString,
    /// Field overrides for this instance
    pub field_overrides: ComponentFieldOverrideVec,
}
/// An override for a single field in a component instance.
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct ComponentFieldOverride {
    /// Field name to override
    pub field_name: AzString,
    /// Value source for this override
    pub source: ComponentFieldValueSource,
}
impl_vec!(
    ComponentFieldOverride,
    ComponentFieldOverrideVec,
    ComponentFieldOverrideVecDestructor,
    ComponentFieldOverrideVecDestructorType,
    ComponentFieldOverrideVecSlice,
    OptionComponentFieldOverride
);
impl_option!(
    ComponentFieldOverride,
    OptionComponentFieldOverride,
    copy = false,
    [Debug, Clone, PartialEq, Eq]
);
impl_vec_debug!(ComponentFieldOverride, ComponentFieldOverrideVec);
impl_vec_partialeq!(ComponentFieldOverride, ComponentFieldOverrideVec);
impl_vec_clone!(
    ComponentFieldOverride,
    ComponentFieldOverrideVec,
    ComponentFieldOverrideVecDestructor
);
/// How a field value is sourced at the instance level.
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum ComponentFieldValueSource {
    /// Use the component's default value
    Default,
    /// Hardcoded literal value (as string, parsed at runtime)
    Literal(AzString),
    /// Bound to an app state path (e.g. "`app_state.user.name`")
    Binding(AzString),
}
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
/// Runtime value for a component field — the "instance" counterpart
/// to `ComponentFieldType` (which is the "class" / type descriptor).
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
#[allow(clippy::large_enum_variant)] // #[repr(C,u8)] FFI enum: boxing a variant changes the C ABI/api.json
pub enum ComponentFieldValue {
    String(AzString),
    Bool(bool),
    I32(i32),
    I64(i64),
    U32(u32),
    U64(u64),
    Usize(usize),
    F32(f32),
    F64(f64),
    ColorU(ColorU),
    /// Option<T> with no value
    None,
    /// Option<T> with a value
    Some(ComponentFieldValueBox),
    /// Vec of values
    Vec(ComponentFieldValueVec),
    /// `StyledDom` slot content
    StyledDom(StyledDom),
    /// Struct fields, in order
    Struct(ComponentFieldNamedValueVec),
    /// Enum variant
    Enum {
        variant: AzString,
        fields: ComponentFieldNamedValueVec,
    },
    /// Callback function reference (function name as string)
    Callback(AzString),
    /// Opaque reference-counted data
    RefAny(crate::refany::RefAny),
}
/// Named field value: (`field_name`, value) pair.
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentFieldNamedValue {
    pub name: AzString,
    pub value: ComponentFieldValue,
}
impl_vec!(
    ComponentFieldNamedValue,
    ComponentFieldNamedValueVec,
    ComponentFieldNamedValueVecDestructor,
    ComponentFieldNamedValueVecDestructorType,
    ComponentFieldNamedValueVecSlice,
    OptionComponentFieldNamedValue
);
impl_option!(
    ComponentFieldNamedValue,
    OptionComponentFieldNamedValue,
    copy = false,
    [Debug, Clone, PartialEq]
);
impl_vec_debug!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
impl_vec_partialeq!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
impl_vec_clone!(
    ComponentFieldNamedValue,
    ComponentFieldNamedValueVec,
    ComponentFieldNamedValueVecDestructor
);
impl ComponentFieldNamedValueVec {
    /// Look up a field by name, return a reference to its value.
20
    #[must_use] pub fn get_field(&self, name: &str) -> Option<&ComponentFieldValue> {
54
        self.as_ref().iter().find_map(|v| {
54
            if v.name.as_str() == name {
11
                Some(&v.value)
            } else {
43
                None
            }
54
        })
20
    }
    /// Convenience: get a field as `&str` if it is `ComponentFieldValue::String`.
5
    #[must_use] pub fn get_string(&self, name: &str) -> Option<&AzString> {
5
        match self.get_field(name) {
1
            Some(ComponentFieldValue::String(s)) => Some(s),
4
            _ => None,
        }
5
    }
}
impl_vec!(
    ComponentFieldValue,
    ComponentFieldValueVec,
    ComponentFieldValueVecDestructor,
    ComponentFieldValueVecDestructorType,
    ComponentFieldValueVecSlice,
    OptionComponentFieldValue
);
impl_option!(
    ComponentFieldValue,
    OptionComponentFieldValue,
    copy = false,
    [Debug, Clone, PartialEq]
);
impl_vec_debug!(ComponentFieldValue, ComponentFieldValueVec);
impl_vec_partialeq!(ComponentFieldValue, ComponentFieldValueVec);
impl_vec_clone!(
    ComponentFieldValue,
    ComponentFieldValueVec,
    ComponentFieldValueVecDestructor
);
/// A field in the component's internal data model.
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentDataField {
    /// Field name, e.g. "counter", "text", "number"
    pub name: AzString,
    /// Rich type descriptor for this field
    pub field_type: ComponentFieldType,
    /// Typed default value, or None if the field is required
    pub default_value: OptionComponentDefaultValue,
    /// Whether this field is required (must be provided by the parent)
    pub required: bool,
    /// Human-readable description
    pub description: AzString,
}
impl_vec!(
    ComponentDataField,
    ComponentDataFieldVec,
    ComponentDataFieldVecDestructor,
    ComponentDataFieldVecDestructorType,
    ComponentDataFieldVecSlice,
    OptionComponentDataField
);
impl_option!(
    ComponentDataField,
    OptionComponentDataField,
    copy = false,
    [Debug, Clone, PartialEq]
);
impl_vec_debug!(ComponentDataField, ComponentDataFieldVec);
impl_vec_partialeq!(ComponentDataField, ComponentDataFieldVec);
impl_vec_clone!(
    ComponentDataField,
    ComponentDataFieldVec,
    ComponentDataFieldVecDestructor
);
/// A named data model (struct definition) for code generation.
///
/// Stored in `ComponentLibrary::data_models`. Components reference these
/// by name in `ComponentDataField::field_type`, enabling nested/structured
/// data models. For example, a `UserCard` component might have a field
/// `user: UserProfile` where `UserProfile` is a `ComponentDataModel`.
#[derive(Debug, Clone)]
#[repr(C)]
pub struct ComponentDataModel {
    /// Type name, e.g. "`UserProfile`", "`TodoItem`"
    pub name: AzString,
    /// Human-readable description
    pub description: AzString,
    /// Fields in this struct
    pub fields: ComponentDataFieldVec,
}
impl ComponentDataModel {
    /// Look up a field by name.
44
    #[must_use] pub fn get_field(&self, name: &str) -> Option<&ComponentDataField> {
44
        self.fields
44
            .as_ref()
44
            .iter()
64
            .find(|f| f.name.as_str() == name)
44
    }
    /// Look up a field's default value as a string, if it exists and is a String variant.
26
    #[must_use] pub fn get_default_string(&self, name: &str) -> Option<&AzString> {
26
        self.get_field(name).and_then(|f| match &f.default_value {
12
            OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s),
3
            _ => None,
15
        })
26
    }
    /// Clone this data model, overriding the default value for a field by name.
    /// If the field is not found, the data model is returned unchanged.
13
    #[must_use] pub fn with_default(mut self, name: &str, value: ComponentDefaultValue) -> Self {
13
        let mut fields_vec = core::mem::replace(
13
            &mut self.fields,
13
            ComponentDataFieldVec::from_const_slice(&[]),
        )
13
        .into_library_owned_vec();
26
        for f in &mut fields_vec {
23
            if f.name.as_str() == name {
10
                f.default_value = OptionComponentDefaultValue::Some(value);
10
                break;
13
            }
        }
13
        self.fields = ComponentDataFieldVec::from_vec(fields_vec);
13
        self
13
    }
}
impl_vec!(
    ComponentDataModel,
    ComponentDataModelVec,
    ComponentDataModelVecDestructor,
    ComponentDataModelVecDestructorType,
    ComponentDataModelVecSlice,
    OptionComponentDataModel
);
impl_option!(
    ComponentDataModel,
    OptionComponentDataModel,
    copy = false,
    [Debug, Clone]
);
impl_vec_debug!(ComponentDataModel, ComponentDataModelVec);
impl_vec_clone!(
    ComponentDataModel,
    ComponentDataModelVec,
    ComponentDataModelVecDestructor
);
impl_vec_mut!(ComponentDataModel, ComponentDataModelVec);
// ============================================================================
// Serde support for ComponentDataModel (feature-gated)
// ============================================================================
#[cfg(feature = "serde-json")]
mod serde_impl {
    #[allow(clippy::wildcard_imports)] // serde impl module mirrors the parent surface
    use super::*;
    use serde::ser::SerializeStruct;
    use serde::{Deserialize, Deserializer, Serialize, Serializer};
    // --- AzString helpers ---
    fn ser_azstring<S: Serializer>(s: &AzString, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(s.as_str())
    }
    fn de_azstring<'de, D: Deserializer<'de>>(deserializer: D) -> Result<AzString, D::Error> {
        let s = String::deserialize(deserializer)?;
        Ok(AzString::from(s.as_str()))
    }
    // --- ComponentFieldType ---
    impl Serialize for ComponentFieldType {
3
        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3
            serializer.serialize_str(&field_type_to_string(self))
3
        }
    }
    impl<'de> Deserialize<'de> for ComponentFieldType {
3
        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3
            let s = String::deserialize(deserializer)?;
3
            Ok(string_to_field_type(&s))
3
        }
    }
3
    fn field_type_to_string(ft: &ComponentFieldType) -> String {
3
        match ft {
2
            ComponentFieldType::String => "String".into(),
            ComponentFieldType::Bool => "bool".into(),
            ComponentFieldType::I32 => "i32".into(),
            ComponentFieldType::I64 => "i64".into(),
1
            ComponentFieldType::U32 => "u32".into(),
            ComponentFieldType::U64 => "u64".into(),
            ComponentFieldType::Usize => "usize".into(),
            ComponentFieldType::F32 => "f32".into(),
            ComponentFieldType::F64 => "f64".into(),
            ComponentFieldType::ColorU => "ColorU".into(),
            ComponentFieldType::CssProperty => "CssProperty".into(),
            ComponentFieldType::ImageRef => "ImageRef".into(),
            ComponentFieldType::FontRef => "FontRef".into(),
            ComponentFieldType::StyledDom => "Dom".into(),
            ComponentFieldType::Callback(sig) => {
                alloc::format!("Callback({})", sig.return_type.as_str())
            }
            ComponentFieldType::RefAny(hint) => alloc::format!("RefAny({})", hint.as_str()),
            ComponentFieldType::OptionType(inner) => {
                alloc::format!("Option<{}>", field_type_to_string(inner.as_ref()))
            }
            ComponentFieldType::VecType(inner) => {
                alloc::format!("Vec<{}>", field_type_to_string(inner.as_ref()))
            }
            ComponentFieldType::StructRef(name) => alloc::format!("struct:{}", name.as_str()),
            ComponentFieldType::EnumRef(name) => alloc::format!("enum:{}", name.as_str()),
        }
3
    }
    // A 5-arm strip_prefix dispatch ladder. `option_if_let_else` (nursery)
    // wants `map_or_else` here, which would nest five closures inside each
    // other's else-branch — strictly less readable than the ladder.
    #[allow(clippy::option_if_let_else)]
3
    fn string_to_field_type(s: &str) -> ComponentFieldType {
3
        match s {
3
            "String" | "string" => ComponentFieldType::String,
1
            "bool" | "Bool" => ComponentFieldType::Bool,
1
            "i32" | "I32" => ComponentFieldType::I32,
1
            "i64" | "I64" => ComponentFieldType::I64,
1
            "u32" | "U32" => ComponentFieldType::U32,
            "u64" | "U64" => ComponentFieldType::U64,
            "usize" | "Usize" => ComponentFieldType::Usize,
            "f32" | "F32" => ComponentFieldType::F32,
            "f64" | "F64" => ComponentFieldType::F64,
            "ColorU" | "Color" | "color" => ComponentFieldType::ColorU,
            "CssProperty" => ComponentFieldType::CssProperty,
            "ImageRef" | "Image" => ComponentFieldType::ImageRef,
            "FontRef" | "Font" => ComponentFieldType::FontRef,
            "Dom" | "StyledDom" | "Children" => ComponentFieldType::StyledDom,
            other => {
                if let Some(inner) =
                    other.strip_prefix("Option<").and_then(|s| s.strip_suffix('>'))
                {
                    ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
                        string_to_field_type(inner),
                    ))
                } else if let Some(inner) =
                    other.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>'))
                {
                    ComponentFieldType::VecType(ComponentFieldTypeBox::new(string_to_field_type(
                        inner,
                    )))
                } else if let Some(name) = other.strip_prefix("struct:") {
                    ComponentFieldType::StructRef(AzString::from(name))
                } else if let Some(name) = other.strip_prefix("enum:") {
                    ComponentFieldType::EnumRef(AzString::from(name))
                } else if other.starts_with("Callback") {
                    let ret = other
                        .strip_prefix("Callback(")
                        .and_then(|s| s.strip_suffix(')'))
                        .unwrap_or("()");
                    ComponentFieldType::Callback(ComponentCallbackSignature {
                        return_type: AzString::from(ret),
                        args: ComponentCallbackArgVec::from_const_slice(&[]),
                    })
                } else if other.starts_with("RefAny") {
                    let hint = other
                        .strip_prefix("RefAny(")
                        .and_then(|s| s.strip_suffix(')'))
                        .unwrap_or("");
                    ComponentFieldType::RefAny(AzString::from(hint))
                } else {
                    ComponentFieldType::String // fallback
                }
            }
        }
3
    }
    // --- ComponentDefaultValue ---
    impl Serialize for ComponentDefaultValue {
2
        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
            use serde::ser::SerializeMap;
2
            match self {
                Self::None => serializer.serialize_none(),
1
                Self::String(s) => serializer.serialize_str(s.as_str()),
                Self::Bool(b) => serializer.serialize_bool(*b),
                Self::I32(v) => serializer.serialize_i32(*v),
                Self::I64(v) => serializer.serialize_i64(*v),
1
                Self::U32(v) => serializer.serialize_u32(*v),
                Self::U64(v) => serializer.serialize_u64(*v),
                Self::Usize(v) => serializer.serialize_u64(*v as u64),
                Self::F32(v) => serializer.serialize_f32(*v),
                Self::F64(v) => serializer.serialize_f64(*v),
                Self::ColorU(c) => serializer.serialize_str(&alloc::format!(
                    "#{:02x}{:02x}{:02x}{:02x}",
                    c.r,
                    c.g,
                    c.b,
                    c.a
                )),
                Self::ComponentInstance(ci) => {
                    let mut map = serializer.serialize_map(Some(2))?;
                    map.serialize_entry("library", ci.library.as_str())?;
                    map.serialize_entry("component", ci.component.as_str())?;
                    map.end()
                }
                Self::CallbackFnPointer(name) => {
                    serializer.serialize_str(name.as_str())
                }
                Self::Json(json_str) => {
                    // Serialize raw JSON string as-is by parsing and re-emitting
                    match serde_json::from_str::<serde_json::Value>(json_str.as_str()) {
                        Ok(v) => v.serialize(serializer),
                        Err(_) => serializer.serialize_str(json_str.as_str()),
                    }
                }
            }
2
        }
    }
    impl<'de> Deserialize<'de> for ComponentDefaultValue {
2
        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2
            let val = serde_json::Value::deserialize(deserializer)?;
            // NOTE: `Value::Null` is deliberately NOT its own arm — it maps to
            // `Self::None`, which is exactly what the catch-all below produces.
2
            Ok(match val {
                serde_json::Value::Bool(b) => Self::Bool(b),
1
                serde_json::Value::Number(n) => {
1
                    n.as_i64().map_or_else(
                        || n.as_f64().map_or(Self::None, Self::F64),
1
                        |i| i32::try_from(i).map_or(Self::I64(i), Self::I32),
                    )
                }
1
                serde_json::Value::String(s) => {
1
                    Self::String(AzString::from(s.as_str()))
                }
                _ => Self::None,
            })
2
        }
    }
    // --- OptionComponentDefaultValue ---
    impl Serialize for OptionComponentDefaultValue {
3
        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3
            match self {
2
                Self::Some(v) => v.serialize(serializer),
1
                Self::None => serializer.serialize_none(),
            }
3
        }
    }
    impl<'de> Deserialize<'de> for OptionComponentDefaultValue {
3
        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3
            let val = Option::<ComponentDefaultValue>::deserialize(deserializer)?;
3
            Ok(val.map_or(Self::None, Self::Some))
3
        }
    }
    // --- ComponentDataField ---
    impl Serialize for ComponentDataField {
3
        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3
            let mut s = serializer.serialize_struct("ComponentDataField", 5)?;
3
            s.serialize_field("name", self.name.as_str())?;
3
            s.serialize_field("type", &self.field_type)?;
3
            s.serialize_field("default", &self.default_value)?;
3
            s.serialize_field("required", &self.required)?;
3
            s.serialize_field("description", self.description.as_str())?;
3
            s.end()
3
        }
    }
    impl<'de> Deserialize<'de> for ComponentDataField {
3
        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
            #[derive(Deserialize)]
            struct Helper {
                name: String,
                #[serde(rename = "type", default = "default_type")]
                field_type: ComponentFieldType,
                #[serde(default)]
                default: OptionComponentDefaultValue,
                #[serde(default)]
                required: bool,
                #[serde(default)]
                description: String,
            }
            const fn default_type() -> ComponentFieldType {
                ComponentFieldType::String
            }
3
            let h = Helper::deserialize(deserializer)?;
3
            Ok(Self {
3
                name: AzString::from(h.name.as_str()),
3
                field_type: h.field_type,
3
                default_value: h.default,
3
                required: h.required,
3
                description: AzString::from(h.description.as_str()),
3
            })
3
        }
    }
    // --- ComponentDataModel ---
    impl Serialize for ComponentDataModel {
2
        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2
            let mut s = serializer.serialize_struct("ComponentDataModel", 3)?;
2
            s.serialize_field("name", self.name.as_str())?;
2
            s.serialize_field("description", self.description.as_str())?;
2
            let fields: Vec<&ComponentDataField> =
2
                self.fields.as_ref().iter().collect();
2
            s.serialize_field("fields", &fields)?;
2
            s.end()
2
        }
    }
    impl<'de> Deserialize<'de> for ComponentDataModel {
        /// A data model is a JSON **object**. This deliberately drives the
        /// deserializer with `deserialize_map` instead of `deserialize_struct`:
        /// the struct hint makes serde accept a *sequence* as well (the
        /// positional encoding used by compact formats), so `from_json("[]")`
        /// used to succeed and hand back a nameless, field-less model instead of
        /// reporting that the input is not a data model at all. Every key stays
        /// optional, so `{}` still deserializes to the empty model.
15
        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
            use serde::de::{IgnoredAny, MapAccess, Visitor};
            struct ModelVisitor;
            impl<'de> Visitor<'de> for ModelVisitor {
                type Value = ComponentDataModel;
6
                fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6
                    f.write_str("a data model object with `name`, `description` and `fields`")
6
                }
3
                fn visit_map<A: MapAccess<'de>>(
3
                    self,
3
                    mut map: A,
3
                ) -> Result<Self::Value, A::Error> {
3
                    let mut name: Option<String> = None;
3
                    let mut description: Option<String> = None;
3
                    let mut fields: Option<Vec<ComponentDataField>> = None;
9
                    while let Some(key) = map.next_key::<String>()? {
6
                        match key.as_str() {
6
                            "name" => name = Some(map.next_value()?),
4
                            "description" => description = Some(map.next_value()?),
2
                            "fields" => fields = Some(map.next_value()?),
                            // Unknown keys are ignored (forward compatibility),
                            // but their values must still be consumed.
                            _ => {
                                map.next_value::<IgnoredAny>()?;
                            }
                        }
                    }
2
                    Ok(ComponentDataModel {
2
                        name: AzString::from(name.unwrap_or_default().as_str()),
2
                        description: AzString::from(description.unwrap_or_default().as_str()),
2
                        fields: ComponentDataFieldVec::from_vec(fields.unwrap_or_default()),
2
                    })
3
                }
            }
15
            deserializer.deserialize_map(ModelVisitor)
15
        }
    }
}
// NOTE: no `pub use serde_impl::*` — the module holds only private helpers and
// trait impls, and trait impls are in scope crate-wide (and for downstream
// users) regardless of the defining module's visibility.
#[cfg(feature = "serde-json")]
impl ComponentDataModel {
    /// Serialize this data model to a JSON string.
    ///
    /// # Errors
    ///
    /// Returns the serializer's error message if the model cannot be
    /// represented as JSON.
2
    pub fn to_json(&self) -> Result<String, String> {
2
        serde_json::to_string_pretty(self).map_err(|e| alloc::format!("{e}"))
2
    }
    /// Deserialize a data model from a JSON string.
    ///
    /// # Errors
    ///
    /// Returns the parser's error message if `json` is malformed or does not
    /// match the data-model shape.
15
    pub fn from_json(json: &str) -> Result<Self, String> {
15
        serde_json::from_str(json).map_err(|e| alloc::format!("{e}"))
15
    }
}
/// Source of a component definition — determines whether it can be exported
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum ComponentSource {
    /// Built into the DLL (HTML elements). Never exported.
    Builtin,
    /// Compiled Rust widget (Button, `TextInput`, etc.). Never exported.
    Compiled,
    /// Defined via JSON/XML at runtime. Can be exported.
    #[default]
    UserDefined,
}
impl ComponentSource {
1
    #[must_use] pub fn create() -> Self {
1
        Self::default()
1
    }
}
/// The target language for code compilation
// Threaded by reference through the codegen call graph; kept non-Copy so
// deriving Copy doesn't force trivially_copy_pass_by_ref churn across the many
// &CompileTarget codegen callers for a perf-neutral change.
#[allow(missing_copy_implementations)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum CompileTarget {
    Rust,
    C,
    Cpp,
    Python,
}
impl_result!(
    StyledDom,
    RenderDomError,
    ResultStyledDomRenderDomError,
    copy = false,
    [Debug, Clone, PartialEq]
);
impl_result!(
    AzString,
    CompileError,
    ResultStringCompileError,
    copy = false,
    [Debug, Clone, PartialEq]
);
/// Render function type: takes component definition + data model (with current values
/// in `default_value` fields) + component map for recursive sub-component instantiation,
/// returns `StyledDom`.
///
/// The `data` parameter is typically `def.data_model` cloned and with caller-provided
/// values substituted into the `default_value` fields.
pub type ComponentRenderFn =
    fn(&ComponentDef, &ComponentDataModel, &ComponentMap) -> ResultStyledDomRenderDomError;
/// Compile function type: takes component definition + target language + data model, returns source code.
pub type ComponentCompileFn = fn(
    &ComponentDef,
    &CompileTarget,
    &ComponentDataModel,
    indent: usize,
) -> ResultStringCompileError;
/// Raw function pointer type that returns a single `ComponentDef` when called.
/// Used as the `cb` field in `RegisterComponentFn`.
pub type RegisterComponentFnType = extern "C" fn() -> ComponentDef;
/// Callback struct for registering individual components at startup.
///
/// In C: pass a bare `extern "C" fn() -> ComponentDef` function pointer —
/// it converts automatically via `From<RegisterComponentFnType>`.
///
/// In Python: construct this struct with `cb` set to a trampoline and
/// `ctx` set to `Some(RefAny(...))` wrapping the Python callable.
#[repr(C)]
pub struct RegisterComponentFn {
    pub cb: RegisterComponentFnType,
    /// For FFI: stores the foreign callable (e.g., `PyFunction`).
    /// Native Rust/C code sets this to None.
    pub ctx: crate::refany::OptionRefAny,
}
impl_callback!(RegisterComponentFn, RegisterComponentFnType);
/// Raw function pointer type that returns a complete `ComponentLibrary` when called.
/// Used as the `cb` field in `RegisterComponentLibraryFn`.
pub type RegisterComponentLibraryFnType = extern "C" fn() -> ComponentLibrary;
/// Callback struct for registering entire component libraries at startup.
///
/// In C: pass a bare `extern "C" fn() -> ComponentLibrary` function pointer —
/// it converts automatically via `From<RegisterComponentLibraryFnType>`.
///
/// In Python: construct this struct with `cb` set to a trampoline and
/// `ctx` set to `Some(RefAny(...))` wrapping the Python callable.
#[repr(C)]
pub struct RegisterComponentLibraryFn {
    pub cb: RegisterComponentLibraryFnType,
    /// For FFI: stores the foreign callable (e.g., `PyFunction`).
    /// Native Rust/C code sets this to None.
    pub ctx: crate::refany::OptionRefAny,
}
impl_callback!(RegisterComponentLibraryFn, RegisterComponentLibraryFnType);
/// A component definition — the "class" / "template" of a component.
/// Can come from Rust builtins, compiled widgets, JSON, or user creation in debugger.
///
#[derive(Clone)]
#[repr(C)]
pub struct ComponentDef {
    /// Collection + name, e.g. builtin:div, shadcn:avatar
    pub id: ComponentId,
    /// Human-readable display name, e.g. "Link" for builtin:a, "Avatar" for shadcn:avatar
    pub display_name: AzString,
    /// Markdown documentation for the component
    pub description: AzString,
    /// The component's CSS
    pub css: AzString,
    /// Where this component was defined (determines exportability)
    pub source: ComponentSource,
    /// Unified data model: all value fields, callback slots, and child slots
    /// in a single named struct. Code gen uses `data_model.name` as the
    /// input struct type name (e.g. "`ButtonData`").
    /// The `default_value` on each field doubles as the "current value" for
    /// preview rendering — callers override defaults before calling `render_fn`.
    pub data_model: ComponentDataModel,
    /// Render to live DOM
    pub render_fn: ComponentRenderFn,
    /// Compile to source code in target language
    pub compile_fn: ComponentCompileFn,
    /// Source code for `render_fn` (user-defined components only)
    pub render_fn_source: OptionString,
    /// Source code for `compile_fn` (user-defined components only)
    pub compile_fn_source: OptionString,
}
impl fmt::Debug for ComponentDef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ComponentDef")
            .field("id", &self.id)
            .field("display_name", &self.display_name)
            .field("source", &self.source)
            .field("data_model", &self.data_model.name)
            .finish_non_exhaustive()
    }
}
impl_vec!(
    ComponentDef,
    ComponentDefVec,
    ComponentDefVecDestructor,
    ComponentDefVecDestructorType,
    ComponentDefVecSlice,
    OptionComponentDef
);
impl_option!(ComponentDef, OptionComponentDef, copy = false, [Clone]);
impl_vec_debug!(ComponentDef, ComponentDefVec);
impl_vec_clone!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor);
impl_vec_mut!(ComponentDef, ComponentDefVec);
/// A named collection of component definitions
#[derive(Debug, Clone)]
#[repr(C)]
pub struct ComponentLibrary {
    /// Library identifier, e.g. "builtin", "shadcn", "myproject"
    pub name: AzString,
    /// Version string
    pub version: AzString,
    /// Human-readable description
    pub description: AzString,
    /// The components in this library
    pub components: ComponentDefVec,
    /// Whether this library can be exported (false for builtin/compiled)
    pub exportable: bool,
    /// Whether this library can be modified by the user (add/remove/edit components).
    /// False for builtin and compiled libraries. True for user-created libraries.
    pub modifiable: bool,
    /// Named data model types defined by this library.
    /// Components reference these by name in their `field_type`.
    pub data_models: ComponentDataModelVec,
    /// Named enum types defined by this library.
    /// Components reference these via `ComponentFieldType::EnumRef(name)`.
    pub enum_models: ComponentEnumModelVec,
}
impl_vec!(
    ComponentLibrary,
    ComponentLibraryVec,
    ComponentLibraryVecDestructor,
    ComponentLibraryVecDestructorType,
    ComponentLibraryVecSlice,
    OptionComponentLibrary
);
impl_option!(
    ComponentLibrary,
    OptionComponentLibrary,
    copy = false,
    [Debug, Clone]
);
impl_vec_debug!(ComponentLibrary, ComponentLibraryVec);
impl_vec_clone!(
    ComponentLibrary,
    ComponentLibraryVec,
    ComponentLibraryVecDestructor
);
impl_vec_mut!(ComponentLibrary, ComponentLibraryVec);
/// The component map — holds libraries with namespaced components.
#[derive(Debug, Clone)]
#[repr(C)]
pub struct ComponentMap {
    /// Libraries indexed by name. "builtin" is always present.
    pub libraries: ComponentLibraryVec,
}
impl ComponentMap {
    /// Qualified lookup: "shadcn:avatar" -> finds library "shadcn", component "avatar"
38
    #[must_use] pub fn get(&self, collection: &str, name: &str) -> Option<&ComponentDef> {
38
        self.libraries
38
            .iter()
38
            .find(|lib| lib.name.as_str() == collection)
1935
            .and_then(|lib| lib.components.iter().find(|c| c.id.name.as_str() == name))
38
    }
    /// Unqualified lookup: "div" -> searches ONLY the "builtin" library.
13
    #[must_use] pub fn get_unqualified(&self, name: &str) -> Option<&ComponentDef> {
13
        self.get("builtin", name)
13
    }
    /// Parse a "collection:name" string into a lookup
11
    #[must_use] pub fn get_by_qualified_name(&self, qualified: &str) -> Option<&ComponentDef> {
11
        if let Some((collection, name)) = qualified.split_once(':') {
7
            self.get(collection, name)
        } else {
4
            self.get_unqualified(qualified)
        }
11
    }
    /// Get all libraries that can be exported (user-defined only)
3
    #[must_use] pub fn get_exportable_libraries(&self) -> Vec<&ComponentLibrary> {
3
        self.libraries.iter().filter(|lib| lib.exportable).collect()
3
    }
    /// Get all component definitions across all libraries
5
    #[must_use] pub fn all_components(&self) -> Vec<&ComponentDef> {
5
        self.libraries
5
            .iter()
5
            .flat_map(|lib| lib.components.iter())
5
            .collect()
5
    }
}
// ============================================================================
// Builtin component bridge — wraps existing render/compile into ComponentDef
// ============================================================================
/// Single source of truth mapping HTML/SVG tag names to node variants.
///
/// Each `"tag" => Variant` entry expands to **both** a `NodeType::Variant` arm in
/// [`tag_to_node_type`] and a `NodeTypeTag::Variant` arm in [`tag_to_node_type_tag`],
/// so the two lookups can never drift apart. Tags whose two enums diverge —
/// `img`, `image`, `icon` — are handled as explicit special cases inside each
/// generated function and are intentionally absent from this table.
macro_rules! html_tag_node_types {
    ($($tag:literal => $variant:ident),* $(,)?) => {
        /// Map a builtin tag name to its corresponding `NodeType`.
        /// Falls back to `NodeType::Div` for unknown tags.
154450
        #[must_use] pub fn tag_to_node_type(tag: &str) -> NodeType {
154450
            match tag {
                // `<img>` becomes a replaced `NodeType::Image`. The `src` attribute is not
                // available here, so a placeholder `NullImage` (0x0, empty tag) is created;
                // `xml_node_to_dom_fast` overrides it with a `NullImage` whose `tag` carries
                // the `src` bytes so a renderer (e.g. printpdf) can resolve the actual image.
154450
                "img" => NodeType::Image(azul_css::css::BoxOrStatic::heap(
5
                    crate::resources::ImageRef::null_image(
5
                        0,
5
                        0,
5
                        crate::resources::RawImageFormat::RGBA8,
5
                        alloc::vec::Vec::new(),
5
                    ),
5
                )),
                // `<icon>content_copy</icon>` becomes an un-named `NodeType::Icon`;
                // the icon SPEC is its text content, consumed by the icon
                // resolution pass (`resolve_icons_in_styled_dom`) against the
                // registered icon packs — exactly like a ligature icon font
                // turns glyph text into an icon. The builders stay generic.
154445
                "icon" => NodeType::Icon(azul_css::css::BoxOrStatic::heap(
45
                    azul_css::AzString::from_const_str(""),
45
                )),
                $($tag => NodeType::$variant,)*
                _ => NodeType::Div,
            }
154450
        }
        /// Map a tag name to its CSS `NodeTypeTag` for CSS matching in the compile pipeline.
        /// Falls back to `NodeTypeTag::Div` for unknown tags.
6
        fn tag_to_node_type_tag(tag: &str) -> NodeTypeTag {
6
            match tag {
                // `img`/`image`/`icon` have no 1:1 `NodeType` equivalent (see
                // `tag_to_node_type`), so they map to dedicated `NodeTypeTag` variants.
6
                "img" | "image" => NodeTypeTag::Img,
6
                "icon" => NodeTypeTag::Icon,
                $($tag => NodeTypeTag::$variant,)*
                _ => NodeTypeTag::Div,
            }
6
        }
    };
}
html_tag_node_types! {
    // Document structure
    "html" => Html,
    "head" => Head,
    "title" => Title,
    "body" => Body,
    // Block-level
    "div" => Div,
    "header" => Header,
    "footer" => Footer,
    "section" => Section,
    "article" => Article,
    "aside" => Aside,
    "nav" => Nav,
    "main" => Main,
    "figure" => Figure,
    "figcaption" => FigCaption,
    "address" => Address,
    "details" => Details,
    "summary" => Summary,
    "dialog" => Dialog,
    // Headings
    "h1" => H1,
    "h2" => H2,
    "h3" => H3,
    "h4" => H4,
    "h5" => H5,
    "h6" => H6,
    // Text content
    "p" => P,
    "span" => Span,
    "pre" => Pre,
    "code" => Code,
    "blockquote" => BlockQuote,
    "br" => Br,
    "hr" => Hr,
    "pagebreak" => PageBreak,
    // Lists
    "ul" => Ul,
    "ol" => Ol,
    "li" => Li,
    "dl" => Dl,
    "dt" => Dt,
    "dd" => Dd,
    "menu" => Menu,
    "menuitem" => MenuItem,
    "dir" => Dir,
    // Tables
    "table" => Table,
    "caption" => Caption,
    "thead" => THead,
    "tbody" => TBody,
    "tfoot" => TFoot,
    "tr" => Tr,
    "th" => Th,
    "td" => Td,
    "colgroup" => ColGroup,
    "col" => Col,
    // Forms
    "form" => Form,
    "fieldset" => FieldSet,
    "legend" => Legend,
    "label" => Label,
    "input" => Input,
    "button" => Button,
    "select" => Select,
    "optgroup" => OptGroup,
    "option" => SelectOption,
    "textarea" => TextArea,
    "output" => Output,
    "progress" => Progress,
    "meter" => Meter,
    "datalist" => DataList,
    // Inline
    "a" => A,
    "strong" => Strong,
    "em" => Em,
    "b" => B,
    "i" => I,
    "u" => U,
    "s" => S,
    "small" => Small,
    "mark" => Mark,
    "del" => Del,
    "ins" => Ins,
    "samp" => Samp,
    "kbd" => Kbd,
    "var" => Var,
    "cite" => Cite,
    "dfn" => Dfn,
    "abbr" => Abbr,
    "acronym" => Acronym,
    "q" => Q,
    "time" => Time,
    "sub" => Sub,
    "sup" => Sup,
    "big" => Big,
    "bdo" => Bdo,
    "bdi" => Bdi,
    "wbr" => Wbr,
    "ruby" => Ruby,
    "rt" => Rt,
    "rtc" => Rtc,
    "rp" => Rp,
    "data" => Data,
    // Embedded content (`img` is a special case in the generated fns)
    "canvas" => Canvas,
    "object" => Object,
    "param" => Param,
    "embed" => Embed,
    "audio" => Audio,
    "video" => Video,
    "source" => Source,
    "track" => Track,
    "map" => Map,
    "area" => Area,
    // SVG elements
    "svg" => Svg,
    "g" => SvgG,
    "defs" => SvgDefs,
    "symbol" => SvgSymbol,
    "use" => SvgUse,
    "switch" => SvgSwitch,
    "path" => SvgPath,
    "circle" => SvgCircle,
    "rect" => SvgRect,
    "ellipse" => SvgEllipse,
    "line" => SvgLine,
    "polygon" => SvgPolygon,
    "polyline" => SvgPolyline,
    "tspan" => SvgTspan,
    "textpath" => SvgTextPath,
    "lineargradient" => SvgLinearGradient,
    "radialgradient" => SvgRadialGradient,
    "stop" => SvgStop,
    "pattern" => SvgPattern,
    "clippath" => SvgClipPathElement,
    "mask" => SvgMask,
    "filter" => SvgFilter,
    "feblend" => SvgFeBlend,
    "fecolormatrix" => SvgFeColorMatrix,
    "fecomponenttransfer" => SvgFeComponentTransfer,
    "fecomposite" => SvgFeComposite,
    "feconvolvematrix" => SvgFeConvolveMatrix,
    "fediffuselighting" => SvgFeDiffuseLighting,
    "fedisplacementmap" => SvgFeDisplacementMap,
    "fedistantlight" => SvgFeDistantLight,
    "fedropshadow" => SvgFeDropShadow,
    "feflood" => SvgFeFlood,
    "fefuncr" => SvgFeFuncR,
    "fefuncg" => SvgFeFuncG,
    "fefuncb" => SvgFeFuncB,
    "fefunca" => SvgFeFuncA,
    "fegaussianblur" => SvgFeGaussianBlur,
    "feimage" => SvgFeImage,
    "femerge" => SvgFeMerge,
    "femergenode" => SvgFeMergeNode,
    "femorphology" => SvgFeMorphology,
    "feoffset" => SvgFeOffset,
    "fepointlight" => SvgFePointLight,
    "fespecularlighting" => SvgFeSpecularLighting,
    "fespotlight" => SvgFeSpotLight,
    "fetile" => SvgFeTile,
    "feturbulence" => SvgFeTurbulence,
    "foreignobject" => SvgForeignObject,
    "desc" => SvgDesc,
    "view" => SvgView,
    "animate" => SvgAnimate,
    "animatemotion" => SvgAnimateMotion,
    "animatetransform" => SvgAnimateTransform,
    "set" => SvgSet,
    "mpath" => SvgMpath,
    // Metadata
    "meta" => Meta,
    "link" => Link,
    "script" => Script,
    "style" => Style,
    "base" => Base,
}
/// Default render function for builtin HTML elements.
/// Delegates to creating a DOM node of the appropriate `NodeType`.
2
fn builtin_render_fn(
2
    def: &ComponentDef,
2
    data: &ComponentDataModel,
2
    _component_map: &ComponentMap,
2
) -> ResultStyledDomRenderDomError {
2
    let node_type = tag_to_node_type(def.id.name.as_str());
2
    let mut dom = Dom::create_node(node_type);
2
    if let Some(text_str) = data.get_default_string("text") {
1
        let prepared = prepare_string(text_str);
1
        if !prepared.is_empty() {
1
            dom = dom.with_children(alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(prepared)].into());
1
        }
1
    }
2
    let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut dom, Css::empty()));
2
    r.into()
2
}
/// Default compile function for builtin HTML elements.
/// Generates `Dom::create_node(NodeType::Div)` style code for the target language.
9
fn builtin_compile_fn(
9
    def: &ComponentDef,
9
    target: &CompileTarget,
9
    data: &ComponentDataModel,
9
    indent: usize,
9
) -> ResultStringCompileError {
9
    let node_type = tag_to_node_type(def.id.name.as_str());
9
    let type_name = format!("{node_type:?}"); // "Div", "Body", "P", etc.
9
    let text = data.get_default_string("text");
9
    let r: Result<AzString, CompileError> = match target {
        CompileTarget::Rust => {
6
            text.map_or_else(|| Ok(format!("Dom::create_node(NodeType::{type_name})").into()), |text_str| Ok(format!(
1
                    "Dom::create_node(NodeType::{}).with_children(vec![Dom::create_text_do_not_use_without_block_level_wrapper(\"{}\")])",
                    type_name,
1
                    text_str.as_str().replace('\\', "\\\\").replace('"', "\\\"")
1
                ).into()))
        }
        CompileTarget::C => {
1
            text.map_or_else(|| Ok(format!("AzDom_create{type_name}()").into()), |text_str| Ok(format!(
                    "AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{}\"))",
                    text_str
                        .as_str()
                        .replace('\\', "\\\\")
                        .replace('"', "\\\"")
                )
                .into()))
        }
1
        CompileTarget::Cpp => Ok(format!("Dom::create_{}()", type_name.to_lowercase()).into()),
1
        CompileTarget::Python => Ok(format!("Dom.create_{}()", type_name.to_lowercase()).into()),
    };
9
    r.into()
9
}
/// Pushes a `<div>` containing `"field_name: value"` text into the children list.
11
fn push_scalar_field(children: &mut Vec<Dom>, field_name: &str, value: &dyn fmt::Display) {
    use crate::dom::{Dom, NodeType};
11
    let text = alloc::format!("{field_name}: {value}");
11
    children.push(
11
        Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into()),
    );
11
}
/// Default render function for user-defined (JSON-imported) components.
///
/// Interprets the `ComponentDef` structure generically:
/// 1. Creates a wrapper `<div>` with the component's CSS class
/// 2. For each data field, renders content based on type:
///    - String fields → text node with current value
///    - Bool fields → conditional display
///    - `StyledDom` fields → embeds the child DOM subtree
///    - StructRef/EnumRef → recursively renders sub-components if found in `ComponentMap`
///    - Other scalar fields → text display of the value
/// 3. Applies the component's scoped CSS
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
4
#[must_use] pub fn user_defined_render_fn(
4
    def: &ComponentDef,
4
    data: &ComponentDataModel,
4
    component_map: &ComponentMap,
4
) -> ResultStyledDomRenderDomError {
    use crate::dom::{Dom, NodeType};
    use azul_css::css::Css;
4
    let mut children: Vec<Dom> = Vec::new();
15
    for field in data.fields.as_ref() {
15
        let field_name = field.name.as_str();
        // Get the current value from default_value
15
        match &field.default_value {
1
            OptionComponentDefaultValue::None => {
1
                // Required field with no value — skip in preview
1
            }
14
            OptionComponentDefaultValue::Some(default_val) => {
14
                match default_val {
1
                    ComponentDefaultValue::String(s) => {
1
                        let text = s.as_str().trim();
1
                        if !text.is_empty() {
1
                            let label_dom = Dom::create_node(NodeType::Div).with_children(
1
                                alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text.to_string())].into(),
1
                            );
1
                            children.push(label_dom);
1
                        }
                    }
1
                    ComponentDefaultValue::Bool(v) => {
1
                        push_scalar_field(&mut children, field_name, v);
1
                    }
1
                    ComponentDefaultValue::I32(v) => {
1
                        push_scalar_field(&mut children, field_name, v);
1
                    }
1
                    ComponentDefaultValue::I64(v) => {
1
                        push_scalar_field(&mut children, field_name, v);
1
                    }
1
                    ComponentDefaultValue::U32(v) => {
1
                        push_scalar_field(&mut children, field_name, v);
1
                    }
1
                    ComponentDefaultValue::U64(v) => {
1
                        push_scalar_field(&mut children, field_name, v);
1
                    }
1
                    ComponentDefaultValue::Usize(v) => {
1
                        push_scalar_field(&mut children, field_name, v);
1
                    }
1
                    ComponentDefaultValue::F32(v) => {
1
                        push_scalar_field(&mut children, field_name, v);
1
                    }
1
                    ComponentDefaultValue::F64(v) => {
1
                        push_scalar_field(&mut children, field_name, v);
1
                    }
1
                    ComponentDefaultValue::ColorU(c) => {
1
                        let text = alloc::format!(
1
                            "{}: #{:02x}{:02x}{:02x}{:02x}",
1
                            field_name,
1
                            c.r,
1
                            c.g,
1
                            c.b,
1
                            c.a
1
                        );
1
                        children.push(
1
                            Dom::create_node(NodeType::Div)
1
                                .with_children(alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into()),
1
                        );
1
                    }
1
                    ComponentDefaultValue::ComponentInstance(ci) => {
                        // Recursively instantiate sub-component from ComponentMap
                        if let Some(sub_comp) =
1
                            component_map.get(ci.library.as_str(), ci.component.as_str())
                        {
                            let sub_data = sub_comp.data_model.clone();
                            match (sub_comp.render_fn)(sub_comp, &sub_data, component_map) {
                                ResultStyledDomRenderDomError::Ok(_styled_dom) => {
                                    // Sub-component rendered successfully — add a placeholder
                                    // (StyledDom cannot be directly converted back to Dom)
                                    let text = alloc::format!(
                                        "[{}:{}]",
                                        ci.library.as_str(),
                                        ci.component.as_str()
                                    );
                                    children.push(
                                        Dom::create_node(NodeType::Div).with_children(
                                            alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into(),
                                        ),
                                    );
                                }
                                ResultStyledDomRenderDomError::Err(_) => {
                                    // On error, show a placeholder
                                    let text = alloc::format!(
                                        "[Error rendering {}:{}]",
                                        ci.library.as_str(),
                                        ci.component.as_str()
                                    );
                                    children.push(
                                        Dom::create_node(NodeType::Div).with_children(
                                            alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into(),
                                        ),
                                    );
                                }
                            }
1
                        } else {
1
                            let text = alloc::format!(
1
                                "[Unknown component {}:{}]",
1
                                ci.library.as_str(),
1
                                ci.component.as_str()
1
                            );
1
                            children.push(
1
                                Dom::create_node(NodeType::Div)
1
                                    .with_children(alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into()),
1
                            );
1
                        }
                    }
1
                    ComponentDefaultValue::CallbackFnPointer(name) => {
1
                        // Callbacks are not rendered, just acknowledged
1
                        let text = alloc::format!("{}: fn({})", field_name, name.as_str());
1
                        children.push(
1
                            Dom::create_node(NodeType::Div)
1
                                .with_children(alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into()),
1
                        );
1
                    }
1
                    ComponentDefaultValue::Json(json_str) => {
1
                        let text = alloc::format!("{}: {}", field_name, json_str.as_str());
1
                        children.push(
1
                            Dom::create_node(NodeType::Div)
1
                                .with_children(alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into()),
1
                        );
1
                    }
1
                    ComponentDefaultValue::None => {
1
                        // No default, skip
1
                    }
                }
            }
        }
    }
4
    let mut wrapper = Dom::create_node(NodeType::Div);
4
    if !children.is_empty() {
2
        wrapper = wrapper.with_children(children.into());
2
    }
    // Apply component CSS
4
    let css = if def.css.as_str().is_empty() {
3
        Css::empty()
    } else {
1
        Css::from_string(def.css.clone())
    };
4
    let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut wrapper, css));
4
    r.into()
4
}
/// Default compile function for user-defined (JSON-imported) components.
///
/// Generates source code that creates the component's DOM structure for the
/// target language. For each data field, emits the appropriate code:
/// - String fields → text node creation
/// - Scalar fields → formatted display
/// - `ComponentInstance` → function call to sub-component's render function
/// - `StyledDom` slots → child parameter pass-through
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
9
#[must_use] pub fn user_defined_compile_fn(
9
    def: &ComponentDef,
9
    target: &CompileTarget,
9
    data: &ComponentDataModel,
9
    indent: usize,
9
) -> ResultStringCompileError {
9
    let tag = def.id.name.as_str();
9
    let indent_str = " ".repeat(indent * 4);
9
    let inner_indent = " ".repeat((indent + 1) * 4);
9
    let r: Result<AzString, CompileError> = match target {
        CompileTarget::Rust => {
6
            let mut lines = Vec::new();
6
            lines.push(alloc::format!("{indent_str}// Component: {tag}"));
6
            lines.push(alloc::format!(
6
                "{indent_str}let mut children: Vec<Dom> = Vec::new();"
            ));
15
            for field in data.fields.as_ref() {
15
                let fname = field.name.as_str();
14
                match &field.default_value {
2
                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
2
                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
2
                        lines.push(alloc::format!(
2
                            "{inner_indent}children.push(Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"));"
2
                        ));
2
                    }
1
                    OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => {
1
                        lines.push(alloc::format!(
1
                            "{inner_indent}children.push(Dom::create_text_do_not_use_without_block_level_wrapper(format!(\"{{}}: {{}}\", \"{fname}\", {b}).as_str()));"
1
                        ));
1
                    }
                    OptionComponentDefaultValue::Some(
                        ComponentDefaultValue::ComponentInstance(ci),
                    ) => {
                        let fn_name =
                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
                        lines.push(alloc::format!(
                            "{}children.push({}()); // sub-component {}:{}",
                            inner_indent,
                            fn_name,
                            ci.library.as_str(),
                            ci.component.as_str()
                        ));
                    }
12
                    _ => {
12
                        // For other types, generate a placeholder comment
12
                        lines.push(alloc::format!(
12
                            "{}// field '{}': {:?}",
12
                            inner_indent,
12
                            fname,
12
                            field.field_type
12
                        ));
12
                    }
                }
            }
6
            lines.push(alloc::format!(
6
                "{indent_str}Dom::create_node(NodeType::Div).with_children(children.into())"
            ));
6
            Ok(lines.join("\n").into())
        }
        CompileTarget::C => {
1
            let mut lines = Vec::new();
1
            lines.push(alloc::format!("{indent_str}/* Component: {tag} */"));
1
            lines.push(alloc::format!(
1
                "{indent_str}AzDom root = AzDom_createDiv();"
            ));
14
            for field in data.fields.as_ref() {
14
                let fname = field.name.as_str();
13
                match &field.default_value {
1
                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
1
                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
1
                        lines.push(alloc::format!(
1
                            "{inner_indent}AzDom_addChild(&root, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{escaped}\")));"
1
                        ));
1
                    }
                    OptionComponentDefaultValue::Some(
                        ComponentDefaultValue::ComponentInstance(ci),
                    ) => {
                        let fn_name =
                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
                        lines.push(alloc::format!(
                            "{inner_indent}AzDom_addChild(&root, {fn_name}());"
                        ));
                    }
13
                    _ => {
13
                        lines.push(alloc::format!("{inner_indent}/* field '{fname}' */"));
13
                    }
                }
            }
1
            lines.push(alloc::format!("{indent_str}return root;"));
1
            Ok(lines.join("\n").into())
        }
        CompileTarget::Cpp => {
1
            let mut lines = Vec::new();
1
            lines.push(alloc::format!("{indent_str}// Component: {tag}"));
1
            lines.push(alloc::format!(
1
                "{indent_str}auto root = Dom::create_div();"
            ));
14
            for field in data.fields.as_ref() {
14
                let fname = field.name.as_str();
13
                match &field.default_value {
1
                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
1
                        let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
1
                        lines.push(alloc::format!(
1
                            "{inner_indent}root.add_child(Dom::create_text_do_not_use_without_block_level_wrapper(String(\"{escaped}\")));"
1
                        ));
1
                    }
                    OptionComponentDefaultValue::Some(
                        ComponentDefaultValue::ComponentInstance(ci),
                    ) => {
                        let fn_name =
                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
                        lines.push(alloc::format!(
                            "{inner_indent}root.add_child({fn_name}());"
                        ));
                    }
13
                    _ => {
13
                        lines.push(alloc::format!("{inner_indent}// field '{fname}'"));
13
                    }
                }
            }
1
            lines.push(alloc::format!("{indent_str}return root;"));
1
            Ok(lines.join("\n").into())
        }
        CompileTarget::Python => {
1
            let mut lines = Vec::new();
1
            lines.push(alloc::format!("{indent_str}# Component: {tag}"));
1
            lines.push(alloc::format!("{indent_str}root = Dom.create_div()"));
14
            for field in data.fields.as_ref() {
14
                let fname = field.name.as_str();
13
                match &field.default_value {
1
                    OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
1
                        let escaped = s
1
                            .as_str()
1
                            .replace('\\', "\\\\")
1
                            .replace('"', "\\\"")
1
                            .replace('\'', "\\'");
1
                        lines.push(alloc::format!(
1
                            "{inner_indent}root = root.with_child(Dom.create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"))"
1
                        ));
1
                    }
                    OptionComponentDefaultValue::Some(
                        ComponentDefaultValue::ComponentInstance(ci),
                    ) => {
                        let fn_name =
                            alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
                        lines.push(alloc::format!(
                            "{inner_indent}root = root.with_child({fn_name}())"
                        ));
                    }
13
                    _ => {
13
                        lines.push(alloc::format!("{inner_indent}# field '{fname}'"));
13
                    }
                }
            }
1
            lines.push(alloc::format!("{indent_str}return root"));
1
            Ok(lines.join("\n").into())
        }
    };
9
    r.into()
9
}
/// Create a `ComponentDef` for a builtin HTML element.
///
/// # Arguments
/// * `tag` - HTML tag name (e.g. "button", "div")
/// * `display_name` - Human-readable name (e.g. "Button", "Div")
/// * `default_text` - Default text content for the preview, or `None` if the element has no text.
///   Pass `Some("Button text")` for `<button>`, `Some("")` for text elements like `<span>` that
///   accept text but have no meaningful default.
/// * `css` - Component-level CSS string. For most builtin elements this is `""` because
///   styling comes from `ua_css.rs` and the `SystemStyle`. Components that need extra
///   styling (e.g. a future high-level button widget) can pass CSS here.
313468
fn builtin_component_def(
313468
    tag: &str,
313468
    display_name: &str,
313468
    default_text: Option<&str>,
313468
    css: &str,
313468
) -> ComponentDef {
313468
    let mut fields = builtin_data_model(tag);
    // If a default_text is provided, this element accepts text content
313468
    if let Some(text) = default_text {
163671
        fields.push(data_field(
163671
            "text",
163671
            ComponentFieldType::String,
163671
            Some(ComponentDefaultValue::String(AzString::from(text))),
163671
            "Text content of the element",
163671
        ));
163671
    }
313468
    let model_name = format!("{display_name}Data");
313468
    ComponentDef {
313468
        id: ComponentId::builtin(tag),
313468
        display_name: AzString::from(display_name),
313468
        description: AzString::from(format!("HTML <{tag}> element").as_str()),
313468
        css: AzString::from(css),
313468
        source: ComponentSource::Builtin,
313468
        data_model: ComponentDataModel {
313468
            name: AzString::from(model_name.as_str()),
313468
            description: AzString::from(format!("Data model for <{tag}>").as_str()),
313468
            fields: fields.into(),
313468
        },
313468
        render_fn: builtin_render_fn,
313468
        compile_fn: builtin_compile_fn,
313468
        render_fn_source: None.into(),
313468
        compile_fn_source: None.into(),
313468
    }
313468
}
/// Helper to create a `ComponentDataField` with a rich type
516053
fn data_field(
516053
    name: &str,
516053
    ft: ComponentFieldType,
516053
    default: Option<ComponentDefaultValue>,
516053
    description: &str,
516053
) -> ComponentDataField {
516053
    let required = default.is_none();
    ComponentDataField {
516053
        name: AzString::from(name),
516053
        field_type: ft,
516053
        default_value: default.map_or_else(|| OptionComponentDefaultValue::None, OptionComponentDefaultValue::Some),
516053
        required,
516053
        description: AzString::from(description),
    }
516053
}
/// Returns the tag-specific data model fields for builtin HTML elements.
/// These are the component's "main data model" — the attributes that define
/// what the component needs as configuration (e.g., `href` for `<a>`,
/// `src` for `<img>`). Universal HTML attributes (id, class, style, etc.)
/// are NOT included here — they are added separately by the debug server.
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
313475
fn builtin_data_model(tag: &str) -> Vec<ComponentDataField> {
    use ComponentDefaultValue as D;
    use ComponentFieldType::{String, Bool, I32};
313475
    match tag {
313475
        "a" => alloc::vec![
2778
            data_field(
2778
                "href",
2778
                String,
2778
                Some(D::String(AzString::from_const_str(""))),
2778
                "URL the link points to"
            ),
2778
            data_field(
2778
                "target",
2778
                String,
2778
                Some(D::String(AzString::from_const_str(""))),
2778
                "Where to open the linked document (_blank, _self, _parent, _top)"
            ),
2778
            data_field(
2778
                "rel",
2778
                String,
2778
                Some(D::String(AzString::from_const_str(""))),
2778
                "Relationship between current and linked document"
            ),
        ],
310697
        "img" | "image" => alloc::vec![
2
            data_field("src", String, None, "URL of the image"),
2
            data_field(
2
                "alt",
2
                String,
2
                Some(D::String(AzString::from_const_str(""))),
2
                "Alternative text for the image"
            ),
2
            data_field(
2
                "width",
2
                String,
2
                Some(D::String(AzString::from_const_str(""))),
2
                "Width of the image"
            ),
2
            data_field(
2
                "height",
2
                String,
2
                Some(D::String(AzString::from_const_str(""))),
2
                "Height of the image"
            ),
        ],
310695
        "form" => alloc::vec![
2774
            data_field(
2774
                "action",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "URL where form data is submitted"
            ),
2774
            data_field(
2774
                "method",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("GET"))),
2774
                "HTTP method for form submission (GET or POST)"
            ),
        ],
307921
        "label" => alloc::vec![data_field(
2774
            "for",
2774
            String,
2774
            Some(D::String(AzString::from_const_str(""))),
2774
            "ID of the form element this label is for"
        ),],
305147
        "button" => alloc::vec![
2774
            data_field(
2774
                "type",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("button"))),
2774
                "Button type (button, submit, reset)"
            ),
2774
            data_field(
2774
                "disabled",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether the button is disabled"
            ),
        ],
302373
        "td" | "th" => alloc::vec![
5548
            data_field(
5548
                "colspan",
5548
                I32,
5548
                Some(D::I32(1)),
5548
                "Number of columns the cell spans"
            ),
5548
            data_field(
5548
                "rowspan",
5548
                I32,
5548
                Some(D::I32(1)),
5548
                "Number of rows the cell spans"
            ),
        ],
296825
        "icon" => alloc::vec![data_field(
2774
            "name",
2774
            String,
2774
            Some(D::String(AzString::from_const_str(""))),
2774
            "Icon name"
        ),],
294051
        "ol" => alloc::vec![
2774
            data_field(
2774
                "start",
2774
                I32,
2774
                Some(D::I32(1)),
2774
                "Start value for the ordered list"
            ),
2774
            data_field(
2774
                "type",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("1"))),
2774
                "Numbering type (1, A, a, I, i)"
            ),
        ],
        // Form controls
291277
        "input" => alloc::vec![
2774
            data_field(
2774
                "type",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("text"))),
2774
                "Input type (text, password, email, number, checkbox, radio, etc.)"
            ),
2774
            data_field(
2774
                "name",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Name of the input for form submission"
            ),
2774
            data_field(
2774
                "value",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Current value of the input"
            ),
2774
            data_field(
2774
                "placeholder",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Placeholder text"
            ),
2774
            data_field(
2774
                "disabled",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether the input is disabled"
            ),
2774
            data_field(
2774
                "required",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether the input is required"
            ),
2774
            data_field(
2774
                "readonly",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether the input is read-only"
            ),
2774
            data_field(
2774
                "checked",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether the checkbox/radio is checked"
            ),
2774
            data_field(
2774
                "min",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Minimum value (for number, range, date)"
            ),
2774
            data_field(
2774
                "max",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Maximum value (for number, range, date)"
            ),
2774
            data_field(
2774
                "step",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Step increment (for number, range)"
            ),
2774
            data_field(
2774
                "pattern",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Regex pattern for validation"
            ),
2774
            data_field(
2774
                "maxlength",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Maximum number of characters"
            ),
        ],
288503
        "select" => alloc::vec![
2774
            data_field(
2774
                "name",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Name for form submission"
            ),
2774
            data_field(
2774
                "multiple",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether multiple options can be selected"
            ),
2774
            data_field(
2774
                "disabled",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether the select is disabled"
            ),
2774
            data_field(
2774
                "required",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether selection is required"
            ),
2774
            data_field(
2774
                "size",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Number of visible options"
            ),
        ],
285729
        "option" => alloc::vec![
2774
            data_field(
2774
                "value",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Value submitted with the form"
            ),
2774
            data_field(
2774
                "selected",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether this option is selected"
            ),
2774
            data_field(
2774
                "disabled",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether this option is disabled"
            ),
        ],
282955
        "optgroup" => alloc::vec![
2774
            data_field(
2774
                "label",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Label for the option group"
            ),
2774
            data_field(
2774
                "disabled",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether the group is disabled"
            ),
        ],
280181
        "textarea" => alloc::vec![
2774
            data_field(
2774
                "name",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Name for form submission"
            ),
2774
            data_field(
2774
                "placeholder",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Placeholder text"
            ),
2774
            data_field("rows", I32, Some(D::I32(2)), "Number of visible text lines"),
2774
            data_field(
2774
                "cols",
2774
                I32,
2774
                Some(D::I32(20)),
2774
                "Visible width in average character widths"
            ),
2774
            data_field(
2774
                "disabled",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether the textarea is disabled"
            ),
2774
            data_field(
2774
                "required",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether content is required"
            ),
2774
            data_field(
2774
                "readonly",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether the textarea is read-only"
            ),
2774
            data_field(
2774
                "maxlength",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Maximum number of characters"
            ),
        ],
277407
        "fieldset" => alloc::vec![data_field(
2774
            "disabled",
2774
            Bool,
2774
            Some(D::Bool(false)),
2774
            "Whether all controls in the fieldset are disabled"
        ),],
274633
        "output" => alloc::vec![
2774
            data_field(
2774
                "for",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "IDs of elements that contributed to the output"
            ),
2774
            data_field(
2774
                "name",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Name for form submission"
            ),
        ],
271859
        "progress" => alloc::vec![
2774
            data_field(
2774
                "value",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Current progress value"
            ),
2774
            data_field(
2774
                "max",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("1"))),
2774
                "Maximum value"
            ),
        ],
269085
        "meter" => alloc::vec![
2774
            data_field(
2774
                "value",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Current value"
            ),
2774
            data_field(
2774
                "min",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("0"))),
2774
                "Minimum value"
            ),
2774
            data_field(
2774
                "max",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("1"))),
2774
                "Maximum value"
            ),
2774
            data_field(
2774
                "low",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Low threshold"
            ),
2774
            data_field(
2774
                "high",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "High threshold"
            ),
2774
            data_field(
2774
                "optimum",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Optimum value"
            ),
        ],
        // Interactive
266311
        "details" => alloc::vec![data_field(
2774
            "open",
2774
            Bool,
2774
            Some(D::Bool(false)),
2774
            "Whether the details are visible"
        ),],
263537
        "dialog" => alloc::vec![data_field(
2774
            "open",
2774
            Bool,
2774
            Some(D::Bool(false)),
2774
            "Whether the dialog is active and can be interacted with"
        ),],
        // Embedded content
260763
        "audio" | "video" => alloc::vec![
5548
            data_field(
5548
                "src",
5548
                String,
5548
                Some(D::String(AzString::from_const_str(""))),
5548
                "URL of the media resource"
            ),
5548
            data_field(
5548
                "controls",
5548
                Bool,
5548
                Some(D::Bool(false)),
5548
                "Whether to show playback controls"
            ),
5548
            data_field(
5548
                "autoplay",
5548
                Bool,
5548
                Some(D::Bool(false)),
5548
                "Whether to start playing automatically"
            ),
5548
            data_field(
5548
                "loop",
5548
                Bool,
5548
                Some(D::Bool(false)),
5548
                "Whether to loop playback"
            ),
5548
            data_field(
5548
                "muted",
5548
                Bool,
5548
                Some(D::Bool(false)),
5548
                "Whether audio is muted"
            ),
5548
            data_field(
5548
                "preload",
5548
                String,
5548
                Some(D::String(AzString::from_const_str("auto"))),
5548
                "Preload hint (none, metadata, auto)"
            ),
        ],
255215
        "source" => alloc::vec![
2774
            data_field("src", String, None, "URL of the media resource"),
2774
            data_field(
2774
                "type",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "MIME type of the resource"
            ),
        ],
252441
        "track" => alloc::vec![
2774
            data_field("src", String, None, "URL of the track file"),
2774
            data_field(
2774
                "kind",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("subtitles"))),
2774
                "Kind of text track (subtitles, captions, descriptions, chapters, metadata)"
            ),
2774
            data_field(
2774
                "srclang",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Language of the track text"
            ),
2774
            data_field(
2774
                "label",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "User-readable title for the track"
            ),
2774
            data_field(
2774
                "default",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Whether this is the default track"
            ),
        ],
249667
        "canvas" => alloc::vec![
2774
            data_field(
2774
                "width",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("300"))),
2774
                "Width of the canvas in pixels"
            ),
2774
            data_field(
2774
                "height",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("150"))),
2774
                "Height of the canvas in pixels"
            ),
        ],
246893
        "embed" => alloc::vec![
2774
            data_field("src", String, None, "URL of the resource to embed"),
2774
            data_field(
2774
                "type",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "MIME type of the embedded content"
            ),
2774
            data_field(
2774
                "width",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Width"
            ),
2774
            data_field(
2774
                "height",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Height"
            ),
        ],
244119
        "object" => alloc::vec![
2774
            data_field(
2774
                "data",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "URL of the resource"
            ),
2774
            data_field(
2774
                "type",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "MIME type of the resource"
            ),
2774
            data_field(
2774
                "width",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Width"
            ),
2774
            data_field(
2774
                "height",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Height"
            ),
        ],
241345
        "param" => alloc::vec![
2774
            data_field("name", String, None, "Name of the parameter"),
2774
            data_field(
2774
                "value",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Value of the parameter"
            ),
        ],
238571
        "area" => alloc::vec![
2774
            data_field(
2774
                "shape",
2774
                String,
2774
                Some(D::String(AzString::from_const_str("default"))),
2774
                "Shape of the area (default, rect, circle, poly)"
            ),
2774
            data_field(
2774
                "coords",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Coordinates of the area"
            ),
2774
            data_field(
2774
                "href",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "URL for the area link"
            ),
2774
            data_field(
2774
                "alt",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Alternative text"
            ),
2774
            data_field(
2774
                "target",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Where to open the linked document"
            ),
        ],
235797
        "map" => alloc::vec![data_field(
2774
            "name",
2774
            String,
2774
            None,
2774
            "Name of the image map (referenced by usemap)"
        ),],
        // Inline semantics with special attributes
233023
        "time" => alloc::vec![data_field(
2774
            "datetime",
2774
            String,
2774
            Some(D::String(AzString::from_const_str(""))),
2774
            "Machine-readable date/time value"
        ),],
230249
        "data" => alloc::vec![data_field(
2774
            "value",
2774
            String,
2774
            Some(D::String(AzString::from_const_str(""))),
2774
            "Machine-readable value"
        ),],
227475
        "abbr" | "acronym" | "dfn" => alloc::vec![data_field(
8322
            "title",
8322
            String,
8322
            Some(D::String(AzString::from_const_str(""))),
8322
            "Full expansion or definition"
        ),],
219153
        "q" | "blockquote" => alloc::vec![data_field(
5548
            "cite",
5548
            String,
5548
            Some(D::String(AzString::from_const_str(""))),
5548
            "URL of the source of the quotation"
        ),],
213605
        "del" | "ins" => alloc::vec![
5548
            data_field(
5548
                "cite",
5548
                String,
5548
                Some(D::String(AzString::from_const_str(""))),
5548
                "URL explaining the change"
            ),
5548
            data_field(
5548
                "datetime",
5548
                String,
5548
                Some(D::String(AzString::from_const_str(""))),
5548
                "Date/time of the change"
            ),
        ],
208057
        "bdo" => alloc::vec![data_field(
2774
            "dir",
2774
            String,
2774
            Some(D::String(AzString::from_const_str("ltr"))),
2774
            "Text direction (ltr, rtl)"
        ),],
205283
        "col" | "colgroup" => alloc::vec![data_field(
5548
            "span",
5548
            I32,
5548
            Some(D::I32(1)),
5548
            "Number of columns the element spans"
        ),],
        // Metadata
199735
        "meta" => alloc::vec![
2774
            data_field(
2774
                "name",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Metadata name"
            ),
2774
            data_field(
2774
                "content",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Metadata value"
            ),
2774
            data_field(
2774
                "charset",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Character encoding"
            ),
2774
            data_field(
2774
                "http-equiv",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "HTTP header equivalent"
            ),
        ],
196961
        "link" => alloc::vec![
2774
            data_field("rel", String, None, "Relationship type"),
2774
            data_field(
2774
                "href",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "URL of the linked resource"
            ),
2774
            data_field(
2774
                "type",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "MIME type of the linked resource"
            ),
        ],
194187
        "script" => alloc::vec![
2774
            data_field(
2774
                "src",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "URL of external script"
            ),
2774
            data_field(
2774
                "type",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "MIME type or module"
            ),
2774
            data_field(
2774
                "async",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Execute asynchronously"
            ),
2774
            data_field(
2774
                "defer",
2774
                Bool,
2774
                Some(D::Bool(false)),
2774
                "Defer execution until page load"
            ),
        ],
191413
        "style" => alloc::vec![data_field(
2774
            "type",
2774
            String,
2774
            Some(D::String(AzString::from_const_str("text/css"))),
2774
            "MIME type of the style sheet"
        ),],
188639
        "base" => alloc::vec![
2774
            data_field(
2774
                "href",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Base URL for relative URLs"
            ),
2774
            data_field(
2774
                "target",
2774
                String,
2774
                Some(D::String(AzString::from_const_str(""))),
2774
                "Default target for hyperlinks"
            ),
        ],
185865
        _ => alloc::vec![],
    }
313475
}
impl Default for ComponentMap {
    /// Returns an empty `ComponentMap` with no libraries.
    ///
    /// Use `AppConfig::create()` (which registers the 52 builtins via
    /// `register_builtin_components`) followed by `ComponentMap::from_libraries()`
    /// to get a fully-populated map.
71
    fn default() -> Self {
71
        Self {
71
            libraries: ComponentLibraryVec::from_const_slice(&[]),
71
        }
71
    }
}
impl ComponentMap {
5
    #[must_use] pub fn create() -> Self {
5
        Self::default()
5
    }
    /// Create a `ComponentMap` with the 52 built-in HTML element components pre-registered.
2740
    #[must_use] pub fn with_builtin() -> Self {
2740
        Self {
2740
            libraries: alloc::vec![register_builtin_components()].into(),
2740
        }
2740
    }
    /// Build a `ComponentMap` from the libraries stored in an `AppConfig`.
    ///
    /// The `component_libraries` field already contains builtins (registered in
    /// `AppConfig::create()`) plus any user-added libraries.  No merging needed —
    /// `add_component_library` / `add_component` handle insertion at registration time.
2
    #[must_use] pub fn from_libraries(libs: &ComponentLibraryVec) -> Self {
2
        Self {
2
            libraries: libs.clone(),
2
        }
2
    }
}
/// Convert XML attributes to a `ComponentDataModel` by cloning the component's
/// base data model and overriding field defaults with values from the XML attributes.
///
/// This is the bridge between the XML parsing layer (key-value string pairs)
/// and the typed component data model. For each field in the base model,
/// if a matching XML attribute exists, its string value is set as the new default.
///
/// # Arguments
/// * `base_model` - The component's data model template (from `ComponentDef::data_model`)
/// * `xml_attributes` - The XML node's attribute map
/// * `text_content` - Optional text content from child text nodes
///
/// # Returns
/// A cloned `ComponentDataModel` with overridden defaults
4
fn xml_attrs_to_data_model(
4
    base_model: &ComponentDataModel,
4
    xml_attributes: &XmlAttributeMap,
4
    text_content: Option<&str>,
4
) -> ComponentDataModel {
4
    let mut model = base_model.clone();
    // Override defaults from XML attributes
4
    let mut fields_vec = core::mem::replace(
4
        &mut model.fields,
4
        ComponentDataFieldVec::from_const_slice(&[]),
    )
4
    .into_library_owned_vec();
20
    for field in &mut fields_vec {
16
        if let Some(attr_value) = xml_attributes.get_key(field.name.as_str()) {
1
            // Override the default_value with the XML attribute's string value
1
            field.default_value = OptionComponentDefaultValue::Some(ComponentDefaultValue::String(
1
                attr_value.clone(),
1
            ));
15
        }
    }
4
    model.fields = ComponentDataFieldVec::from_vec(fields_vec);
    // Handle text content — set the "text" field if present
4
    if let Some(text) = text_content {
2
        let prepared = prepare_string(text);
2
        if !prepared.is_empty() {
1
            model = model.with_default(
1
                "text",
1
                ComponentDefaultValue::String(AzString::from(prepared.as_str())),
1
            );
1
        }
2
    }
4
    model
4
}
// ============================================================================
// Structural builtin components: if, for, map
// ============================================================================
/// `builtin:if` — conditional rendering.
/// Takes `condition: Bool`, `then: StyledDom`, and optionally `else: StyledDom`.
2777
fn builtin_if_component() -> ComponentDef {
2777
    ComponentDef {
2777
        id: ComponentId::builtin("if"),
2777
        display_name: AzString::from_const_str("If"),
2777
        description: AzString::from_const_str("Conditional rendering: shows 'then' if condition is true, else shows 'else' (if provided)."),
2777
        css: AzString::from_const_str(""),
2777
        source: ComponentSource::Builtin,
2777
        data_model: ComponentDataModel {
2777
            name: AzString::from_const_str("IfData"),
2777
            description: AzString::from_const_str("Data for conditional rendering"),
2777
            fields: alloc::vec![
2777
                data_field("condition", ComponentFieldType::Bool, Some(ComponentDefaultValue::Bool(false)), "The boolean condition to evaluate"),
2777
            ].into(),
2777
        },
2777
        render_fn: builtin_if_render_fn,
2777
        compile_fn: builtin_if_compile_fn,
2777
        render_fn_source: None.into(),
2777
        compile_fn_source: None.into(),
2777
    }
2777
}
2
fn builtin_if_render_fn(
2
    _comp: &ComponentDef,
2
    data_model: &ComponentDataModel,
2
    _component_map: &ComponentMap,
2
) -> ResultStyledDomRenderDomError {
    // Evaluate the condition field
2
    let condition = data_model
2
        .fields
2
        .iter()
2
        .find(|f| f.name.as_str() == "condition")
2
        .and_then(|f| match &f.default_value {
1
            OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => Some(*b),
            _ => None,
1
        })
2
        .unwrap_or(false);
2
    let label = if condition {
1
        "if: true (then branch)"
    } else {
1
        "if: false (else branch)"
    };
2
    let mut dom =
2
        Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(label)].into());
2
    let css = Css::empty();
2
    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
2
}
8
fn builtin_if_compile_fn(
8
    _comp: &ComponentDef,
8
    target: &CompileTarget,
8
    _data: &ComponentDataModel,
8
    _indent: usize,
8
) -> ResultStringCompileError {
8
    match target {
2
        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
2
            "if data.condition {\n    // then branch\n    Dom::create_div()\n} else {\n    // else branch\n    Dom::create_div()\n}"
2
        )),
2
        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
2
            "if (data.condition) {\n    // then branch\n    AzDom_createDiv();\n} else {\n    // else branch\n    AzDom_createDiv();\n}"
2
        )),
2
        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
2
            "if (data.condition) {\n    // then branch\n    Dom::create_div();\n} else {\n    // else branch\n    Dom::create_div();\n}"
2
        )),
2
        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
2
            "if data.condition:\n    # then branch\n    Dom.create_div()\nelse:\n    # else branch\n    Dom.create_div()"
2
        )),
    }
8
}
/// `builtin:for` — iterative rendering.
/// Takes `count: U32` (number of iterations), renders children N times.
2777
fn builtin_for_component() -> ComponentDef {
2777
    ComponentDef {
2777
        id: ComponentId::builtin("for"),
2777
        display_name: AzString::from_const_str("For Loop"),
2777
        description: AzString::from_const_str(
2777
            "Iterative rendering: repeats children 'count' times.",
2777
        ),
2777
        css: AzString::from_const_str(""),
2777
        source: ComponentSource::Builtin,
2777
        data_model: ComponentDataModel {
2777
            name: AzString::from_const_str("ForData"),
2777
            description: AzString::from_const_str("Data for iterative rendering"),
2777
            fields: alloc::vec![data_field(
2777
                "count",
2777
                ComponentFieldType::U32,
2777
                Some(ComponentDefaultValue::U32(3)),
2777
                "Number of iterations"
2777
            ),]
2777
            .into(),
2777
        },
2777
        render_fn: builtin_for_render_fn,
2777
        compile_fn: builtin_for_compile_fn,
2777
        render_fn_source: None.into(),
2777
        compile_fn_source: None.into(),
2777
    }
2777
}
2
fn builtin_for_render_fn(
2
    _comp: &ComponentDef,
2
    data_model: &ComponentDataModel,
2
    _component_map: &ComponentMap,
2
) -> ResultStyledDomRenderDomError {
2
    let count = data_model
2
        .fields
2
        .iter()
2
        .find(|f| f.name.as_str() == "count")
2
        .and_then(|f| match &f.default_value {
1
            OptionComponentDefaultValue::Some(ComponentDefaultValue::U32(n)) => Some(*n),
1
            _ => None,
2
        })
2
        .unwrap_or(3);
2
    let mut items: Vec<Dom> = Vec::new();
3
    for i in 0..count {
3
        items.push(
3
            Dom::create_node(NodeType::Div)
3
                .with_children(alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(alloc::format!("Item {i}"))].into()),
3
        );
3
    }
2
    let mut dom = Dom::create_node(NodeType::Div).with_children(items.into());
2
    let css = Css::empty();
2
    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
2
}
8
fn builtin_for_compile_fn(
8
    _comp: &ComponentDef,
8
    target: &CompileTarget,
8
    _data: &ComponentDataModel,
8
    _indent: usize,
8
) -> ResultStringCompileError {
8
    match target {
2
        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
2
            "let mut children = Vec::new();\nfor i in 0..data.count {\n    children.push(Dom::create_div());\n}\nDom::create_div().with_children(children)"
2
        )),
2
        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
2
            "AzDom container = AzDom_createDiv();\nfor (uint32_t i = 0; i < data.count; i++) {\n    AzDom_addChild(&container, AzDom_createDiv());\n}"
2
        )),
2
        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
2
            "auto container = Dom::create_div();\nfor (uint32_t i = 0; i < data.count; i++) {\n    container.add_child(Dom::create_div());\n}"
2
        )),
2
        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
2
            "container = Dom.create_div()\nfor i in range(data.count):\n    container = container.with_child(Dom.create_div())"
2
        )),
    }
8
}
/// `builtin:map` — map data to DOM.
/// Takes `data_json: String` (JSON array) + maps each element.
2777
fn builtin_map_component() -> ComponentDef {
2777
    ComponentDef {
2777
        id: ComponentId::builtin("map"),
2777
        display_name: AzString::from_const_str("Map"),
2777
        description: AzString::from_const_str(
2777
            "Map data to DOM: applies a template to each item in a collection.",
2777
        ),
2777
        css: AzString::from_const_str(""),
2777
        source: ComponentSource::Builtin,
2777
        data_model: ComponentDataModel {
2777
            name: AzString::from_const_str("MapData"),
2777
            description: AzString::from_const_str("Data for map rendering"),
2777
            fields: alloc::vec![data_field(
2777
                "data_json",
2777
                ComponentFieldType::String,
2777
                Some(ComponentDefaultValue::String(AzString::from_const_str(
2777
                    "[]"
2777
                ))),
2777
                "JSON array of items to map over"
2777
            ),]
2777
            .into(),
2777
        },
2777
        render_fn: builtin_map_render_fn,
2777
        compile_fn: builtin_map_compile_fn,
2777
        render_fn_source: None.into(),
2777
        compile_fn_source: None.into(),
2777
    }
2777
}
2
fn builtin_map_render_fn(
2
    _comp: &ComponentDef,
2
    data_model: &ComponentDataModel,
2
    _component_map: &ComponentMap,
2
) -> ResultStyledDomRenderDomError {
    // For now, render a placeholder — actual mapping requires callback support
2
    let data_str = data_model
2
        .fields
2
        .iter()
2
        .find(|f| f.name.as_str() == "data_json")
2
        .and_then(|f| match &f.default_value {
1
            OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
1
                Some(s.as_str().to_string())
            }
            _ => None,
1
        })
2
        .unwrap_or_else(|| "[]".to_string());
2
    let label = alloc::format!("map: data_json={data_str}");
2
    let mut dom =
2
        Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(label)].into());
2
    let css = Css::empty();
2
    ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
2
}
8
fn builtin_map_compile_fn(
8
    _comp: &ComponentDef,
8
    target: &CompileTarget,
8
    _data: &ComponentDataModel,
8
    _indent: usize,
8
) -> ResultStringCompileError {
8
    match target {
2
        CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
2
            "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)"
2
        )),
2
        CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
2
            "// Parse data.data_json and map each item\nAzDom container = AzDom_createDiv();\n// TODO: iterate parsed JSON array"
2
        )),
2
        CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
2
            "// Parse data.data_json and map each item\nauto container = Dom::create_div();\n// TODO: iterate parsed JSON array"
2
        )),
2
        CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
2
            "import json\nitems = json.loads(data.data_json)\ncontainer = Dom.create_div()\nfor item in items:\n    container = container.with_child(Dom.create_div())"
2
        )),
    }
8
}
/// Register the 52 built-in HTML element components.
///
/// This is an `extern "C"` function pointer compatible with
/// `RegisterComponentLibraryFnType`, so it can be passed directly to
/// `AppConfig::add_component_library()`.
///
/// Called once during `AppConfig::create()` — the framework dogfoods
/// its own component registration system for builtins.
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
2774
#[must_use] pub extern "C" fn register_builtin_components() -> ComponentLibrary {
2774
    ComponentLibrary {
2774
        name: AzString::from_const_str("builtin"),
2774
        version: AzString::from_const_str("1.0.0"),
2774
        description: AzString::from_const_str("Built-in HTML elements"),
2774
        exportable: false,
2774
        modifiable: false,
2774
        data_models: Vec::new().into(),
2774
        enum_models: Vec::new().into(),
2774
        components: alloc::vec![
2774
            // Structural
2774
            builtin_component_def("html", "HTML", None, ""),
2774
            builtin_component_def("head", "Head", None, ""),
2774
            builtin_component_def("title", "Title", Some(""), ""),
2774
            builtin_component_def("body", "Body", None, ""),
2774
            // Block-level
2774
            builtin_component_def("div", "Div", None, ""),
2774
            builtin_component_def("header", "Header", None, ""),
2774
            builtin_component_def("footer", "Footer", None, ""),
2774
            builtin_component_def("section", "Section", None, ""),
2774
            builtin_component_def("article", "Article", None, ""),
2774
            builtin_component_def("aside", "Aside", None, ""),
2774
            builtin_component_def("nav", "Nav", None, ""),
2774
            builtin_component_def("main", "Main", None, ""),
2774
            builtin_component_def("figure", "Figure", None, ""),
2774
            builtin_component_def("figcaption", "Figure Caption", Some(""), ""),
2774
            builtin_component_def("address", "Address", Some(""), ""),
2774
            builtin_component_def("details", "Details", None, ""),
2774
            builtin_component_def("summary", "Summary", Some("Details"), ""),
2774
            builtin_component_def("dialog", "Dialog", None, ""),
2774
            // Headings — default text is the heading level name so preview is visible
2774
            builtin_component_def("h1", "Heading 1", Some("Heading 1"), ""),
2774
            builtin_component_def("h2", "Heading 2", Some("Heading 2"), ""),
2774
            builtin_component_def("h3", "Heading 3", Some("Heading 3"), ""),
2774
            builtin_component_def("h4", "Heading 4", Some("Heading 4"), ""),
2774
            builtin_component_def("h5", "Heading 5", Some("Heading 5"), ""),
2774
            builtin_component_def("h6", "Heading 6", Some("Heading 6"), ""),
2774
            // Text content
2774
            builtin_component_def("p", "Paragraph", Some("Paragraph text"), ""),
2774
            builtin_component_def("span", "Span", Some(""), ""),
2774
            builtin_component_def("pre", "Preformatted", Some(""), ""),
2774
            builtin_component_def("code", "Code", Some(""), ""),
2774
            builtin_component_def("blockquote", "Blockquote", Some(""), ""),
2774
            builtin_component_def("br", "Line Break", None, ""),
2774
            builtin_component_def("hr", "Horizontal Rule", None, ""),
2774
            builtin_component_def("pagebreak", "Page Break", None, ""),
2774
            builtin_component_def("icon", "Icon", Some(""), ""),
2774
            // Lists
2774
            builtin_component_def("ul", "Unordered List", None, ""),
2774
            builtin_component_def("ol", "Ordered List", None, ""),
2774
            builtin_component_def("li", "List Item", Some("List item"), ""),
2774
            builtin_component_def("dl", "Description List", None, ""),
2774
            builtin_component_def("dt", "Description Term", Some(""), ""),
2774
            builtin_component_def("dd", "Description Details", Some(""), ""),
2774
            builtin_component_def("menu", "Menu", None, ""),
2774
            builtin_component_def("menuitem", "Menu Item", Some(""), ""),
2774
            builtin_component_def("dir", "Directory List", None, ""),
2774
            // Tables
2774
            builtin_component_def("table", "Table", None, ""),
2774
            builtin_component_def("caption", "Table Caption", Some(""), ""),
2774
            builtin_component_def("thead", "Table Head", None, ""),
2774
            builtin_component_def("tbody", "Table Body", None, ""),
2774
            builtin_component_def("tfoot", "Table Foot", None, ""),
2774
            builtin_component_def("tr", "Table Row", None, ""),
2774
            builtin_component_def("th", "Table Header Cell", Some("Header"), ""),
2774
            builtin_component_def("td", "Table Data Cell", Some(""), ""),
2774
            builtin_component_def("colgroup", "Column Group", None, ""),
2774
            builtin_component_def("col", "Column", None, ""),
2774
            // Inline
2774
            builtin_component_def("a", "Link", Some("Link text"), ""),
2774
            builtin_component_def("strong", "Strong", Some(""), ""),
2774
            builtin_component_def("em", "Emphasis", Some(""), ""),
2774
            builtin_component_def("b", "Bold", Some(""), ""),
2774
            builtin_component_def("i", "Italic", Some(""), ""),
2774
            builtin_component_def("u", "Underline", Some(""), ""),
2774
            builtin_component_def("s", "Strikethrough", Some(""), ""),
2774
            builtin_component_def("small", "Small", Some(""), ""),
2774
            builtin_component_def("mark", "Mark", Some(""), ""),
2774
            builtin_component_def("del", "Deleted Text", Some(""), ""),
2774
            builtin_component_def("ins", "Inserted Text", Some(""), ""),
2774
            builtin_component_def("sub", "Subscript", Some(""), ""),
2774
            builtin_component_def("sup", "Superscript", Some(""), ""),
2774
            builtin_component_def("samp", "Sample Output", Some(""), ""),
2774
            builtin_component_def("kbd", "Keyboard Input", Some(""), ""),
2774
            builtin_component_def("var", "Variable", Some(""), ""),
2774
            builtin_component_def("cite", "Citation", Some(""), ""),
2774
            builtin_component_def("dfn", "Definition", Some(""), ""),
2774
            builtin_component_def("abbr", "Abbreviation", Some(""), ""),
2774
            builtin_component_def("acronym", "Acronym", Some(""), ""),
2774
            builtin_component_def("q", "Inline Quote", Some(""), ""),
2774
            builtin_component_def("time", "Time", Some(""), ""),
2774
            builtin_component_def("big", "Big", Some(""), ""),
2774
            builtin_component_def("bdo", "BiDi Override", Some(""), ""),
2774
            builtin_component_def("bdi", "BiDi Isolate", Some(""), ""),
2774
            builtin_component_def("wbr", "Word Break Opportunity", None, ""),
2774
            builtin_component_def("ruby", "Ruby Annotation", None, ""),
2774
            builtin_component_def("rt", "Ruby Text", Some(""), ""),
2774
            builtin_component_def("rtc", "Ruby Text Container", None, ""),
2774
            builtin_component_def("rp", "Ruby Parenthesis", Some(""), ""),
2774
            builtin_component_def("data", "Data", Some(""), ""),
2774
            // Forms
2774
            builtin_component_def("form", "Form", None, ""),
2774
            builtin_component_def("fieldset", "Field Set", None, ""),
2774
            builtin_component_def("legend", "Legend", Some("Legend"), ""),
2774
            builtin_component_def("label", "Label", Some("Label"), ""),
2774
            builtin_component_def("input", "Input", None, ""),
2774
            builtin_component_def("button", "Button", Some("Button text"), ""),
2774
            builtin_component_def("select", "Select", None, ""),
2774
            builtin_component_def("optgroup", "Option Group", None, ""),
2774
            builtin_component_def("option", "Option", Some(""), ""),
2774
            builtin_component_def("textarea", "Text Area", Some(""), ""),
2774
            builtin_component_def("output", "Output", Some(""), ""),
2774
            builtin_component_def("progress", "Progress", None, ""),
2774
            builtin_component_def("meter", "Meter", None, ""),
2774
            builtin_component_def("datalist", "Data List", None, ""),
2774
            // Embedded content
2774
            builtin_component_def("canvas", "Canvas", None, ""),
2774
            builtin_component_def("object", "Object", None, ""),
2774
            builtin_component_def("param", "Parameter", None, ""),
2774
            builtin_component_def("embed", "Embed", None, ""),
2774
            builtin_component_def("audio", "Audio", None, ""),
2774
            builtin_component_def("video", "Video", None, ""),
2774
            builtin_component_def("source", "Source", None, ""),
2774
            builtin_component_def("track", "Track", None, ""),
2774
            builtin_component_def("map", "Image Map", None, ""),
2774
            builtin_component_def("area", "Map Area", None, ""),
2774
            builtin_component_def("svg", "SVG", None, ""),
2774
            // Metadata
2774
            builtin_component_def("meta", "Meta", None, ""),
2774
            builtin_component_def("link", "Link (Resource)", None, ""),
2774
            builtin_component_def("script", "Script", Some(""), ""),
2774
            builtin_component_def("style", "Style", Some(""), ""),
2774
            builtin_component_def("base", "Base URL", None, ""),
2774
            // Structural control-flow builtins (F1-F3)
2774
            builtin_if_component(),
2774
            builtin_for_component(),
2774
            builtin_map_component(),
2774
        ]
2774
        .into(),
2774
    }
2774
}
// ============================================================================
// End new component system types
// ============================================================================
/// Wrapper for the XML parser - necessary to easily create a Dom from
/// XML without putting an XML solver into `azul-core`.
#[derive(Debug, Default)]
pub struct DomXml {
    pub parsed_dom: StyledDom,
}
impl DomXml {
    /// Convenience function, only available in tests, useful for quickly writing UI tests.
    /// Wraps the XML string in the required `<app></app>` braces, panics if the XML couldn't be
    /// parsed.
    ///
    /// ## Example
    ///
    /// ```rust,ignore
    /// # use azul::dom::Dom;
    /// # use azul::xml::DomXml;
    /// let dom = DomXml::mock("<div id='test' />");
    /// dom.assert_eq(Dom::create_div().with_id("test"));
    /// ```
    ///
    /// # Panics
    ///
    /// Panics if the rendered DOM does not equal `other` (this is a test-only
    /// assertion helper).
    #[cfg(test)]
    pub fn assert_eq(self, other: StyledDom) {
        let mut body = Dom::create_body();
        let mut fixed = StyledDom::create(&mut body, Css::empty());
        fixed.append_child(other);
        assert!(!(self.parsed_dom != fixed), 
                "\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: \
                 ----------\r\n{}\r\n",
                self.parsed_dom.get_html_string("", "", true),
                fixed.get_html_string("", "", true)
            );
    }
1
    #[must_use] pub fn into_styled_dom(self) -> StyledDom {
1
        self.into()
1
    }
}
impl From<DomXml> for StyledDom {
2
    fn from(val: DomXml) -> Self {
2
        val.parsed_dom
2
    }
}
/// Represents a child of an XML node - either an element or text
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum XmlNodeChild {
    /// A text node
    Text(AzString),
    /// An element node
    Element(XmlNode),
}
impl_option!(
    XmlNodeChild,
    OptionXmlNodeChild,
    copy = false,
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl XmlNodeChild {
    /// Get the text content if this is a text node
2444
    #[must_use] pub fn as_text(&self) -> Option<&str> {
2444
        match self {
2431
            Self::Text(s) => Some(s.as_str()),
13
            Self::Element(_) => None,
        }
2444
    }
    /// Get the element if this is an element node
3036
    #[must_use] pub const fn as_element(&self) -> Option<&XmlNode> {
3036
        match self {
2
            Self::Text(_) => None,
3034
            Self::Element(node) => Some(node),
        }
3036
    }
    /// Get the element mutably if this is an element node
2
    pub const fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
2
        match self {
1
            Self::Text(_) => None,
1
            Self::Element(node) => Some(node),
        }
2
    }
}
impl_vec!(
    XmlNodeChild,
    XmlNodeChildVec,
    XmlNodeChildVecDestructor,
    XmlNodeChildVecDestructorType,
    XmlNodeChildVecSlice,
    OptionXmlNodeChild
);
impl_vec_mut!(XmlNodeChild, XmlNodeChildVec);
impl_vec_debug!(XmlNodeChild, XmlNodeChildVec);
impl_vec_partialeq!(XmlNodeChild, XmlNodeChildVec);
impl_vec_eq!(XmlNodeChild, XmlNodeChildVec);
impl_vec_partialord!(XmlNodeChild, XmlNodeChildVec);
impl_vec_ord!(XmlNodeChild, XmlNodeChildVec);
impl_vec_hash!(XmlNodeChild, XmlNodeChildVec);
impl_vec_clone!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor);
/// Represents one XML node tag
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct XmlNode {
    /// Type of the node
    pub node_type: XmlTagName,
    /// Attributes of an XML node (note: not yet filtered and / or broken into function arguments!)
    pub attributes: XmlAttributeMap,
    /// Direct children of this node (can be text or element nodes)
    pub children: XmlNodeChildVec,
}
impl_option!(
    XmlNode,
    OptionXmlNode,
    copy = false,
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl XmlNode {
583
    pub fn create<I: Into<XmlTagName>>(node_type: I) -> Self {
583
        Self {
583
            node_type: node_type.into(),
583
            ..Default::default()
583
        }
583
    }
6096
    #[must_use] pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
6096
        Self {
6096
            children: v.into(),
6096
            ..self
6096
        }
6096
    }
    /// Get all text content concatenated from direct children
2324
    #[must_use] pub fn get_text_content(&self) -> String {
2324
        self.children
2324
            .as_ref()
2324
            .iter()
2325
            .filter_map(|child| child.as_text())
2324
            .collect::<Vec<_>>()
2324
            .join("")
2324
    }
    /// Check if this node has only text children (no element children)
29
    #[must_use] pub fn has_only_text_children(&self) -> bool {
29
        self.children
29
            .as_ref()
29
            .iter()
29
            .all(|child| matches!(child, XmlNodeChild::Text(_)))
29
    }
}
impl_vec!(
    XmlNode,
    XmlNodeVec,
    XmlNodeVecDestructor,
    XmlNodeVecDestructorType,
    XmlNodeVecSlice,
    OptionXmlNode
);
impl_vec_mut!(XmlNode, XmlNodeVec);
impl_vec_debug!(XmlNode, XmlNodeVec);
impl_vec_partialeq!(XmlNode, XmlNodeVec);
impl_vec_eq!(XmlNode, XmlNodeVec);
impl_vec_partialord!(XmlNode, XmlNodeVec);
impl_vec_ord!(XmlNode, XmlNodeVec);
impl_vec_hash!(XmlNode, XmlNodeVec);
impl_vec_clone!(XmlNode, XmlNodeVec, XmlNodeVecDestructor);
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum DomXmlParseError {
    /// No `<html></html>` node component present
    NoHtmlNode,
    /// Multiple `<html>` nodes
    MultipleHtmlRootNodes,
    /// No ´<body></body>´ node in the root HTML
    NoBodyInHtml,
    /// The DOM can only have one <body> node, not multiple.
    MultipleBodyNodes,
    /// Note: Sadly, the error type can only be a string because xmlparser
    /// returns all errors as strings. There is an open PR to fix
    /// this deficiency, but since the XML parsing is only needed for
    /// hot-reloading and compiling, it doesn't matter that much.
    Xml(XmlError),
    /// Invalid hierarchy close tags, i.e `<app></p></app>`
    MalformedHierarchy(MalformedHierarchyError),
    /// A component raised an error while rendering the DOM - holds the component name + error
    /// string
    RenderDom(RenderDomError),
    /// Something went wrong while parsing an XML component
    Component(ComponentParseError),
    /// Error parsing global CSS in head node
    Css(CssParseErrorOwned),
}
impl From<XmlError> for DomXmlParseError {
    fn from(e: XmlError) -> Self {
        Self::Xml(e)
    }
}
impl From<ComponentParseError> for DomXmlParseError {
    fn from(e: ComponentParseError) -> Self {
        Self::Component(e)
    }
}
impl From<RenderDomError> for DomXmlParseError {
1
    fn from(e: RenderDomError) -> Self {
1
        Self::RenderDom(e)
1
    }
}
impl From<CssParseErrorOwned> for DomXmlParseError {
    fn from(e: CssParseErrorOwned) -> Self {
        Self::Css(e)
    }
}
/// Error that can happen from the translation from XML code to Rust code -
/// stringified, since it is only used for printing and is not exposed in the public API
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum CompileError {
    Dom(RenderDomError),
    Xml(DomXmlParseError),
    Css(CssParseErrorOwned),
}
impl From<ComponentError> for CompileError {
    fn from(e: ComponentError) -> Self {
        Self::Dom(RenderDomError::Component(e))
    }
}
impl From<CssParseErrorOwned> for CompileError {
    fn from(e: CssParseErrorOwned) -> Self {
        Self::Css(e)
    }
}
impl fmt::Display for CompileError {
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::CompileError::{Dom, Xml, Css};
2
        match self {
1
            Dom(d) => write!(f, "{d}"),
1
            Xml(s) => write!(f, "{s}"),
            Css(s) => write!(f, "{}", s.to_shared()),
        }
2
    }
}
impl From<RenderDomError> for CompileError {
1
    fn from(e: RenderDomError) -> Self {
1
        Self::Dom(e)
1
    }
}
impl From<DomXmlParseError> for CompileError {
8
    fn from(e: DomXmlParseError) -> Self {
8
        Self::Xml(e)
8
    }
}
/// Wrapper for `UselessFunctionArgument` error data.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct UselessFunctionArgumentError {
    pub component_name: AzString,
    pub argument_name: AzString,
    pub valid_args: StringVec,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum ComponentError {
    /// While instantiating a component, a function argument
    /// was encountered that the component won't use or react to.
    UselessFunctionArgument(UselessFunctionArgumentError),
    /// A certain node type can't be rendered, because the
    /// renderer for this node is not available isn't available
    ///
    /// `UnknownComponent(component_name)`
    UnknownComponent(AzString),
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum RenderDomError {
    Component(ComponentError),
    /// Error parsing the CSS on the component style
    CssError(CssParseErrorOwned),
}
impl From<ComponentError> for RenderDomError {
1
    fn from(e: ComponentError) -> Self {
1
        Self::Component(e)
1
    }
}
impl From<CssParseErrorOwned> for RenderDomError {
    fn from(e: CssParseErrorOwned) -> Self {
        Self::CssError(e)
    }
}
/// Wrapper for `MissingType` error data.
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct MissingTypeError {
    pub arg_pos: usize,
    pub arg_name: AzString,
}
/// Wrapper for `WhiteSpaceInComponentName` error data.
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct WhiteSpaceInComponentNameError {
    pub arg_pos: usize,
    pub arg_name: AzString,
}
/// Wrapper for `WhiteSpaceInComponentType` error data.
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct WhiteSpaceInComponentTypeError {
    pub arg_pos: usize,
    pub arg_name: AzString,
    pub arg_type: AzString,
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum ComponentParseError {
    /// Given `XmlNode` is not a `<component />` node.
    NotAComponent,
    /// A `<component>` node does not have a `name` attribute.
    UnnamedComponent,
    /// Argument at position `usize` is either empty or has no name
    MissingName(usize),
    /// Argument at position `usize` with the name
    /// `String` doesn't have a `: type`
    MissingType(MissingTypeError),
    /// Component name may not contain a whitespace
    /// (probably missing a `:` between the name and the type)
    WhiteSpaceInComponentName(WhiteSpaceInComponentNameError),
    /// Component type may not contain a whitespace
    /// (probably missing a `,` between the type and the next name)
    WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError),
    /// Error parsing the <style> tag / CSS
    CssError(CssParseErrorOwned),
}
impl fmt::Display for DomXmlParseError {
142
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::DomXmlParseError::{NoHtmlNode, MultipleHtmlRootNodes, NoBodyInHtml, MultipleBodyNodes, Xml, MalformedHierarchy, RenderDom, Component, Css};
142
        match self {
122
            NoHtmlNode => write!(
122
                f,
122
                "No <html> node found as the root of the file - empty file?"
            ),
1
            MultipleHtmlRootNodes => write!(
1
                f,
1
                "Multiple <html> nodes found as the root of the file - only one root node allowed"
            ),
12
            NoBodyInHtml => write!(
12
                f,
12
                "No <body> node found as a direct child of an <html> node - malformed DOM \
12
                 hierarchy?"
            ),
1
            MultipleBodyNodes => write!(
1
                f,
1
                "Multiple <body> nodes present, only one <body> node is allowed"
            ),
1
            Xml(e) => write!(f, "Error parsing XML: {e}"),
1
            MalformedHierarchy(e) => write!(
1
                f,
1
                "Invalid </{}> tag: expected </{}>",
1
                e.got.as_str(),
1
                e.expected.as_str()
            ),
3
            RenderDom(e) => write!(f, "Error rendering DOM: {e}"),
1
            Component(c) => write!(f, "Error parsing component in <head> node:\r\n{c}"),
            Css(c) => write!(f, "Error parsing CSS in <head> node:\r\n{}", c.to_shared()),
        }
142
    }
}
impl fmt::Display for ComponentParseError {
7
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::ComponentParseError::{NotAComponent, UnnamedComponent, MissingName, MissingType, WhiteSpaceInComponentName, WhiteSpaceInComponentType, CssError};
7
        match self {
2
            NotAComponent => write!(f, "Expected <component/> node, found no such node"),
1
            UnnamedComponent => write!(
1
                f,
1
                "Found <component/> tag with out a \"name\" attribute, component must have a name"
            ),
1
            MissingName(arg_pos) => write!(
1
                f,
1
                "Argument at position {arg_pos} is either empty or has no name"
            ),
1
            MissingType(e) => write!(
1
                f,
1
                "Argument \"{}\" at position {} doesn't have a `: type`",
                e.arg_name, e.arg_pos
            ),
1
            WhiteSpaceInComponentName(e) => {
1
                write!(
1
                    f,
1
                    "Missing `:` between the name and the type in argument {} (around \"{}\")",
                    e.arg_pos, e.arg_name
                )
            }
1
            WhiteSpaceInComponentType(e) => {
1
                write!(
1
                    f,
1
                    "Missing `,` between two arguments (in argument {}, position {}, around \
1
                     \"{}\")",
                    e.arg_name, e.arg_pos, e.arg_type
                )
            }
            CssError(lsf) => write!(f, "Error parsing <style> tag: {}", lsf.to_shared()),
        }
7
    }
}
impl fmt::Display for ComponentError {
7
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::ComponentError::{UselessFunctionArgument, UnknownComponent};
7
        match self {
1
            UselessFunctionArgument(e) => {
1
                write!(
1
                    f,
1
                    "Useless component argument \"{}\": \"{}\" - available args are: {:#?}",
                    e.component_name, e.argument_name, e.valid_args
                )
            }
6
            UnknownComponent(name) => write!(f, "Unknown component: \"{name}\""),
        }
7
    }
}
impl fmt::Display for RenderDomError {
5
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::RenderDomError::{Component, CssError};
5
        match self {
5
            Component(c) => write!(f, "{c}"),
            CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
        }
5
    }
}
/// Find the one and only `<body>` node, return error if
/// there is no app node or there are multiple app nodes
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
/// # Errors
///
/// Returns an error if the document has no `<html>` root node.
2818
pub fn get_html_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
2818
    let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
2746
        if let XmlNodeChild::Element(node) = child {
            // HTML element names are case-insensitive (ASCII). NOT normalize_casing:
            // that inserts '_' before each uppercase letter for component-name
            // canonicalisation, so "HTML" would become "h_t_m_l" and never match.
2698
            if node.node_type.as_str().eq_ignore_ascii_case("html") {
2686
                Some(node)
            } else {
12
                None
            }
        } else {
48
            None
        }
2746
    });
2818
    let html_node = html_node_iterator
2818
        .next()
2818
        .ok_or(DomXmlParseError::NoHtmlNode)?;
2685
    if html_node_iterator.next().is_some() {
1
        Err(DomXmlParseError::MultipleHtmlRootNodes)
    } else {
2684
        Ok(html_node)
    }
2818
}
/// Find the one and only `<body>` node, return error if
/// there is no app node or there are multiple app nodes
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
/// # Errors
///
/// Returns an error if the document has no `<body>` node.
2689
pub fn get_body_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
1043
    fn find_body_recursive(nodes: &[XmlNodeChild], depth: usize) -> Option<&XmlNode> {
        // AUDIT 2026-07-08: bound recursion depth to avoid a stack overflow on
        // pathologically deep markup while hunting for the <body> element.
1043
        if depth > MAX_XML_NESTING_DEPTH {
1
            return None;
1042
        }
1556
        for child in nodes {
1028
            if let XmlNodeChild::Element(node) = child {
                // case-insensitive ASCII tag match; see get_html_node.
1028
                if node.node_type.as_str().eq_ignore_ascii_case("body") {
2
                    return Some(node);
1026
                }
                // Recurse into children
1026
                if let Some(found) = find_body_recursive(node.children.as_ref(), depth + 1) {
512
                    return Some(found);
514
                }
            }
        }
528
        None
1043
    }
    // First try to find body as a direct child (proper HTML structure)
6524
    let direct_body = root_nodes.iter().find_map(|child| {
6524
        if let XmlNodeChild::Element(node) = child {
            // case-insensitive ASCII tag match; see get_html_node.
5017
            if node.node_type.as_str().eq_ignore_ascii_case("body") {
2672
                Some(node)
            } else {
2345
                None
            }
        } else {
1507
            None
        }
6524
    });
2689
    if let Some(body) = direct_body {
2672
        return Ok(body);
17
    }
    // If not found as direct child, search recursively (for malformed HTML like example.com)
    // where <body> might be nested inside <head> due to missing </head> tag
17
    find_body_recursive(root_nodes, 0).ok_or(DomXmlParseError::NoBodyInHtml)
2689
}
/// Searches in the the `root_nodes` for a `node_type`, convenience function in order to
/// for example find the first <blah /> node in all these nodes.
/// This function searches recursively through the entire tree.
12028
fn find_node_by_type<'a>(root_nodes: &'a [XmlNodeChild], node_type: &str) -> Option<&'a XmlNode> {
    // First check direct children
19959
    for child in root_nodes {
12594
        if let XmlNodeChild::Element(node) = child {
            // case-insensitive ASCII tag match; see get_html_node.
11669
            if node.node_type.as_str().eq_ignore_ascii_case(node_type) {
4663
                return Some(node);
7006
            }
925
        }
    }
    // If not found, search recursively (for malformed HTML)
14732
    for child in root_nodes {
7369
        if let XmlNodeChild::Element(node) = child {
7005
            if let Some(found) = find_node_by_type(node.children.as_ref(), node_type) {
2
                return Some(found);
7003
            }
364
        }
    }
7363
    None
12028
}
11
#[must_use] pub fn find_attribute<'a>(node: &'a XmlNode, attribute: &str) -> Option<&'a AzString> {
11
    node.attributes
11
        .iter()
17
        .find(|n| normalize_casing(n.key.as_str()).as_str() == attribute)
11
        .map(|s| &s.value)
11
}
/// Normalizes input such as `abcDef`, `AbcDef`, `abc-def` to the normalized form of `abc_def`
39982
#[must_use] pub fn normalize_casing(input: &str) -> String {
39982
    let mut words: Vec<String> = Vec::new();
39982
    let mut cur_str = Vec::new();
183671
    for ch in input.chars() {
183671
        if ch.is_uppercase() || ch == '_' || ch == '-' {
50025
            if !cur_str.is_empty() {
50011
                words.push(cur_str.iter().collect());
50011
                cur_str.clear();
50011
            }
50025
            if ch.is_uppercase() {
50013
                cur_str.extend(ch.to_lowercase());
50013
            }
133646
        } else {
133646
            cur_str.extend(ch.to_lowercase());
133646
        }
    }
39982
    if !cur_str.is_empty() {
39978
        words.push(cur_str.iter().collect());
39978
        cur_str.clear();
39978
    }
39982
    words.join("_")
39982
}
/// Given a root node, traverses along the hierarchy, and returns a
/// mutable reference to the last child node of the root node
#[allow(trivial_casts)]
8
pub fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> {
8
    let mut hierarchy = hierarchy.to_vec();
8
    hierarchy.reverse();
8
    let Some(item) = hierarchy.pop() else {
1
        return Some(root_node);
    };
7
    let child = root_node.children.as_mut().get_mut(item)?;
5
    match child {
4
        XmlNodeChild::Element(node) => get_item_internal(&mut hierarchy, node),
1
        XmlNodeChild::Text(_) => None, // Can't traverse into text nodes
    }
8
}
404
fn get_item_internal<'a>(
404
    hierarchy: &mut Vec<usize>,
404
    root_node: &'a mut XmlNode,
404
) -> Option<&'a mut XmlNode> {
404
    if hierarchy.is_empty() {
3
        return Some(root_node);
401
    }
401
    let Some(cur_item) = hierarchy.pop() else {
        return Some(root_node);
    };
401
    let child = root_node.children.as_mut().get_mut(cur_item)?;
400
    match child {
400
        XmlNodeChild::Element(node) => get_item_internal(hierarchy, node),
        XmlNodeChild::Text(_) => None, // Can't traverse into text nodes
    }
404
}
/// Parses an XML string and returns a `StyledDom` with the components instantiated in the
/// `<app></app>`
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
/// # Errors
///
/// Returns an error if the XML cannot be parsed into a DOM (malformed markup or an unknown component).
2675
pub fn str_to_dom<'a>(
2675
    root_nodes: &'a [XmlNodeChild],
2675
    component_map: &'a ComponentMap,
2675
    max_width: Option<f32>,
2675
) -> Result<StyledDom, DomXmlParseError> {
    // Delegate to the fast path (Dom::Fast / CompactDom arena).
2675
    str_to_dom_fast(root_nodes, component_map, max_width)
2675
}
/// Parse XML to `StyledDom` via arena-based `FastDom` (no tree intermediary).
///
/// **Note**: `str_to_dom()` now delegates to this function, so you can use
/// either one. This function is kept for backward compatibility.
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
2675
fn str_to_dom_fast<'a>(
2675
    root_nodes: &'a [XmlNodeChild],
2675
    component_map: &'a ComponentMap,
2675
    max_width: Option<f32>,
2675
) -> Result<StyledDom, DomXmlParseError> {
2675
    let html_node = get_html_node(root_nodes)?;
2586
    let body_node = get_body_node(html_node.children.as_ref())?;
2585
    let mut global_style = None;
2585
    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
2277
        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
2277
            let text = style_node.get_text_content();
2277
            if !text.is_empty() {
2267
                let parsed_css = Css::from_string(text.into());
2267
                global_style = Some(parsed_css);
2276
            }
        }
308
    }
2585
    render_dom_from_body_node_fast(body_node, global_style, component_map, max_width)
2585
        .map_err(Into::into)
2675
}
/// Parses XML nodes and returns a `Dom` with CSS stylesheets attached (but not applied).
///
/// Unlike `str_to_dom` which returns a fully styled `StyledDom`, this function
/// returns an unstyled `Dom` whose `css` field carries the parsed `<style>` rules.
/// The layout framework will apply the CSS during the cascade pass.
///
/// This is the correct function for building a `Dom` from XML in layout callbacks
/// (which must return `Dom`, not `StyledDom`).
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
/// # Errors
///
/// Returns an error if the XML cannot be parsed into a DOM (malformed markup or an unknown component).
123
pub fn str_to_dom_unstyled<'a>(
123
    root_nodes: &'a [XmlNodeChild],
123
    component_map: &'a ComponentMap,
123
) -> Result<Dom, DomXmlParseError> {
123
    let html_node = get_html_node(root_nodes)?;
89
    let body_node = get_body_node(html_node.children.as_ref())?;
78
    let mut global_style = None;
78
    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
56
        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
34
            let text = style_node.get_text_content();
34
            if !text.is_empty() {
34
                let parsed_css = Css::from_string(text.into());
34
                global_style = Some(parsed_css);
34
            }
22
        }
22
    }
    // Build the DOM tree from the body node
78
    let body_dom = xml_node_to_dom_fast(body_node, component_map, false, 0)
78
        .map_err(DomXmlParseError::from)?;
    // Wrap in proper HTML structure (NodeType is imported at module top)
78
    let root_node_type = body_dom.root.node_type.clone();
78
    let mut full_dom = match root_node_type {
        NodeType::Html => body_dom,
78
        NodeType::Body => Dom::create_html().with_child(body_dom),
        _ => {
            let body_wrapper = Dom::create_body().with_child(body_dom);
            Dom::create_html().with_child(body_wrapper)
        }
    };
    // Attach CSS to the Dom's css field instead of applying it immediately
78
    if let Some(css) = global_style {
34
        full_dom.css = alloc::vec![css].into();
45
    }
78
    Ok(full_dom)
123
}
/// Parses an XML string and returns a `String`, which contains the Rust source code
/// (i.e. it compiles the XML to valid Rust)
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
/// # Errors
///
/// Returns an error if the XML cannot be parsed or compiled to Rust code.
6
pub fn str_to_rust_code<'a>(
6
    root_nodes: &'a [XmlNodeChild],
6
    imports: &str,
6
    component_map: &'a ComponentMap,
6
) -> Result<String, CompileError> {
6
    let html_node = get_html_node(root_nodes)?;
2
    let body_node = get_body_node(html_node.children.as_ref())?;
2
    let mut global_style = Css::empty();
2
    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
2
        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
2
            let text = style_node.get_text_content();
2
            if !text.is_empty() {
                let parsed_css = azul_css::parser2::new_from_str(&text).0;
                global_style = parsed_css;
2
            }
        }
    }
2
    global_style.sort_by_specificity();
2
    let mut css_blocks = BTreeMap::new();
2
    let mut extra_blocks = VecContents::default();
2
    let app_source = compile_body_node_to_rust_code(
2
        body_node,
2
        component_map,
2
        &mut extra_blocks,
2
        &mut css_blocks,
2
        &global_style,
2
        CssMatcher {
2
            path: Vec::new(),
2
            indices_in_parent: vec![0],
2
            children_length: vec![body_node.children.as_ref().len()],
2
        },
    )?;
2
    let app_source = app_source
2
        .lines()
13
        .map(|l| format!("        {l}"))
2
        .collect::<Vec<String>>()
2
        .join("\r\n");
    // NOTE: `css_blocks` / `extra_blocks` are no longer emitted — per-node styles
    // are now inlined as `.with_css("..")` strings (public API) rather than as
    // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` blocks (that API was
    // removed in 32d44ed8a). The maps stay in the signatures for compatibility.
2
    let _ = (&css_blocks, &extra_blocks);
2
    let main_func = "
2

            
2
use azul::{
2
    app::{App, AppConfig},
2
    dom::Dom,
2
    callbacks::{RefAny, LayoutCallbackInfo},
2
    window::WindowCreateOptions,
2
};
2

            
2
struct Data { }
2

            
2
extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> Dom {
2
    crate::ui::render()
2
}
2

            
2
fn main() {
2
    let config = AppConfig::create();
2
    let app = App::create(RefAny::new(Data { }), config);
2
    let window = WindowCreateOptions::create(render);
2
    app.run(window);
2
}";
2
    let ui_module = format!(
2
        "#[allow(unused_imports)]\r\npub mod ui {{
2

            
2
    use azul::prelude::*;
2
    use azul::dom::{{NodeType, TabIndex, SmallAriaInfo}};
2
    use azul::str::String as AzString;
2

            
2
    pub fn render() -> Dom {{\r\n{app_source}\r\n    }}\r\n}}"
    );
2
    let source_code = format!(
2
        "#![windows_subsystem = \"windows\"]\r\n//! Auto-generated UI source \
2
         code\r\n{}\r\n{}\r\n\r\n{}{}",
        imports,
2
        compile_components(Vec::new()), // no user-defined components to compile
        ui_module,
        main_func,
    );
2
    Ok(source_code)
6
}
// Compile all components to source code
#[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
3
fn compile_components(
3
    components: Vec<(
3
        ComponentName,
3
        CompiledComponent,
3
        ComponentArguments,
3
        BTreeMap<String, String>,
3
    )>,
3
) -> String {
3
    let cs = components
3
        .iter()
3
        .map(|(name, function_body, function_args, css_blocks)| {
            let name = &normalize_casing(name);
            let f = compile_component(name, function_args, function_body)
                .lines()
                .map(|l| format!("    {l}"))
                .collect::<Vec<String>>()
                .join("\r\n");
            // let css_blocks = ...
            format!(
                "#[allow(unused_imports)]\r\npub mod {name} {{\r\n    use azul::dom::Dom;\r\n    use \
                 azul::str::String as AzString;\r\n{f}\r\n}}"
            )
        })
3
        .collect::<Vec<String>>()
3
        .join("\r\n\r\n");
3
    let cs = cs
3
        .lines()
3
        .map(|l| format!("    {l}"))
3
        .collect::<Vec<String>>()
3
        .join("\r\n");
3
    if cs.is_empty() {
3
        cs
    } else {
        format!("pub mod components {{\r\n{cs}\r\n}}")
    }
3
}
7
fn format_component_args(component_args: &ComponentArgumentVec) -> String {
7
    let mut args = component_args
7
        .iter()
7
        .map(|a| format!("{}: {}", a.name, a.arg_type))
7
        .collect::<Vec<String>>();
7
    args.sort_by(|a, b| b.cmp(a));
7
    args.join(", ")
7
}
4
#[must_use] pub fn compile_component(
4
    component_name: &str,
4
    component_args: &ComponentArguments,
4
    component_function_body: &str,
4
) -> String {
4
    let component_name = &normalize_casing(component_name);
4
    let function_args = format_component_args(&component_args.args);
4
    let component_function_body = component_function_body
4
        .lines()
4
        .map(|l| format!("    {l}"))
4
        .collect::<Vec<String>>()
4
        .join("\r\n");
4
    let should_inline = component_function_body.lines().count() == 1;
4
    format!(
4
        "{}pub fn render({}{}{}) -> Dom {{\r\n{}\r\n}}",
4
        if should_inline { "#[inline]\r\n" } else { "" },
        // pass the text content as the first
4
        if component_args.accepts_text {
2
            "text: AzString"
        } else {
2
            ""
        },
4
        if function_args.is_empty() || !component_args.accepts_text {
3
            ""
        } else {
1
            ", "
        },
        function_args,
        component_function_body,
    )
4
}
/// Parse an SVG numeric attribute value to f32.
704
fn parse_svg_float(attr: Option<&AzString>) -> Option<f32> {
704
    attr?.as_str().trim().parse::<f32>().ok()
704
}
/// Parse an SVG `points` attribute (used by `<polygon>` and `<polyline>`).
45
fn parse_svg_points(pts: &str, close: bool) -> Option<crate::svg::SvgMultiPolygon> {
45
    let nums: Vec<f32> = pts
80669
        .split(|c: char| c == ',' || c.is_ascii_whitespace())
40258
        .filter(|s| !s.is_empty())
40252
        .filter_map(|s| s.parse::<f32>().ok())
45
        .collect();
45
    if nums.len() < 4 || !nums.len().is_multiple_of(2) {
6
        return None;
39
    }
39
    let mut elements = Vec::new();
39
    let points: Vec<azul_css::props::basic::SvgPoint> = nums
39
        .chunks_exact(2)
20122
        .map(|c| azul_css::props::basic::SvgPoint { x: c[0], y: c[1] })
39
        .collect();
20083
    for w in points.windows(2) {
20083
        elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
20083
            w[0], w[1],
20083
        )));
20083
    }
39
    if close && points.len() >= 2 {
24
        let first = points[0];
24
        let last = *points.last().unwrap();
24
        if (first.x - last.x).abs() > 0.001 || (first.y - last.y).abs() > 0.001 {
23
            elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
23
                last, first,
23
            )));
23
        }
15
    }
39
    Some(crate::svg::SvgMultiPolygon {
39
        rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
39
            items: crate::svg::SvgPathElementVec::from_vec(elements),
39
        }]),
39
    })
45
}
/// Fast XML to Dom conversion that builds Dom tree directly without intermediate `StyledDom`
/// This is O(n) instead of O(n²) for large documents
/// Apply the shared set of XML attributes onto a single [`NodeData`] node.
///
/// Handles `<img src>` rebuild, `id`/`class`, `focusable`, `tabindex`, inline
/// `style`, and SVG-shape geometry — the block that was previously duplicated
/// verbatim between [`xml_node_to_dom_fast`] (operating on `dom.root`) and
/// [`xml_node_to_fast_dom`] (operating on the arena `NodeData`). `component_name`
/// must already be normalized (lowercased); the caller computes `child_inside_svg`.
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
39926
fn apply_xml_node_attributes(
39926
    node: &mut crate::dom::NodeData,
39926
    xml_node: &XmlNode,
39926
    component_name: &str,
39926
    inside_svg: bool,
39926
) {
    use crate::dom::{IdOrClass, NodeType, TabIndex};
    // `<img src="...">`: rebuild the placeholder Image node so its `NullImage`
    // carries the `src` string (as UTF-8 bytes in `tag`). The bytes are NOT
    // resolved here — a downstream renderer (printpdf, the compositor, ...) uses
    // the tag to look up and embed the actual image. Optional `width`/`height`
    // attributes set the intrinsic size used for layout (CSS still overrides).
39926
    if component_name == "img" {
2
        if let Some(src) = xml_node.attributes.get_key("src") {
2
            let width = xml_node
2
                .attributes
2
                .get_key("width")
2
                .and_then(|w| {
2
                    w.as_str()
2
                        .trim()
2
                        .trim_end_matches("px")
2
                        .trim()
2
                        .parse::<usize>()
2
                        .ok()
2
                })
2
                .unwrap_or(0);
2
            let height = xml_node
2
                .attributes
2
                .get_key("height")
2
                .and_then(|h| {
2
                    h.as_str()
2
                        .trim()
2
                        .trim_end_matches("px")
2
                        .trim()
2
                        .parse::<usize>()
2
                        .ok()
2
                })
2
                .unwrap_or(0);
2
            let image_ref = crate::resources::ImageRef::null_image(
2
                width,
2
                height,
2
                crate::resources::RawImageFormat::RGBA8,
2
                src.as_str().as_bytes().to_vec(),
            );
2
            node
2
                .set_node_type(NodeType::Image(azul_css::css::BoxOrStatic::heap(image_ref)));
        }
39924
    }
    // Set id and class attributes
39926
    let mut ids_and_classes = Vec::new();
39926
    if let Some(id_str) = xml_node.attributes.get_key("id") {
277
        for id in id_str.split_whitespace() {
277
            ids_and_classes.push(IdOrClass::Id(id.into()));
277
        }
39649
    }
39926
    if let Some(class_str) = xml_node.attributes.get_key("class") {
3168
        for class in class_str.split_whitespace() {
3168
            ids_and_classes.push(IdOrClass::Class(class.into()));
3168
        }
36758
    }
39926
    if !ids_and_classes.is_empty() {
3445
        node.set_ids_and_classes(ids_and_classes.into());
36481
    }
    // Handle focusable attribute
39926
    if let Some(focusable) = xml_node
39926
        .attributes
39926
        .get_key("focusable")
39926
        .and_then(|f| parse_bool(f.as_str()))
    {
9
        if focusable { node.set_tab_index(TabIndex::Auto) } else { node.set_tab_index(TabIndex::NoKeyboardFocus) }
39917
    }
    // Handle tabindex attribute
39926
    if let Some(tab_index) = xml_node
39926
        .attributes
39926
        .get_key("tabindex")
39926
        .and_then(|val| val.parse::<isize>().ok())
    {
4
        match tab_index {
1
            0 => node.set_tab_index(TabIndex::Auto),
4
            i if i > 0 => node.set_tab_index(TabIndex::OverrideInParent(u32::try_from(i).unwrap_or(u32::MAX))),
2
            _ => node.set_tab_index(TabIndex::NoKeyboardFocus),
        }
39921
    }
    // Table cell span attributes (`colspan` / `rowspan`).
39926
    apply_cell_span_attributes(node, xml_node);
    // HTML `dir` attribute → the `direction` CSS property (dir="rtl"/"ltr"). Without
    // this, dir="rtl" (the common way to set RTL in HTML) had no effect. Appended
    // BEFORE the inline `style` below so author style still wins on equal specificity.
39926
    let dir_prop = xml_node.attributes.get_key("dir").and_then(|d| {
        let v = d.as_str().trim();
        if v.eq_ignore_ascii_case("rtl") {
            Some(azul_css::props::style::StyleDirection::Rtl)
        } else if v.eq_ignore_ascii_case("ltr") {
            Some(azul_css::props::style::StyleDirection::Ltr)
        } else {
            None
        }
    });
    // Handle inline style attribute (and the mapped `dir` attribute above)
39926
    let style_attr = xml_node.attributes.get_key("style");
39926
    if style_attr.is_some() || dir_prop.is_some() {
        use azul_css::dynamic_selector::CssPropertyWithConditions;
11
        let css_key_map = azul_css::props::property::get_css_key_map();
11
        let mut props: Vec<CssPropertyWithConditions> = Vec::new();
11
        if let Some(dir) = dir_prop {
            props.push(CssPropertyWithConditions::simple(
                azul_css::props::property::CssProperty::Direction(
                    azul_css::css::CssPropertyValue::Exact(dir),
                ),
            ));
11
        }
11
        if let Some(style) = style_attr {
11
            let mut attributes = Vec::new();
22
            for s in style.as_str().split(';') {
22
                let mut s = s.split(':');
22
                let Some(key) = s.next() else {
                    continue;
                };
22
                let Some(value) = s.next() else {
11
                    continue;
                };
                // Called for its side effect (writes parsed props into `attributes`);
                // the returned value is intentionally discarded.
11
                drop(azul_css::parser2::parse_css_declaration(
11
                    key.trim(),
11
                    value.trim(),
11
                    azul_css::parser2::ErrorLocationRange::default(),
11
                    &css_key_map,
11
                    &mut Vec::new(),
11
                    &mut attributes,
                ));
            }
11
            props.extend(attributes.into_iter().filter_map(|s| match s {
11
                CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
                CssDeclaration::Dynamic(_) => None,
11
            }));
        }
11
        if !props.is_empty() {
11
            node.set_css_props(props.into());
11
        }
39915
    }
    // Handle SVG shape elements when inside an <svg> context
39926
    let tag = component_name;
39926
    let is_svg_shape = inside_svg
231
        && matches!(
253
            tag,
253
            "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline"
        );
39926
    if is_svg_shape {
231
        let clip = match tag {
231
            "path" => xml_node
44
                .attributes
44
                .get_key("d")
44
                .and_then(|d| crate::path_parser::parse_svg_path_d(d.as_str()).ok()),
187
            "circle" => {
55
                let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
55
                let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
55
                let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
55
                if r > 0.0 {
44
                    Some(crate::svg::SvgMultiPolygon {
44
                        rings: crate::svg::SvgPathVec::from_vec(vec![
44
                            crate::path_parser::svg_circle_to_paths(cx, cy, r),
44
                        ]),
44
                    })
                } else {
11
                    None
                }
            }
132
            "rect" => {
66
                let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
66
                let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
66
                let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
66
                let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
66
                let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
66
                let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
66
                if w > 0.0 && h > 0.0 {
55
                    Some(crate::svg::SvgMultiPolygon {
55
                        rings: crate::svg::SvgPathVec::from_vec(vec![
55
                            crate::path_parser::svg_rect_to_path(x, y, w, h, rx, ry),
55
                        ]),
55
                    })
                } else {
11
                    None
                }
            }
66
            "ellipse" => {
22
                let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
22
                let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
22
                let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
22
                let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
22
                if rx > 0.0 && ry > 0.0 {
                    // Approximate ellipse with 4 cubic beziers (using rx for x-kappa, ry for y-kappa)
                    use azul_css::props::basic::{SvgCubicCurve, SvgPoint};
                    const KAPPA: f32 = 0.552_284_8;
22
                    let kx = rx * KAPPA;
22
                    let ky = ry * KAPPA;
22
                    let elements = vec![
22
                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
22
                            start: SvgPoint { x: cx, y: cy - ry },
22
                            ctrl_1: SvgPoint {
22
                                x: cx + kx,
22
                                y: cy - ry,
22
                            },
22
                            ctrl_2: SvgPoint {
22
                                x: cx + rx,
22
                                y: cy - ky,
22
                            },
22
                            end: SvgPoint { x: cx + rx, y: cy },
22
                        }),
22
                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
22
                            start: SvgPoint { x: cx + rx, y: cy },
22
                            ctrl_1: SvgPoint {
22
                                x: cx + rx,
22
                                y: cy + ky,
22
                            },
22
                            ctrl_2: SvgPoint {
22
                                x: cx + kx,
22
                                y: cy + ry,
22
                            },
22
                            end: SvgPoint { x: cx, y: cy + ry },
22
                        }),
22
                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
22
                            start: SvgPoint { x: cx, y: cy + ry },
22
                            ctrl_1: SvgPoint {
22
                                x: cx - kx,
22
                                y: cy + ry,
22
                            },
22
                            ctrl_2: SvgPoint {
22
                                x: cx - rx,
22
                                y: cy + ky,
22
                            },
22
                            end: SvgPoint { x: cx - rx, y: cy },
22
                        }),
22
                        crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
22
                            start: SvgPoint { x: cx - rx, y: cy },
22
                            ctrl_1: SvgPoint {
22
                                x: cx - rx,
22
                                y: cy - ky,
22
                            },
22
                            ctrl_2: SvgPoint {
22
                                x: cx - kx,
22
                                y: cy - ry,
22
                            },
22
                            end: SvgPoint { x: cx, y: cy - ry },
22
                        }),
                    ];
22
                    Some(crate::svg::SvgMultiPolygon {
22
                        rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
22
                            items: crate::svg::SvgPathElementVec::from_vec(elements),
22
                        }]),
22
                    })
                } else {
                    None
                }
            }
44
            "line" => {
11
                let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
11
                let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
11
                let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
11
                let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
11
                Some(crate::svg::SvgMultiPolygon {
11
                    rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
11
                        items: crate::svg::SvgPathElementVec::from_vec(vec![
11
                            crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
11
                                azul_css::props::basic::SvgPoint { x: x1, y: y1 },
11
                                azul_css::props::basic::SvgPoint { x: x2, y: y2 },
11
                            )),
11
                        ]),
11
                    }]),
11
                })
            }
33
            "polygon" | "polyline" => xml_node
33
                .attributes
33
                .get_key("points")
33
                .and_then(|pts| parse_svg_points(pts.as_str(), tag == "polygon")),
            _ => None,
        };
231
        if let Some(mp) = clip {
209
            node.set_svg_data(crate::dom::SvgNodeData::Path(mp));
209
        }
39695
    }
39926
}
/// Parse the HTML `colspan` / `rowspan` presentational attributes into
/// `AttributeType`s on the node. The table layout reads them back via
/// `get_cell_spans`. Without this the XML→DOM conversion dropped them and every
/// cell defaulted to span 1, so `<th colspan="2">` only covered one column.
/// Parsed unconditionally — non-cell elements simply don't carry these attributes.
39926
fn apply_cell_span_attributes(node: &mut crate::dom::NodeData, xml_node: &XmlNode) {
39926
    let mut spans = Vec::new();
39926
    if let Some(n) = xml_node
39926
        .attributes
39926
        .get_key("colspan")
39926
        .and_then(|v| v.as_str().trim().parse::<i32>().ok())
    {
        spans.push(crate::dom::AttributeType::ColSpan(n));
39926
    }
39926
    if let Some(n) = xml_node
39926
        .attributes
39926
        .get_key("rowspan")
39926
        .and_then(|v| v.as_str().trim().parse::<i32>().ok())
    {
        spans.push(crate::dom::AttributeType::RowSpan(n));
39926
    }
39926
    if !spans.is_empty() {
        let mut v = node.attributes().clone().into_library_owned_vec();
        v.extend(spans);
        node.set_attributes(v.into());
39926
    }
39926
}
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
// component_map is threaded through the whole fast-DOM pipeline for parity with the
// component-expanding interpreter path (see ~xml.rs:2845); this fast path never expands
// components, so it only forwards the map into recursive calls. Removing it here would
// cascade unused-param removals up the entire pipeline.
#[allow(clippy::only_used_in_recursion)]
6912
fn xml_node_to_dom_fast<'a>(
6912
    xml_node: &'a XmlNode,
6912
    component_map: &'a ComponentMap,
6912
    inside_svg: bool,
6912
    depth: usize,
6912
) -> Result<Dom, RenderDomError> {
    use crate::dom::Dom;
6912
    let component_name = normalize_casing(&xml_node.node_type);
    // Look up the component definition
6912
    let node_type = tag_to_node_type(&component_name);
6912
    let mut dom = Dom::create_node(node_type);
6912
    apply_xml_node_attributes(&mut dom.root, xml_node, &component_name, inside_svg);
6912
    let child_inside_svg = inside_svg || component_name == "svg";
    // AUDIT 2026-07-08: bound recursion depth to avoid a native stack overflow on
    // pathologically deep markup. At the cap, this node is emitted without its
    // children (truncation) rather than crashing the process.
    // AUDIT-TODO: a worklist-based iterative builder would preserve deep subtrees.
6912
    if depth >= MAX_XML_NESTING_DEPTH {
14
        return Ok(dom);
6898
    }
    // Recursively convert children
6898
    let mut children = Vec::new();
8086
    for child in xml_node.children.as_ref() {
8074
        match child {
6817
            XmlNodeChild::Element(child_node) => {
6817
                let child_dom =
6817
                    xml_node_to_dom_fast(child_node, component_map, child_inside_svg, depth + 1)?;
6817
                children.push(child_dom);
            }
1257
            XmlNodeChild::Text(text) => {
1257
                let text_dom = Dom::create_text_do_not_use_without_block_level_wrapper(AzString::from(text.as_str()));
1257
                children.push(text_dom);
1257
            }
        }
    }
6898
    if !children.is_empty() {
6874
        dom = dom.with_children(children.into());
6874
    }
6898
    Ok(dom)
6912
}
/// Builder for arena-based DOM construction (`FastDom`).
/// Builds two parallel Vecs (hierarchy + `node_data`) in a single DFS pass.
#[derive(Debug)]
pub struct CompactDomBuilder {
    hierarchy: Vec<crate::styled_dom::NodeHierarchyItem>,
    node_data: Vec<crate::dom::NodeData>,
    css: Vec<crate::dom::CssWithNodeId>,
    /// Stack of (`node_index`, `previous_child_index`) for open elements
    stack: Vec<(usize, Option<usize>)>,
}
impl Default for CompactDomBuilder {
1
    fn default() -> Self {
1
        Self::new()
1
    }
}
impl CompactDomBuilder {
2594
    #[must_use] pub const fn new() -> Self {
2594
        Self {
2594
            hierarchy: Vec::new(),
2594
            node_data: Vec::new(),
2594
            css: Vec::new(),
2594
            stack: Vec::new(),
2594
        }
2594
    }
2024
    #[must_use] pub fn with_capacity(cap: usize) -> Self {
2024
        Self {
2024
            hierarchy: Vec::with_capacity(cap),
2024
            node_data: Vec::with_capacity(cap),
2024
            css: Vec::new(),
2024
            stack: Vec::new(),
2024
        }
2024
    }
    /// Open a new element node. Must be paired with `close_node()`.
201129
    pub fn open_node(&mut self, node_data: crate::dom::NodeData) {
        use crate::id::NodeId;
        use crate::styled_dom::NodeHierarchyItem;
201129
        let idx = self.hierarchy.len();
        // Determine parent from stack
201129
        let parent_raw = if let Some(&(parent_idx, _)) = self.stack.last() {
197112
            NodeId::into_raw(&Some(NodeId::new(parent_idx)))
        } else {
4017
            0 // No parent (root)
        };
        // Determine previous sibling from parent's last child tracking
201129
        let prev_sibling_raw = if let Some(&(_, prev_child)) = self.stack.last() {
197112
            prev_child
197112
                .map_or(0, |pi| NodeId::into_raw(&Some(NodeId::new(pi))))
        } else {
4017
            0
        };
        // If there's a previous sibling, set its next_sibling to us
201129
        if let Some(&(_, Some(prev_idx))) = self.stack.last() {
49789
            self.hierarchy[prev_idx].next_sibling = NodeId::into_raw(&Some(NodeId::new(idx)));
151710
        }
        // Update parent's "last seen child" to us
201129
        if let Some(parent) = self.stack.last_mut() {
197112
            parent.1 = Some(idx);
197112
        }
        // Push the hierarchy item (last_child will be set in close_node)
201129
        self.hierarchy.push(NodeHierarchyItem {
201129
            parent: parent_raw,
201129
            previous_sibling: prev_sibling_raw,
201129
            next_sibling: 0, // Will be set by next sibling's open_node
201129
            last_child: 0,   // Will be set in close_node
201129
        });
201129
        self.node_data.push(node_data);
        // Push onto stack: this node is now the "open" element, no children yet
201129
        self.stack.push((idx, None));
201129
    }
    /// Close the current element. Sets the `last_child` pointer.
201130
    pub fn close_node(&mut self) {
        use crate::id::NodeId;
201130
        if let Some((idx, last_child_idx)) = self.stack.pop() {
            // Set last_child on this node's hierarchy item
201040
            self.hierarchy[idx].last_child = last_child_idx
201040
                .map_or(0, |lc| NodeId::into_raw(&Some(NodeId::new(lc))));
90
        }
201130
    }
    /// Add a leaf node (text, br, hr, etc.) that has no children.
51643
    pub fn add_leaf(&mut self, node_data: crate::dom::NodeData) {
51643
        self.open_node(node_data);
51643
        self.close_node();
51643
    }
    /// Add a CSS stylesheet scoped to a node ID.
2
    pub fn add_css(&mut self, node_id: usize, css: Css) {
2
        self.css.push(crate::dom::CssWithNodeId { node_id, css });
2
    }
    /// Finish building and produce a `FastDom`.
4133
    #[must_use] pub fn finish(self) -> crate::dom::FastDom {
4133
        crate::dom::FastDom {
4133
            node_hierarchy: self.hierarchy.into(),
4133
            node_data: self.node_data.into(),
4133
            css: self.css.into(),
4133
        }
4133
    }
}
/// Convert an XML node tree into a `FastDom` (arena-based) in a single DFS pass.
/// This is the fast path equivalent of `xml_node_to_dom_fast`.
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
// See xml_node_to_dom_fast: component_map is forwarded for pipeline parity, not read here.
#[allow(clippy::only_used_in_recursion)]
33014
fn xml_node_to_fast_dom<'a>(
33014
    xml_node: &'a XmlNode,
33014
    component_map: &'a ComponentMap,
33014
    inside_svg: bool,
33014
    builder: &mut CompactDomBuilder,
33014
    depth: usize,
33014
) -> Result<(), RenderDomError> {
    use crate::dom::NodeData;
33014
    let component_name = normalize_casing(&xml_node.node_type);
33014
    let node_type = tag_to_node_type(&component_name);
33014
    let mut node_data = NodeData::create_node(node_type);
33014
    apply_xml_node_attributes(&mut node_data, xml_node, &component_name, inside_svg);
33014
    let child_inside_svg = inside_svg || component_name == "svg";
    // Open this node in the builder
33014
    builder.open_node(node_data);
    // AUDIT 2026-07-08: bound recursion depth to avoid a native stack overflow on
    // pathologically deep markup. At the cap, children are dropped (the node is
    // still opened+closed) rather than crashing the process.
    // AUDIT-TODO: a worklist-based iterative builder would preserve deep subtrees.
33014
    if depth < MAX_XML_NESTING_DEPTH {
        // Recursively convert children
80509
        for child in xml_node.children.as_ref() {
80499
            match child {
30426
                XmlNodeChild::Element(child_node) => {
30426
                    xml_node_to_fast_dom(
30426
                        child_node,
30426
                        component_map,
30426
                        child_inside_svg,
30426
                        builder,
30426
                        depth + 1,
                    )?;
                }
50073
                XmlNodeChild::Text(text) => {
50073
                    builder.add_leaf(NodeData::create_text_do_not_use_without_block_level_wrapper(AzString::from(text.as_str())));
50073
                }
            }
        }
3
    }
    // Close this node
33014
    builder.close_node();
33014
    Ok(())
33014
}
/// Render a DOM from an XML body node using the fast arena-based path.
/// Builds a `FastDom` directly (no tree intermediary), then creates `StyledDom`.
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
2585
fn render_dom_from_body_node_fast<'a>(
2585
    body_node: &'a XmlNode,
2585
    mut global_css: Option<Css>,
2585
    component_map: &'a ComponentMap,
2585
    max_width: Option<f32>,
2585
) -> Result<StyledDom, RenderDomError> {
    use crate::dom::{NodeData, NodeType};
2585
    let mut builder = CompactDomBuilder::new();
    // Build the HTML > Body wrapper + body content in one pass
    // Open <html>
2585
    builder.open_node(NodeData::create_node(NodeType::Html));
    // Open <body> (the body_node content goes inside)
2585
    xml_node_to_fast_dom(body_node, component_map, false, &mut builder, 0)?;
    // Close <html>
2585
    builder.close_node();
    // Collect CSS rules from each source.
2585
    let mut combined_rules: Vec<CssRuleBlock> = Vec::new();
2585
    if let Some(max_width) = max_width {
8
        let max_width_css =
8
            Css::from_string(format!("html {{ max-width: {max_width}px; }}").into());
8
        combined_rules.extend(max_width_css.rules.into_library_owned_vec());
2582
    }
2585
    let mut combined_keyframes = Vec::new();
2585
    if let Some(css) = global_css.take() {
2267
        combined_rules.extend(css.rules.into_library_owned_vec());
2267
        combined_keyframes.extend(css.keyframes.into_library_owned_vec());
2276
    }
2585
    let mut combined_css = Css::new(combined_rules);
2585
    combined_css.keyframes = combined_keyframes.into();
    // Add CSS to the FastDom
2585
    let mut fast_dom = builder.finish();
2585
    fast_dom.css = vec![crate::dom::CssWithNodeId {
2585
        node_id: 0, // Global scope (root)
2585
        css: combined_css,
2585
    }]
2585
    .into();
    // Create StyledDom via the fast path (no tree→arena conversion)
2585
    let styled = StyledDom::create_from_fast_dom(fast_dom);
2585
    Ok(styled)
2585
}
// render_dom_from_body_node() removed — use render_dom_from_body_node_fast() or str_to_dom()
15
fn set_stringified_attributes(
15
    dom_string: &mut String,
15
    xml_attributes: &XmlAttributeMap,
15
    filtered_xml_attributes: &ComponentArgumentVec,
15
    tabs: usize,
15
) {
15
    let t0 = String::from("    ").repeat(tabs);
15
    let t = String::from("    ").repeat(tabs + 1);
    // push ids and classes as chained `.with_id("..")` / `.with_class("..")`
    // calls (public builder API; both take `Into<AzString>`, so bare &str works).
15
    let _ = &t;
15
    for id in xml_attributes
15
        .get_key("id")
15
        .map(|s| s.split_whitespace().collect::<Vec<_>>())
15
        .unwrap_or_default()
3
    {
3
        let _ = write!(
3
            dom_string,
3
            "\r\n{}.with_id(\"{}\")",
3
            t0,
3
            format_args_dynamic(id, filtered_xml_attributes)
3
        );
3
    }
15
    for class in xml_attributes
15
        .get_key("class")
15
        .map(|s| s.split_whitespace().collect::<Vec<_>>())
15
        .unwrap_or_default()
2
    {
2
        let _ = write!(
2
            dom_string,
2
            "\r\n{}.with_class(\"{}\")",
2
            t0,
2
            format_args_dynamic(class, filtered_xml_attributes)
2
        );
2
    }
15
    if let Some(focusable) = xml_attributes
15
        .get_key("focusable")
15
        .map(|f| format_args_dynamic(f, filtered_xml_attributes))
15
        .and_then(|f| parse_bool(&f))
    {
2
        if focusable { let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)"); } else { let _ = write!(dom_string,
1
            "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
1
        ); }
13
    }
15
    if let Some(tab_index) = xml_attributes
15
        .get_key("tabindex")
15
        .map(|val| format_args_dynamic(val, filtered_xml_attributes))
15
        .and_then(|val| val.parse::<isize>().ok())
    {
2
        match tab_index {
1
            0 => { let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)"); },
2
            i if i > 0 => { let _ = write!(dom_string,
1
                "\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
1
                t, usize::try_from(i).unwrap_or(0)
1
            ); },
1
            _ => { let _ = write!(dom_string,
1
                "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
1
            ); },
        }
12
    }
15
}
/// Item of a split string - either a variable name (with optional format spec) or a string
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub enum DynamicItem {
    /// A variable reference, e.g. {counter} or {counter:?} or {price:.2}
    Var {
        name: String,
        /// Optional format specifier after the colon: "?" for debug, ".2" for precision, etc.
        format_spec: Option<String>,
    },
    Str(String),
}
/// Splits a string into formatting arguments, supporting format specifiers like `{var:?}`
/// ```rust
/// # use azul_core::xml::DynamicItem::*;
/// # use azul_core::xml::split_dynamic_string;
/// let s = "hello {a}, {b}{{ {c} }}";
/// let split = split_dynamic_string(s);
/// let output = vec![
///     Str("hello ".to_string()),
///     Var { name: "a".to_string(), format_spec: None },
///     Str(", ".to_string()),
///     Var { name: "b".to_string(), format_spec: None },
///     Str("{ ".to_string()),
///     Var { name: "c".to_string(), format_spec: None },
///     Str(" }".to_string()),
/// ];
/// assert_eq!(output, split);
/// ```
41
#[must_use] pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
    use self::DynamicItem::{Str, Var};
41
    let input: Vec<char> = input.chars().collect();
41
    let input_chars_len = input.len();
41
    let mut items = Vec::new();
41
    let mut current_idx = 0;
41
    let mut last_idx = 0;
150173
    while current_idx < input_chars_len {
150132
        let c = input[current_idx];
100023
        match c {
100023
            '{' if input.get(current_idx + 1).copied() != Some('{') => {
                // variable start, search until next closing brace or whitespace or end of string
22
                let mut start_offset = 1;
22
                let mut has_found_variable = false;
100069
                while let Some(c) = input.get(current_idx + start_offset) {
100066
                    if c.is_whitespace() {
3
                        break;
100063
                    }
100063
                    if *c == '}' && input.get(current_idx + start_offset + 1).copied() != Some('}')
                    {
16
                        start_offset += 1;
16
                        has_found_variable = true;
16
                        break;
100047
                    }
100047
                    start_offset += 1;
                }
                // advance current_idx accordingly
                // on fail, set cursor to end
                // set last_idx accordingly
22
                if has_found_variable {
16
                    if last_idx != current_idx {
8
                        items.push(Str(input[last_idx..current_idx].iter().collect()));
8
                    }
                    // subtract 1 from start for opening brace, one from end for closing brace
16
                    let var_content: String = input
16
                        [(current_idx + 1)..(current_idx + start_offset - 1)]
16
                        .iter()
16
                        .collect();
                    // Split on first ':' to separate variable name from format specifier
16
                    let (var_name, format_spec) = if let Some(colon_pos) = var_content.find(':') {
4
                        let name = var_content[..colon_pos].to_string();
4
                        let spec = var_content[(colon_pos + 1)..].to_string();
4
                        (name, Some(spec))
                    } else {
12
                        (var_content, None)
                    };
16
                    items.push(Var {
16
                        name: var_name,
16
                        format_spec,
16
                    });
16
                    current_idx += start_offset;
16
                    last_idx = current_idx;
6
                } else {
6
                    current_idx += start_offset;
6
                }
            }
150110
            _ => {
150110
                current_idx += 1;
150110
            }
        }
    }
41
    if current_idx != last_idx {
26
        items.push(Str(input[last_idx..].iter().collect()));
26
    }
91
    for item in &mut items {
        // replace {{ with { in strings
50
        if let Str(s) = item {
34
            *s = s.replace("{{", "{").replace("}}", "}");
34
        }
    }
41
    items
41
}
/// Combines the split string back into its original form while replacing the variables with their
/// values
///
/// let variables = btreemap!{ "a" => "value1", "b" => "value2" };
/// [Str("hello "), Var("a"), Str(", "), Var("b"), Str("{ "), Var("c"), Str(" }}")]
/// => "hello value1, valuec{ {c} }"
27
fn combine_and_replace_dynamic_items(
27
    input: &[DynamicItem],
27
    variables: &ComponentArgumentVec,
27
) -> String {
27
    let mut s = String::new();
58
    for item in input {
31
        match item {
8
            DynamicItem::Var { name, format_spec } => {
8
                let variable_name = normalize_casing(name.trim());
8
                if let Some(resolved_var) = variables
8
                    .iter()
8
                    .find(|s| s.name.as_str() == variable_name)
8
                    .map(|q| &q.arg_type) {
4
                    // Format specifiers are applied at compile time, not at runtime replacement
4
                    s.push_str(resolved_var);
4
                } else {
4
                    s.push('{');
4
                    s.push_str(name);
4
                    if let Some(spec) = format_spec {
1
                        s.push(':');
1
                        s.push_str(spec);
3
                    }
4
                    s.push('}');
                }
            }
23
            DynamicItem::Str(dynamic_str) => {
23
                s.push_str(dynamic_str);
23
            }
        }
    }
27
    s
27
}
/// Given a string and a key => value mapping, replaces parts of the string with the value, i.e.:
///
/// ```rust
/// # use azul_core::xml::{format_args_dynamic, ComponentArgument, ComponentArgumentVec};
/// # use azul_css::AzString;
/// let variables: ComponentArgumentVec = vec![
///     ComponentArgument { name: AzString::from("a"), arg_type: AzString::from("value1") },
///     ComponentArgument { name: AzString::from("b"), arg_type: AzString::from("value2") },
/// ].into();
///
/// let initial = "hello {a}, {b}{{ {c} }}";
/// let expected = "hello value1, value2{ {c} }".to_string();
/// assert_eq!(format_args_dynamic(initial, &variables), expected);
/// ```
///
/// Note: the number (0, 1, etc.) is the order of the argument, it is irrelevant for
/// runtime formatting, only important for keeping the component / function arguments
/// in order when compiling the arguments to Rust code
26
#[must_use] pub fn format_args_dynamic(input: &str, variables: &ComponentArgumentVec) -> String {
26
    let dynamic_str_items = split_dynamic_string(input);
26
    combine_and_replace_dynamic_items(&dynamic_str_items, variables)
26
}
/// Decode a numeric character reference body (the part between `&` and `;`),
/// e.g. `"#65"` -> `'A'`, `"#x41"` -> `'A'`. Returns `None` if it is not a valid
/// numeric reference.
28
fn decode_numeric_entity(entity: &str) -> Option<char> {
28
    let num = entity.strip_prefix('#')?;
21
    let code = if let Some(hex) = num.strip_prefix(['x', 'X']) {
9
        u32::from_str_radix(hex, 16).ok()?
    } else {
12
        num.parse::<u32>().ok()?
    };
12
    char::from_u32(code)
28
}
/// Decode the common HTML/XML entities in a single left-to-right pass.
///
/// Handles `&lt;` `&gt;` `&amp;` `&quot;` `&apos;` and numeric references
/// (`&#NN;` / `&#xHH;`). `&nbsp;` and any unrecognized `&...;` sequence are left
/// verbatim. The single pass guarantees `&amp;` never double-decodes a following
/// entity. See [`prepare_string`] for why `&nbsp;` is deliberately preserved.
27
fn decode_entities(input: &str) -> String {
    // Longest handled entity body is a hex numeric ref like `#x10FFFF` (8 bytes);
    // cap the `;` search window so a stray `&` far from a `;` stays cheap.
    const MAX_ENTITY_BODY: usize = 12;
27
    let mut out = String::with_capacity(input.len());
27
    let bytes = input.as_bytes();
27
    let mut i = 0;
220152
    while i < input.len() {
220125
        if bytes[i] == b'&' {
20023
            if let Some(semi_rel) = input[i + 1..].find(';') {
22
                if semi_rel <= MAX_ENTITY_BODY {
21
                    let body = &input[i + 1..i + 1 + semi_rel];
21
                    let end = i + 1 + semi_rel; // index of ';'
                    // Leave &nbsp; for the per-line pass in prepare_string.
21
                    if body.eq_ignore_ascii_case("nbsp") {
3
                        out.push_str(&input[i..=end]);
3
                        i = end + 1;
3
                        continue;
18
                    }
18
                    let decoded = match body {
18
                        "lt" => Some('<'),
16
                        "gt" => Some('>'),
14
                        "amp" => Some('&'),
9
                        "quot" => Some('"'),
6
                        "apos" => Some('\''),
5
                        _ => decode_numeric_entity(body),
                    };
18
                    if let Some(c) = decoded {
16
                        out.push(c);
16
                        i = end + 1;
16
                        continue;
2
                    }
1
                }
20001
            }
            // Not a recognized entity: emit the '&' literally.
20004
            out.push('&');
20004
            i += 1;
200102
        } else {
200102
            // Copy one whole UTF-8 char (i is always on a char boundary here).
200102
            let ch = input[i..].chars().next().unwrap_or('\u{FFFD}');
200102
            out.push(ch);
200102
            i += ch.len_utf8();
200102
        }
    }
27
    out
27
}
// NOTE: Two sequential returns count as a single return, while single returns get ignored.
21
#[must_use] pub fn prepare_string(input: &str) -> String {
    const SPACE: &str = " ";
    const RETURN: &str = "\n";
21
    let input = input.trim();
21
    if input.is_empty() {
4
        return String::new();
17
    }
    // AUDIT 2026-07-08: previously only `&lt;`/`&gt;` were decoded. Decode the full
    // common named-entity set (`&lt;` `&gt;` `&amp;` `&quot;` `&apos;`) plus numeric
    // references (`&#NN;` decimal and `&#xHH;` hex) in a single left-to-right pass.
    // A single pass is used deliberately so `&amp;` cannot double-decode a following
    // entity (e.g. "&amp;lt;" -> literal "&lt;", not "<"). `&nbsp;` is intentionally
    // left untouched here so the per-line pass below (which runs AFTER trimming) can
    // still turn it into a space that survives leading/trailing trim.
17
    let input = decode_entities(input);
17
    let input_len = input.len();
17
    let mut final_lines: Vec<String> = Vec::new();
17
    let mut last_line_was_empty = false;
26
    for line in input.lines() {
26
        let line = line.trim();
26
        let line = line.replace("&nbsp;", " ");
26
        let current_line_is_empty = line.is_empty();
26
        if !current_line_is_empty {
22
            if last_line_was_empty {
2
                final_lines.push(format!("{RETURN}{line}"));
20
            } else {
20
                final_lines.push(line.to_string());
20
            }
4
        }
26
        last_line_was_empty = current_line_is_empty;
    }
17
    let mut target = String::with_capacity(input_len);
22
    for (line_idx, line) in final_lines.iter().enumerate() {
        // A joining space goes before every line EXCEPT the first (idx 0) and a
        // paragraph break (RETURN-prefixed). The old code also skipped the LAST line,
        // which dropped the word boundary for a soft-wrapped final line
        // ("Hello\nworld" -> "Helloworld").
22
        if !(line.starts_with(RETURN) || line_idx == 0) {
3
            target.push_str(SPACE);
19
        }
22
        target.push_str(line);
    }
17
    target
21
}
/// Parses a string ("true" or "false")
147
#[must_use] pub fn parse_bool(input: &str) -> Option<bool> {
147
    match input {
147
        "true" => Some(true),
110
        "false" => Some(false),
86
        _ => None,
    }
147
}
#[derive(Debug, Clone)]
pub struct CssMatcher {
    path: Vec<CssPathSelector>,
    indices_in_parent: Vec<usize>,
    children_length: Vec<usize>,
}
impl CssMatcher {
13
    fn get_hash(&self) -> u64 {
        use core::hash::Hash;
        use core::hash::Hasher;
13
        let mut hasher = crate::hash::DefaultHasher::new();
29
        for p in &self.path {
16
            p.hash(&mut hasher);
16
        }
13
        hasher.finish()
13
    }
}
impl CssMatcher {
3
    fn matches(&self, path: &CssPath) -> bool {
        use azul_css::css::CssPathSelector::*;
        use crate::style::{CssGroupIterator, CssGroupSplitReason};
3
        if self.path.is_empty() {
1
            return false;
2
        }
2
        if path.selectors.as_ref().is_empty() {
1
            return false;
1
        }
        // self_matcher is only ever going to contain "Children" selectors, never "DirectChildren"
1
        let mut path_groups = CssGroupIterator::new(path.selectors.as_ref()).collect::<Vec<_>>();
1
        path_groups.reverse();
1
        if path_groups.is_empty() {
            return false;
1
        }
1
        let mut self_groups = CssGroupIterator::new(self.path.as_ref()).collect::<Vec<_>>();
1
        self_groups.reverse();
1
        if self_groups.is_empty() {
            return false;
1
        }
1
        if self.indices_in_parent.len() != self_groups.len() {
1
            return false;
        }
        if self.children_length.len() != self_groups.len() {
            return false;
        }
        // self_groups = [ // HTML
        //     "body",
        //     "div.__azul_native-ribbon-container"
        //     "div.__azul_native-ribbon-tabs"
        //     "p.home"
        // ]
        //
        // path_groups = [ // CSS
        //     ".__azul_native-ribbon-tabs"
        //     "div.after-tabs"
        // ]
        // get the first path group and see if it matches anywhere in the self group
        let mut cur_selfgroup_scan = 0;
        let mut cur_pathgroup_scan = 0;
        let mut valid = false;
        let mut path_group = path_groups[cur_pathgroup_scan].clone();
        while cur_selfgroup_scan < self_groups.len() {
            let mut advance = None;
            // scan all remaining path groups
            for (id, cg) in self_groups[cur_selfgroup_scan..].iter().enumerate() {
                let gm = group_matches(
                    &path_group.0,
                    &self_groups[cur_selfgroup_scan + id].0,
                    self.indices_in_parent[cur_selfgroup_scan + id],
                    self.children_length[cur_selfgroup_scan + id],
                );
                if gm {
                    // ok: ".__azul_native-ribbon-tabs" was found within self_groups
                    // advance the self_groups by n
                    advance = Some(id);
                    break;
                }
            }
            match advance {
                Some(n) => {
                    // group was found in remaining items
                    // advance cur_pathgroup_scan by 1 and cur_selfgroup_scan by n
                    if cur_pathgroup_scan == path_groups.len() - 1 {
                        // last path group
                        return cur_selfgroup_scan + n == self_groups.len() - 1;
                    }
                    cur_pathgroup_scan += 1;
                    cur_selfgroup_scan += n;
                    path_group = path_groups[cur_pathgroup_scan].clone();
                }
                None => return false, // group was not found in remaining items
            }
        }
        // only return true if all path_groups matched
        cur_pathgroup_scan == path_groups.len() - 1
3
    }
}
// does p.home match div.after-tabs?
// a: div.after-tabs
26
fn group_matches(
26
    a: &[&CssPathSelector],
26
    b: &[&CssPathSelector],
26
    idx_in_parent: usize,
26
    parent_children: usize,
26
) -> bool {
    use azul_css::css::{CssNthChildSelector, CssPathPseudoSelector, CssPathSelector::{Global, PseudoSelector, Type, Class, Id}};
38
    for selector in a {
10
        match selector {
            // always matches
            Global |
PseudoSelector(CssPathPseudoSelector::Hover | CssPathPseudoSelector::Active |
2
CssPathPseudoSelector::Focus) => {}
3
            Type(tag) => {
3
                if !b.iter().any(|t| **t == Type(*tag)) {
2
                    return false;
1
                }
            }
1
            Class(class) => {
1
                if !b.iter().any(|t| **t == Class(class.clone())) {
                    return false;
1
                }
            }
1
            Id(id) => {
1
                if !b.iter().any(|t| **t == Id(id.clone())) {
1
                    return false;
                }
            }
            PseudoSelector(CssPathPseudoSelector::First) => {
2
                if idx_in_parent != 0 {
1
                    return false;
1
                }
            }
            PseudoSelector(CssPathPseudoSelector::Last) => {
3
                if idx_in_parent != parent_children.saturating_sub(1) {
1
                    return false;
2
                }
            }
2
            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(i))) => {
2
                if idx_in_parent != *i as usize {
1
                    return false;
1
                }
            }
            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even)) => {
3
                if !idx_in_parent.is_multiple_of(2) {
2
                    return false;
1
                }
            }
            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
2
                if idx_in_parent.is_multiple_of(2) {
1
                    return false;
1
                }
            }
3
            PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
3
                if !idx_in_parent.saturating_sub(p.offset as usize).is_multiple_of(p.pattern_repeat as usize)
                {
1
                    return false;
2
                }
            }
4
            _ => return false, // can't happen
        }
    }
12
    true
26
}
struct CssBlock {
    ending: Option<CssPathPseudoSelector>,
    block: CssRuleBlock,
}
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
/// # Errors
///
/// Returns an error if the body node cannot be compiled to Rust code.
4
pub fn compile_body_node_to_rust_code<'a>(
4
    body_node: &'a XmlNode,
4
    component_map: &'a ComponentMap,
4
    extra_blocks: &mut VecContents,
4
    css_blocks: &mut BTreeMap<String, String>,
4
    css: &Css,
4
    mut matcher: CssMatcher,
4
) -> Result<String, CompileError> {
    use azul_css::css::CssDeclaration;
4
    let t = "";
4
    let t2 = "    ";
4
    let mut dom_string = String::from("Dom::create_body()");
4
    let node_type = CssPathSelector::Type(NodeTypeTag::Body);
4
    matcher.path.push(node_type);
4
    let ids = body_node
4
        .attributes
4
        .get_key("id")
4
        .map(|s| s.split_whitespace().collect::<Vec<_>>())
4
        .unwrap_or_default();
4
    matcher.path.extend(
4
        ids.into_iter()
4
            .map(|id| CssPathSelector::Id(id.to_string().into())),
    );
4
    let classes = body_node
4
        .attributes
4
        .get_key("class")
4
        .map(|s| s.split_whitespace().collect::<Vec<_>>())
4
        .unwrap_or_default();
4
    matcher.path.extend(
4
        classes
4
            .into_iter()
4
            .map(|class| CssPathSelector::Class(class.to_string().into())),
    );
4
    let matcher_hash = matcher.get_hash();
4
    let css_blocks_for_this_node = get_css_blocks(css, &matcher);
4
    if !css_blocks_for_this_node.is_empty() {
        // Track property types for the helper-const machinery, then emit the
        // matched declarations as an inline CSS string. (The old path emitted a
        // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` + `.with_inline_css_props`,
        // but that API was removed in 32d44ed8a; `.with_css(<str>)` is the
        // current equivalent and parses pseudo blocks too.)
        for css_block in &css_blocks_for_this_node {
            for declaration in css_block.block.declarations.as_ref() {
                let prop = match declaration {
                    CssDeclaration::Static(s) => s,
                    CssDeclaration::Dynamic(d) => &d.default_value,
                };
                extra_blocks.insert_from_css_property(prop);
            }
        }
        let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
        if !inline_css.is_empty() {
            let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
            let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
        }
        let _ = (&mut *css_blocks, matcher_hash); // retained for signature compat
4
    }
4
    if !body_node.children.as_ref().is_empty() {
        use azul_css::codegen::format::GetHash;
3
        let children_hash = body_node.children.as_ref().get_hash();
3
        dom_string.push_str("\r\n.with_children(vec![\r\n");
3
        for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
3
            match child {
2
                XmlNodeChild::Element(child_node) => {
2
                    let mut matcher = matcher.clone();
2
                    matcher.path.push(CssPathSelector::Children);
2
                    matcher.indices_in_parent.push(child_idx);
2
                    matcher.children_length.push(body_node.children.len());
2
                    let _ = write!(dom_string,
2
                        "{}{},\r\n",
                        t,
2
                        compile_node_to_rust_code_inner(
2
                            child_node,
2
                            component_map,
                            1,
2
                            extra_blocks,
2
                            css_blocks,
2
                            css,
2
                            matcher,
                        )?
                    );
                }
1
                XmlNodeChild::Text(text) => {
1
                    let text = text.trim();
1
                    if !text.is_empty() {
                        let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
                        let _ = write!(dom_string,
                            "{t}Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"),\r\n"
                        );
1
                    }
                }
            }
        }
3
        let _ = write!(dom_string, "\r\n{t}])");
1
    }
4
    let dom_string = dom_string.trim();
4
    Ok(dom_string.to_string())
4
}
/// Serialize the CSS blocks matched for a node into one inline CSS string for
/// `Dom::with_css(...)`. `with_css` parses via `Css::parse_inline`, which runs
/// the full selector+nesting machinery, so `:hover`/`:active`/`:focus` are
/// emitted as nested pseudo blocks and round-trip faithfully; plain rules are
/// emitted flat as `key: value;` (via `CssProperty::key()` / `value()`).
1
fn css_blocks_to_inline_string(blocks: &[CssBlock]) -> String {
    fn decls_of(block: &CssBlock) -> Vec<String> {
        block
            .block
            .declarations
            .as_ref()
            .iter()
            .map(|d| {
                let prop = match d {
                    CssDeclaration::Static(s) => s,
                    CssDeclaration::Dynamic(dy) => &dy.default_value,
                };
                format!("{}: {};", prop.key(), prop.value())
            })
            .collect()
    }
1
    let mut normal: Vec<String> = Vec::new();
1
    let mut pseudo: Vec<String> = Vec::new();
1
    for block in blocks {
        let pseudo_sel = match block.ending {
            Some(CssPathPseudoSelector::Hover) => Some(":hover"),
            Some(CssPathPseudoSelector::Active) => Some(":active"),
            Some(CssPathPseudoSelector::Focus) => Some(":focus"),
            _ => None,
        };
        match pseudo_sel {
            None => normal.extend(decls_of(block)),
            Some(sel) => pseudo.push(format!("{} {{ {} }}", sel, decls_of(block).join(" "))),
        }
    }
1
    let mut parts = normal;
1
    parts.extend(pseudo);
1
    parts.join(" ")
1
}
15
fn get_css_blocks(css: &Css, matcher: &CssMatcher) -> Vec<CssBlock> {
15
    let mut blocks = Vec::new();
15
    for css_block in css.rules.as_ref() {
        if matcher.matches(&css_block.path) {
            let mut ending = None;
            if let Some(CssPathSelector::PseudoSelector(p)) =
                css_block.path.selectors.as_ref().last()
            {
                ending = Some(p.clone());
            }
            blocks.push(CssBlock {
                ending,
                block: css_block.clone(),
            });
        }
    }
15
    blocks
15
}
9
fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
    use self::DynamicItem::{Var, Str};
9
    if input.is_empty() {
2
        String::from("AzString::from_const_str(\"\")")
7
    } else if input.len() == 1 {
        // common: there is only one "dynamic item" - skip the "format!()" macro
5
        match &input[0] {
3
            Var { name, format_spec } => {
3
                let var_name = normalize_casing(name.trim());
3
                if let Some(spec) = format_spec {
1
                    format!("format!(\"{{:{spec}}}\", {var_name}).into()")
                } else {
2
                    var_name
                }
            }
2
            Str(s) => format!("AzString::from_const_str(\"{s}\")"),
        }
    } else {
        // build a "format!("{var}, blah", var)" string
2
        let mut formatted_str = String::from("format!(\"");
2
        let mut variables = Vec::new();
8
        for item in input {
6
            match item {
3
                Var { name, format_spec } => {
3
                    let variable_name = normalize_casing(name.trim());
3
                    if let Some(spec) = format_spec {
                        let _ = write!(formatted_str, "{{{variable_name}:{spec}}}");
3
                    } else {
3
                        let _ = write!(formatted_str, "{{{variable_name}}}");
3
                    }
3
                    variables.push(variable_name.clone());
                }
3
                Str(s) => {
3
                    let s = s.replace('"', "\\\"");
3
                    formatted_str.push_str(&s);
3
                }
            }
        }
2
        formatted_str.push('\"');
2
        if !variables.is_empty() {
2
            formatted_str.push_str(", ");
2
        }
2
        formatted_str.push_str(&variables.join(", "));
2
        formatted_str.push_str(").into()");
2
        formatted_str
    }
9
}
6
fn format_args_for_rust_code(input: &str) -> String {
6
    let dynamic_str_items = split_dynamic_string(input);
6
    compile_and_format_dynamic_items(&dynamic_str_items)
6
}
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
// component_map is forwarded through the codegen recursion for parity with the
// component-expanding path; this Rust-codegen path only threads it into recursive calls.
#[allow(clippy::only_used_in_recursion)]
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
2
fn compile_node_to_rust_code_inner(
2
    node: &XmlNode,
2
    component_map: &ComponentMap,
2
    tabs: usize,
2
    extra_blocks: &mut VecContents,
2
    css_blocks: &mut BTreeMap<String, String>,
2
    css: &Css,
2
    mut matcher: CssMatcher,
2
) -> Result<String, CompileError> {
    use azul_css::css::CssDeclaration;
2
    let t = String::from("    ").repeat(tabs - 1);
2
    let t2 = String::from("    ").repeat(tabs);
2
    let component_name = normalize_casing(&node.node_type);
    // Look up the CSS NodeTypeTag
2
    let node_type_tag = tag_to_node_type_tag(&component_name);
2
    let node_type = CssPathSelector::Type(node_type_tag);
    // Emit a plain `create_node(<Tag>)` for the base node. Do NOT route through
    // the component `compile_fn`: its Rust arm bakes inline text into a
    // `.with_children(..)`, which the child-walk below would then OVERWRITE with
    // a second `.with_children(..)` — silently dropping the text on any node
    // that has BOTH text and element children. The child-walk handles ALL
    // children (text + elements) in order, so the base node must stay childless.
    // Interactive/data tags (Button/Input/…) whose NodeType carries data fall
    // back to `div`, matching the C/C++/Python walkers (`safe_container_tag`).
2
    let ctor = analyze_node_ctor(&component_name, node);
2
    let mut dom_string = ctor.render_rust().map_or_else(|| {
1
        let tag = safe_container_tag(&format!("{:?}", tag_to_node_type(&component_name)));
1
        format!("{t2}Dom::create_node(NodeType::{tag})")
1
    }, |expr| format!("{t2}{expr}"));
2
    matcher.path.push(node_type);
2
    let ids = node
2
        .attributes
2
        .get_key("id")
2
        .map(|s| s.split_whitespace().collect::<Vec<_>>())
2
        .unwrap_or_default();
2
    matcher.path.extend(
2
        ids.into_iter()
2
            .map(|id| CssPathSelector::Id(id.to_string().into())),
    );
2
    let classes = node
2
        .attributes
2
        .get_key("class")
2
        .map(|s| s.split_whitespace().collect::<Vec<_>>())
2
        .unwrap_or_default();
2
    matcher.path.extend(
2
        classes
2
            .into_iter()
2
            .map(|class| CssPathSelector::Class(class.to_string().into())),
    );
2
    let matcher_hash = matcher.get_hash();
2
    let css_blocks_for_this_node = get_css_blocks(css, &matcher);
2
    if !css_blocks_for_this_node.is_empty() {
        // Track property types for the helper-const machinery, then emit the
        // matched declarations as an inline CSS string. (The old path emitted a
        // `const CSS_MATCH_*: NodeDataInlineCssPropertyVec` + `.with_inline_css_props`,
        // but that API was removed in 32d44ed8a; `.with_css(<str>)` is the
        // current equivalent and parses pseudo blocks too.)
        for css_block in &css_blocks_for_this_node {
            for declaration in css_block.block.declarations.as_ref() {
                let prop = match declaration {
                    CssDeclaration::Static(s) => s,
                    CssDeclaration::Dynamic(d) => &d.default_value,
                };
                extra_blocks.insert_from_css_property(prop);
            }
        }
        let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
        if !inline_css.is_empty() {
            let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
            let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
        }
        let _ = (&mut *css_blocks, matcher_hash); // retained for signature compat
2
    }
2
    set_stringified_attributes(
2
        &mut dom_string,
2
        &node.attributes,
2
        &ComponentArgumentVec::new(),
2
        tabs,
    );
    // Text folded into the ctor (Tier A/C) is skipped, as is a `<caption>`
    // already injected by `create_table`.
2
    let mut caption_skipped = false;
2
    let mut children_string = node
2
        .children
2
        .as_ref()
2
        .iter()
2
        .enumerate()
2
        .filter_map(|(child_idx, c)| match c {
            XmlNodeChild::Element(child_node) => {
                if ctor.skip_caption()
                    && !caption_skipped
                    && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
                {
                    caption_skipped = true;
                    return None;
                }
                let mut matcher = matcher.clone();
                matcher.path.push(CssPathSelector::Children);
                matcher.indices_in_parent.push(child_idx);
                matcher.children_length.push(node.children.len());
                Some(compile_node_to_rust_code_inner(
                    child_node,
                    component_map,
                    tabs + 1,
                    extra_blocks,
                    css_blocks,
                    css,
                    matcher,
                ))
            }
2
            XmlNodeChild::Text(text) => {
2
                if ctor.consumes_text() {
1
                    return None;
1
                }
1
                let text = text.trim();
1
                if text.is_empty() {
                    None
                } else {
1
                    let t2 = String::from("    ").repeat(tabs);
1
                    let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
1
                    Some(Ok(format!(
1
                        "{t2}Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\")"
1
                    )))
                }
            }
2
        })
2
        .collect::<Result<Vec<_>, _>>()?
2
        .join(",\r\n");
2
    if !children_string.is_empty() {
1
        let _ = write!(dom_string,
1
            "\r\n{t2}.with_children(vec![\r\n{children_string}\r\n{t2}])"
1
        );
1
    }
2
    Ok(dom_string)
2
}
// ───────────────────────────────────────────────────────────────────────────
// Generic FLUENT DOM-builder emitter (C++ / Python).
//
// Rust has its own dedicated walker above (`compile_*_to_rust_code`). C++ and
// Python share this generic walker because their builder APIs are also fluent
// (`Dom::create_*().with_css(..).with_child(..)`); only the surface tokens
// differ, captured in `FluentSyntax`. Plain C is imperative and has its own
// walker (`compile_*_to_c_code`).
// ───────────────────────────────────────────────────────────────────────────
/// Tags with a zero-arg per-tag creator (`create_<tag>()` / `AzDom_create<Tag>()`
/// / `create_node(NodeType::<Tag>)`). Interactive / data elements (Button, Input,
/// Img, Select, Textarea, Label, A, Table, …) take constructor arguments, so an
/// exported page maps them to a plain `div` container (structure preserved; the
/// user re-wires behavior). Keep these CamelCase to match `NodeTypeTag` debug names.
const SAFE_CONTAINER_TAGS: &[&str] = &[
    // These must match the real `NodeType` Debug names exactly (the lookup below is a
    // string compare against `{:?}`). Six used to be mis-cased — "Blockquote",
    // "Colgroup", "Figcaption", "Tbody", "Tfoot", "Thead" — so those tags silently
    // degraded to "Div".
    "Abbr", "Acronym", "Address", "Article", "Aside", "B", "Bdi", "Bdo", "Big",
    "BlockQuote", "Body", "Br", "Caption", "Cite", "Code", "ColGroup", "Dd",
    "Del", "Dfn", "Dir", "Div", "Dl", "Dt", "Em", "Embed", "FigCaption",
    "Figure", "Footer", "H1", "H2", "H3", "H4", "H5", "H6", "Head", "Header",
    "Hr", "Html", "I", "Ins", "Kbd", "Li", "Link", "Main", "Map", "Mark",
    "Meta", "Nav", "Object", "Ol", "P", "Pre", "Q", "Rp", "Rt", "Rtc", "Ruby",
    "S", "Samp", "Script", "Section", "Small", "Span", "Strong", "Style", "Sub",
    "Sup", "Svg", "TBody", "Td", "TFoot", "Th", "THead", "Title", "Tr", "U",
    "Ul", "Var", "Wbr",
];
/// The CamelCase tag to actually emit a creator for: the tag itself if it has a
/// zero-arg creator, else `"Div"`.
17
fn safe_container_tag(tag_dbg: &str) -> &'static str {
894
    SAFE_CONTAINER_TAGS.iter().copied().find(|t| *t == tag_dbg).unwrap_or("Div")
17
}
// ───────────────────────────────────────────────────────────────────────────
// Semantic / accessibility-aware constructor selection.
//
// Instead of mapping every element to a plain `div`, an exported live page
// picks the *most specific* Azul constructor so the generated app keeps the
// page's semantics + accessibility tree:
//
//   • Tier A  `create_<tag>_with_text(text)` — a tag with a single text child
//             and no element children (P, Span, H1-H6, Li, Td, Code, …).
//   • Tier B  aria-only / void widgets (Details, Summary, Form, Canvas, Area,
//             …) — `create_<tag>(SmallAriaInfo::label(..))` when `aria-label`
//             is present, else `create_<tag>_no_a11y()`.
//   • Tier C  multi-arg widgets (Button, A, Label, Input, Select, Option,
//             Optgroup, Textarea, Table) — args pulled from HTML attributes.
//   • Tier D  scalar-driven widgets (Progress, Meter, Dialog) — the `*_no_a11y`
//             form with extracted numeric args (the full aria structs are
//             complex; the NoA11y form is simplest + correct).
//
// Every symbol emitted here is verified to exist in `target/codegen/azul.h` (C)
// and `azul20.hpp` (C++); anything else falls back to `safe_container_tag`
// (`div`). The four walkers share `analyze_node_ctor` and each renders the
// result with its own surface tokens.
// ───────────────────────────────────────────────────────────────────────────
/// A single positional argument of a semantic constructor. String payloads are
/// RAW — escaping happens at render time (matching the walkers).
#[derive(Debug, Clone)]
enum CtorArg {
    /// Plain string literal (`AzString` / `String` / `"…"`).
    Str(String),
    /// `SmallAriaInfo` built from an accessible label.
    Aria(String),
    /// `f32` numeric literal.
    Float(f32),
    /// `OptionString::Some(text)`.
    OptSome(String),
    /// `OptionString::None`.
    OptNone,
}
/// The constructor chosen for an element node.
enum NodeCtor {
    /// Plain container — keep each walker's existing `create_<tag>()` path.
    Plain,
    /// A specific semantic constructor.
    Semantic {
        /// Canonical CamelCase suffix after `create` / `AzDom_create`
        /// (e.g. `Button`, `ButtonNoA11y`, `PWithText`, `A`, `ANoA11y`).
        suffix: String,
        args: Vec<CtorArg>,
        /// The node's direct text is folded into the ctor — skip text children
        /// in the walk so it isn't emitted twice.
        consumes_text: bool,
        /// The table aria form injects its own `<caption>` child — drop the
        /// first literal `<caption>` element so it isn't duplicated.
        skip_caption: bool,
    },
}
/// Uppercase the first character (`button` → `Button`, `h1` → `H1`). HTML tags
/// are single lowercase tokens, so this yields the exact `AzDom_create<Suffix>`
/// spelling.
32
fn cap_first(tag: &str) -> String {
32
    let mut c = tag.chars();
32
    c.next().map_or_else(String::new, |f| f.to_uppercase().collect::<String>() + c.as_str())
32
}
/// CamelCase → `snake_case` for the C++/Python/Rust method names
/// (`ButtonNoA11y` → `button_no_a11y`, `PWithText` → `p_with_text`,
/// `ANoA11y` → `a_no_a11y`, `H1WithText` → `h1_with_text`).
29
fn camel_to_snake(s: &str) -> String {
29
    let chars: Vec<char> = s.chars().collect();
29
    let mut out = String::new();
10217
    for (i, &ch) in chars.iter().enumerate() {
10217
        if ch.is_ascii_uppercase() && i > 0 {
42
            let prev = chars[i - 1];
42
            let next_lower = chars.get(i + 1).is_some_and(char::is_ascii_lowercase);
42
            if prev.is_ascii_lowercase()
16
                || prev.is_ascii_digit()
15
                || (prev.is_ascii_uppercase() && next_lower)
41
            {
41
                out.push('_');
41
            }
10175
        }
10217
        out.extend(ch.to_lowercase());
    }
29
    out
29
}
/// Escape `\` and `"` for a double-quoted string literal.
30
fn esc_lit(s: &str) -> String {
30
    s.replace('\\', "\\\\").replace('"', "\\\"")
30
}
/// Format an `f32` as a valid float literal with a decimal point (`1` → `1.0`).
22
fn fmt_f32_lit(f: f32) -> String {
22
    let s = format!("{f}");
22
    if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") {
10
        s
    } else {
12
        format!("{s}.0")
    }
22
}
/// Joined, trimmed text of a node's *direct* text children (`"  Go  "` → `"Go"`).
29
fn node_direct_text(node: &XmlNode) -> String {
29
    node.children
29
        .as_ref()
29
        .iter()
29
        .filter_map(|c| match c {
19
            XmlNodeChild::Text(t) => {
19
                let t = t.trim();
19
                if t.is_empty() { None } else { Some(t.to_string()) }
            }
3
            XmlNodeChild::Element(_) => None,
22
        })
29
        .collect::<Vec<_>>()
29
        .join(" ")
29
}
/// Non-empty `aria-label` attribute value, if present.
29
fn node_aria_label(node: &XmlNode) -> Option<String> {
29
    node.attributes.get_key("aria-label").and_then(|v| {
5
        let v = v.as_str().trim();
5
        if v.is_empty() { None } else { Some(v.to_string()) }
5
    })
29
}
/// Attribute value, or `default` when absent.
7
fn node_attr_or(node: &XmlNode, key: &str, default: &str) -> String {
7
    node.attributes
7
        .get_key(key).map_or_else(|| default.to_string(), |v| v.as_str().to_string())
7
}
/// Attribute parsed as `f32`, or `default` when absent / unparsable.
21
fn node_attr_f32(node: &XmlNode, key: &str, default: f32) -> f32 {
21
    node.attributes
21
        .get_key(key)
21
        .and_then(|v| v.as_str().trim().parse::<f32>().ok())
21
        .unwrap_or(default)
21
}
/// Text of the node's first `<caption>` element child, if any (non-empty).
4
fn first_caption_text(node: &XmlNode) -> Option<String> {
4
    node.children.as_ref().iter().find_map(|c| match c {
3
        XmlNodeChild::Element(e) if e.node_type.as_str().eq_ignore_ascii_case("caption") => {
3
            let t = e.get_text_content();
3
            let t = t.trim();
3
            if t.is_empty() { None } else { Some(t.to_string()) }
        }
        _ => None,
3
    })
4
}
/// Tags with a single-arg `create_<tag>_with_text(text)` constructor (Tier A).
const WITH_TEXT_TAGS: &[&str] = &[
    "acronym", "b", "bdi", "bdo", "big", "blockquote", "cite", "code", "del",
    "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "ins", "kbd", "li",
    "mark", "p", "pre", "rp", "rt", "s", "samp", "small", "span", "strong",
    "style", "sub", "sup", "td", "th", "title", "u", "var",
];
/// Pick the semantic constructor for `tag` (lowercase HTML tag) + `node`.
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
25
fn analyze_node_ctor(tag: &str, node: &XmlNode) -> NodeCtor {
    // Helper for the common "no caption skip" case.
16
    fn sem(suffix: impl Into<String>, args: Vec<CtorArg>, consumes_text: bool) -> NodeCtor {
16
        NodeCtor::Semantic {
16
            suffix: suffix.into(),
16
            args,
16
            consumes_text,
16
            skip_caption: false,
16
        }
16
    }
25
    let aria = node_aria_label(node);
25
    let has_aria = aria.is_some();
25
    let label = aria.unwrap_or_default();
    // `has_only_text_children()` is also true for childless nodes; pair it with
    // `has_text` so empty elements stay plain containers.
25
    let pure_text = node.has_only_text_children();
25
    let text = node_direct_text(node);
25
    let has_text = !text.is_empty();
25
    let cap = cap_first(tag);
    // Tier A — *_with_text (single text child, no element children).
25
    if WITH_TEXT_TAGS.contains(&tag) {
8
        if pure_text && has_text {
6
            return sem(format!("{cap}WithText"), vec![CtorArg::Str(text)], true);
2
        }
2
        return NodeCtor::Plain;
17
    }
17
    match tag {
        // Tier B — aria-only / void widgets.
17
        "details" | "form" | "fieldset" | "legend" | "menu" | "output"
17
        | "datalist" | "canvas" | "audio" | "video" | "area" => {
            if has_aria {
                sem(cap, vec![CtorArg::Aria(label)], false)
            } else {
                sem(format!("{cap}NoA11y"), vec![], false)
            }
        }
        // Summary is Tier B but also has a WithText form for a single text child.
17
        "summary" => {
            if pure_text && has_text {
                if has_aria {
                    sem("SummaryWithText", vec![CtorArg::Str(text), CtorArg::Aria(label)], true)
                } else {
                    sem("SummaryWithTextNoA11y", vec![CtorArg::Str(text)], true)
                }
            } else if has_aria {
                sem("Summary", vec![CtorArg::Aria(label)], false)
            } else {
                sem("SummaryNoA11y", vec![], false)
            }
        }
        // Tier C — multi-arg widgets (args from HTML attributes).
17
        "button" => {
3
            if has_aria {
1
                sem("Button", vec![CtorArg::Str(text), CtorArg::Aria(label)], true)
            } else {
2
                sem("ButtonNoA11y", vec![CtorArg::Str(text)], true)
            }
        }
14
        "a" => {
3
            let href = node_attr_or(node, "href", "");
3
            if has_aria {
                sem("A", vec![CtorArg::Str(href), CtorArg::Str(text), CtorArg::Aria(label)], true)
            } else {
3
                let lbl = if has_text { CtorArg::OptSome(text) } else { CtorArg::OptNone };
3
                sem("ANoA11y", vec![CtorArg::Str(href), lbl], true)
            }
        }
11
        "label" => {
            let for_id = node_attr_or(node, "for", "");
            if has_aria {
                sem("Label", vec![CtorArg::Str(for_id), CtorArg::Str(text), CtorArg::Aria(label)], true)
            } else {
                sem("LabelNoA11y", vec![CtorArg::Str(for_id), CtorArg::Str(text)], true)
            }
        }
11
        "input" => {
            let ty = node_attr_or(node, "type", "text");
            let name = node_attr_or(node, "name", "");
            if has_aria {
                sem("Input", vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
            } else {
                sem("InputNoA11y", vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label)], false)
            }
        }
11
        "textarea" => {
            let name = node_attr_or(node, "name", "");
            if has_aria {
                sem("Textarea", vec![CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
            } else {
                sem("TextareaNoA11y", vec![CtorArg::Str(name), CtorArg::Str(label)], false)
            }
        }
11
        "select" => {
            let name = node_attr_or(node, "name", "");
            if has_aria {
                sem("Select", vec![CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
            } else {
                sem("SelectNoA11y", vec![CtorArg::Str(name), CtorArg::Str(label)], false)
            }
        }
11
        "option" => {
            let value = node_attr_or(node, "value", "");
            if has_aria {
                sem("Option", vec![CtorArg::Str(value), CtorArg::Str(text), CtorArg::Aria(label)], true)
            } else {
                sem("OptionNoA11y", vec![CtorArg::Str(value), CtorArg::Str(text)], true)
            }
        }
11
        "optgroup" => {
            let lbl = node_attr_or(node, "label", "");
            if has_aria {
                sem("Optgroup", vec![CtorArg::Str(lbl), CtorArg::Aria(label)], false)
            } else {
                sem("OptgroupNoA11y", vec![CtorArg::Str(lbl)], false)
            }
        }
11
        "table" => {
2
            if has_aria {
                // The aria form injects a caption child, so take the caption from
                // the literal <caption> (or the aria label) and drop the literal.
1
                let caption = first_caption_text(node).unwrap_or_else(|| label.clone());
1
                NodeCtor::Semantic {
1
                    suffix: "Table".to_string(),
1
                    args: vec![CtorArg::Str(caption), CtorArg::Aria(label)],
1
                    consumes_text: false,
1
                    skip_caption: true,
1
                }
            } else {
1
                sem("TableNoA11y", vec![], false)
            }
        }
        // Tier D — scalar-driven widgets (NoA11y form with extracted numbers).
9
        "progress" => sem(
            "ProgressNoA11y",
2
            vec![
2
                CtorArg::Float(node_attr_f32(node, "value", 0.0)),
2
                CtorArg::Float(node_attr_f32(node, "max", 1.0)),
            ],
            false,
        ),
7
        "meter" => sem(
            "MeterNoA11y",
1
            vec![
1
                CtorArg::Float(node_attr_f32(node, "value", 0.0)),
1
                CtorArg::Float(node_attr_f32(node, "min", 0.0)),
1
                CtorArg::Float(node_attr_f32(node, "max", 1.0)),
            ],
            false,
        ),
6
        "dialog" => sem("DialogNoA11y", vec![], false),
6
        _ => NodeCtor::Plain,
    }
25
}
impl CtorArg {
    /// Rust expression for this argument (`AzString::from(..)` works for both the
    /// `Into<AzString>` and the concrete `AzString` parameter forms).
19
    fn render_rust(&self) -> String {
19
        match self {
9
            Self::Str(s) => format!("AzString::from(\"{}\")", esc_lit(s)),
2
            Self::Aria(s) => format!("SmallAriaInfo::label(AzString::from(\"{}\"))", esc_lit(s)),
5
            Self::Float(f) => fmt_f32_lit(*f),
1
            Self::OptSome(s) => format!("OptionString::Some(AzString::from(\"{}\"))", esc_lit(s)),
2
            Self::OptNone => "OptionString::None".to_string(),
        }
19
    }
11
    fn render_c(&self) -> String {
11
        match self {
4
            Self::Str(s) => format!("AZ_STR(\"{}\")", esc_lit(s)),
            Self::Aria(s) => format!("AzSmallAriaInfo_label(AZ_STR(\"{}\"))", esc_lit(s)),
5
            Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
1
            Self::OptSome(s) => format!("AzOptionString_some(AZ_STR(\"{}\"))", esc_lit(s)),
1
            Self::OptNone => "AzOptionString_none()".to_string(),
        }
11
    }
4
    fn render_cpp(&self) -> String {
4
        match self {
3
            Self::Str(s) => format!("String(\"{}\")", esc_lit(s)),
            Self::Aria(s) => format!("SmallAriaInfo::label(String(\"{}\"))", esc_lit(s)),
            Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
            Self::OptSome(s) => format!("OptionString::some(String(\"{}\"))", esc_lit(s)),
1
            Self::OptNone => "OptionString::none()".to_string(),
        }
4
    }
5
    fn render_python(&self) -> String {
5
        match self {
4
            Self::Str(s) => format!("\"{}\"", esc_lit(s)),
            Self::Aria(s) => format!("azul.SmallAriaInfo.label(\"{}\")", esc_lit(s)),
            Self::Float(f) => fmt_f32_lit(*f),
            Self::OptSome(s) => format!("azul.OptionString.some(\"{}\")", esc_lit(s)),
1
            Self::OptNone => "azul.OptionString.none()".to_string(),
        }
5
    }
}
impl NodeCtor {
8
    const fn consumes_text(&self) -> bool {
8
        matches!(self, Self::Semantic { consumes_text: true, .. })
8
    }
3
    const fn skip_caption(&self) -> bool {
3
        matches!(self, Self::Semantic { skip_caption: true, .. })
3
    }
    /// `Dom::create_…(args)` for Rust, or `None` for a plain container.
13
    fn render_rust(&self) -> Option<String> {
13
        match self {
2
            Self::Plain => None,
11
            Self::Semantic { suffix, args, .. } => Some(format!(
11
                "Dom::create_{}({})",
11
                camel_to_snake(suffix),
11
                args.iter().map(CtorArg::render_rust).collect::<Vec<_>>().join(", ")
11
            )),
        }
13
    }
    /// `AzDom_create…(args)` for C, or `None` for a plain container.
6
    fn render_c(&self) -> Option<String> {
6
        match self {
2
            Self::Plain => None,
4
            Self::Semantic { suffix, args, .. } => Some(format!(
4
                "AzDom_create{}({})",
4
                suffix,
4
                args.iter().map(CtorArg::render_c).collect::<Vec<_>>().join(", ")
4
            )),
        }
6
    }
    /// Fluent `Dom::create_…` (C++) / `azul.Dom.create_…` (Python), or `None`.
8
    fn render_fluent(&self, target: &CompileTarget) -> Option<String> {
8
        match self {
1
            Self::Plain => None,
7
            Self::Semantic { suffix, args, .. } => {
7
                let snake = camel_to_snake(suffix);
7
                let (prefix, rendered) = match target {
2
                    CompileTarget::Cpp => (
2
                        format!("Dom::create_{snake}"),
2
                        args.iter().map(CtorArg::render_cpp).collect::<Vec<_>>(),
2
                    ),
3
                    CompileTarget::Python => (
3
                        format!("azul.Dom.create_{snake}"),
3
                        args.iter().map(CtorArg::render_python).collect::<Vec<_>>(),
3
                    ),
2
                    _ => return None,
                };
5
                Some(format!("{}({})", prefix, rendered.join(", ")))
            }
        }
8
    }
}
/// Per-language token hooks for the fluent walker. The `&str` args are already
/// escaped for a double-quoted string literal.
struct FluentSyntax {
    target: CompileTarget,
    /// tag debug-name (e.g. "Div") -> full create expression
    create_node: fn(&str) -> String,
    /// escaped text -> create-text expression
    create_text: fn(&str) -> String,
    /// escaped css -> `.with_css(..)` call
    with_css: fn(&str) -> String,
    /// escaped class -> `.with_class(..)` call
    with_class: fn(&str) -> String,
    /// escaped id -> `.with_id(..)` call
    with_id: fn(&str) -> String,
    /// escaped child expression -> `.with_child(..)` call (children are chained)
    with_child: fn(&str) -> String,
}
const CPP_SYNTAX: FluentSyntax = FluentSyntax {
    target: CompileTarget::Cpp,
    // Use per-tag creators (Dom::create_div(), create_p(), create_body(), …)
    // — `NodeType` is a tagged union, so `create_node` would need union
    // construction; the per-tag creators exist for every common HTML element.
1
    create_node: |tag| alloc::format!("Dom::create_{}()", tag.to_lowercase()),
    create_text: |s| alloc::format!("Dom::create_text_do_not_use_without_block_level_wrapper(String(\"{s}\"))"),
    with_css: |s| alloc::format!(".with_css(String(\"{s}\"))"),
    with_class: |s| alloc::format!(".with_class(String(\"{s}\"))"),
    with_id: |s| alloc::format!(".with_id(String(\"{s}\"))"),
1
    with_child: |c| alloc::format!(".with_child({c})"),
};
const PYTHON_SYNTAX: FluentSyntax = FluentSyntax {
    target: CompileTarget::Python,
    // Per-tag creators (azul.Dom.create_div(), …) — see CPP_SYNTAX note.
1
    create_node: |tag| alloc::format!("azul.Dom.create_{}()", tag.to_lowercase()),
    create_text: |s| alloc::format!("azul.Dom.create_text_do_not_use_without_block_level_wrapper(\"{s}\")"),
    with_css: |s| alloc::format!(".with_css(\"{s}\")"),
    with_class: |s| alloc::format!(".with_class(\"{s}\")"),
    with_id: |s| alloc::format!(".with_id(\"{s}\")"),
1
    with_child: |c| alloc::format!(".with_child({c})"),
};
/// Walk one element node, emitting a fluent create-expression for `syntax`'s
/// language. Mirrors `compile_node_to_rust_code_inner` but token-parameterized.
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
// See compile_node_to_rust_code_inner: component_map is forwarded for codegen-path parity.
#[allow(clippy::only_used_in_recursion)]
2
fn compile_node_fluent(
2
    node: &XmlNode,
2
    syntax: &FluentSyntax,
2
    component_map: &ComponentMap,
2
    css: &Css,
2
    mut matcher: CssMatcher,
2
) -> Result<String, CompileError> {
    use azul_css::css::CssDeclaration;
2
    let component_name = normalize_casing(&node.node_type);
2
    let node_type_tag = tag_to_node_type_tag(&component_name);
2
    let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
    // Base create-expression. For an exported live page every node is a plain
    // HTML element, so emit a per-tag creator directly via the language hooks
    // (universal + verified) rather than the per-component `compile_fn`, whose
    // C++/Python arms emit stale placeholder syntax (`Dom.div()` etc.).
    // Interactive/data tags (whose creators need args) fall back to `div`. Any
    // element text shows up as a Text child below and is handled there.
2
    let ctor = analyze_node_ctor(&component_name, node);
2
    let mut s = ctor.render_fluent(&syntax.target).map_or_else(|| (syntax.create_node)(safe_container_tag(&tag_dbg)), |expr| expr);
2
    matcher.path.push(CssPathSelector::Type(node_type_tag));
2
    let ids: Vec<String> = node.attributes.get_key("id")
2
        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
2
        .unwrap_or_default();
2
    matcher.path.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
2
    let classes: Vec<String> = node.attributes.get_key("class")
2
        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
2
        .unwrap_or_default();
2
    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
    // Inline CSS (matched rules -> `.with_css("..")`, pseudo blocks included).
2
    let blocks = get_css_blocks(css, &matcher);
2
    if !blocks.is_empty() {
        let inline_css = css_blocks_to_inline_string(&blocks);
        if !inline_css.is_empty() {
            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
            s.push_str(&(syntax.with_css)(&esc));
        }
2
    }
2
    for id in &ids {
        s.push_str(&(syntax.with_id)(&id.replace('\\', "\\\\").replace('"', "\\\"")));
    }
2
    for class in &classes {
        s.push_str(&(syntax.with_class)(&class.replace('\\', "\\\\").replace('"', "\\\"")));
    }
    // Children (chained `.with_child(..)`). Text folded into the ctor (Tier A/C)
    // is skipped here, as is a `<caption>` already injected by `create_table`.
2
    let mut caption_skipped = false;
2
    for (child_idx, child) in node.children.as_ref().iter().enumerate() {
2
        match child {
            XmlNodeChild::Element(child_node) => {
                if ctor.skip_caption()
                    && !caption_skipped
                    && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
                {
                    caption_skipped = true;
                    continue;
                }
                let mut m = matcher.clone();
                m.path.push(CssPathSelector::Children);
                m.indices_in_parent.push(child_idx);
                m.children_length.push(node.children.len());
                let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
                s.push_str(&(syntax.with_child)(&child_src));
            }
2
            XmlNodeChild::Text(text) => {
2
                if ctor.consumes_text() {
2
                    continue;
                }
                let text = text.trim();
                if !text.is_empty() {
                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
                    s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
                }
            }
        }
    }
2
    Ok(s)
2
}
/// Build the `<body>` render-expression for `syntax`'s language.
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
2
fn compile_body_fluent<'a>(
2
    body_node: &'a XmlNode,
2
    syntax: &FluentSyntax,
2
    component_map: &'a ComponentMap,
2
    css: &Css,
2
    mut matcher: CssMatcher,
2
) -> Result<String, CompileError> {
2
    let mut s = (syntax.create_node)("Body");
2
    matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
2
    let classes: Vec<String> = body_node.attributes.get_key("class")
2
        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
2
        .unwrap_or_default();
2
    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
2
    let blocks = get_css_blocks(css, &matcher);
2
    if !blocks.is_empty() {
        let inline_css = css_blocks_to_inline_string(&blocks);
        if !inline_css.is_empty() {
            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
            s.push_str(&(syntax.with_css)(&esc));
        }
2
    }
2
    for class in &classes {
        s.push_str(&(syntax.with_class)(&class.replace('\\', "\\\\").replace('"', "\\\"")));
    }
2
    for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
2
        match child {
2
            XmlNodeChild::Element(child_node) => {
2
                let mut m = matcher.clone();
2
                m.path.push(CssPathSelector::Children);
2
                m.indices_in_parent.push(child_idx);
2
                m.children_length.push(body_node.children.len());
2
                let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
2
                s.push_str(&(syntax.with_child)(&child_src));
            }
            XmlNodeChild::Text(text) => {
                let text = text.trim();
                if !text.is_empty() {
                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
                    s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
                }
            }
        }
    }
2
    Ok(s)
2
}
/// Parse the page's `<style>` and seed a matcher rooted at `<body>`. Shared by
/// the C++/Python/C entry points (mirrors the head of `str_to_rust_code`).
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
9
fn parse_page_style_and_body(
9
    root_nodes: &[XmlNodeChild],
9
) -> Result<(Css, &XmlNode), CompileError> {
9
    let html_node = get_html_node(root_nodes)?;
6
    let body_node = get_body_node(html_node.children.as_ref())?;
6
    let mut global_style = Css::empty();
6
    if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
6
        if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5
            let text = style_node.get_text_content();
5
            if !text.is_empty() {
1
                global_style = azul_css::parser2::new_from_str(&text).0;
4
            }
1
        }
    }
6
    global_style.sort_by_specificity();
6
    Ok((global_style, body_node))
9
}
7
fn body_matcher(body_node: &XmlNode) -> CssMatcher {
7
    CssMatcher {
7
        path: Vec::new(),
7
        indices_in_parent: vec![0],
7
        children_length: vec![body_node.children.as_ref().len()],
7
    }
7
}
/// Compile a full HTML page to a compilable **C++** Azul app.
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
/// # Errors
///
/// Returns an error if the XML cannot be parsed or compiled to C++ code.
2
pub fn str_to_cpp_code<'a>(
2
    root_nodes: &'a [XmlNodeChild],
2
    component_map: &'a ComponentMap,
2
) -> Result<String, CompileError> {
2
    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
1
    let render = compile_body_fluent(body_node, &CPP_SYNTAX, component_map, &global_style, body_matcher(body_node))?;
1
    Ok(alloc::format!(
1
        "// Auto-generated UI source code (C++). Build:\n\
1
         //   clang++ -std=c++20 -I <azul>/target/codegen main.cpp -lazul\n\
1
         #include \"azul20.hpp\"\n\
1
         using namespace azul;\n\n\
1
         struct Data {{}};\n\n\
1
         AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n    \
1
         return {render};\n}}\n\n\
1
         int main() {{\n    \
1
         RefAny data = RefAny::create(Data{{}});\n    \
1
         WindowCreateOptions window = WindowCreateOptions::create(render);\n    \
1
         App app = App::create(std::move(data), AppConfig::default_());\n    \
1
         app.run(std::move(window));\n    \
1
         return 0;\n}}\n"
1
    ))
2
}
/// Compile a full HTML page to a compilable **Python** Azul app.
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
/// # Errors
///
/// Returns an error if the XML cannot be parsed or compiled to Python code.
2
pub fn str_to_python_code<'a>(
2
    root_nodes: &'a [XmlNodeChild],
2
    component_map: &'a ComponentMap,
2
) -> Result<String, CompileError> {
2
    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
1
    let render = compile_body_fluent(body_node, &PYTHON_SYNTAX, component_map, &global_style, body_matcher(body_node))?;
1
    Ok(alloc::format!(
1
        "# Auto-generated UI source code (Python). Run: python3 main.py\n\
1
         import azul\n\n\
1
         class Data:\n    pass\n\n\
1
         def render(data, info):\n    return (\n        {}\n    )\n\n\
1
         def main():\n    \
1
         app = azul.App.create(Data(), azul.AppConfig.create())\n    \
1
         window = azul.WindowCreateOptions.create(render)\n    \
1
         app.run(window)\n\n\
1
         if __name__ == \"__main__\":\n    main()\n",
1
        render.replace("\r\n", "\n        ")
1
    ))
2
}
// ───────────────────────────────────────────────────────────────────────────
// Imperative C emitter. C has no fluent builder: each node is a statement that
// creates an `AzDom` local, applies css/class (by-value, returns), and pushes
// children via `AzDom_addChild(&parent, child)`. A recursive walk emits the
// statements bottom-up and returns the variable name holding each node.
// ───────────────────────────────────────────────────────────────────────────
/// C per-tag creator suffix: `NodeTypeTag` debug name with first char kept and
/// the rest lowercased (`Div`->`Div`, `BlockQuote`->`Blockquote`, `H1`->`H1`),
/// matching `AzDom_create<Suffix>` in azul.h.
7
fn c_creator_suffix(tag_dbg: &str) -> String {
7
    let mut chars = tag_dbg.chars();
7
    chars.next().map_or_else(|| "Div".to_string(), |first| {
6
            let rest: String = chars.as_str().to_lowercase();
6
            alloc::format!("{first}{rest}")
6
        })
7
}
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
2
fn compile_node_c(
2
    node: &XmlNode,
2
    component_map: &ComponentMap,
2
    css: &Css,
2
    mut matcher: CssMatcher,
2
    counter: &mut usize,
2
    out: &mut String,
2
) -> Result<String, CompileError> {
2
    let _ = component_map;
2
    let component_name = normalize_casing(&node.node_type);
2
    let node_type_tag = tag_to_node_type_tag(&component_name);
2
    let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
2
    let var = alloc::format!("n{}", *counter);
2
    *counter += 1;
2
    let ctor = analyze_node_ctor(&component_name, node);
2
    match ctor.render_c() {
1
        Some(expr) => { let _ = writeln!(out, "    AzDom {var} = {expr};"); },
1
        None => { let _ = writeln!(out,
1
            "    AzDom {} = AzDom_create{}();",
1
            var,
1
            c_creator_suffix(safe_container_tag(&tag_dbg))
1
        ); },
    }
2
    matcher.path.push(CssPathSelector::Type(node_type_tag));
2
    let ids: Vec<String> = node.attributes.get_key("id")
2
        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
2
        .unwrap_or_default();
2
    matcher.path.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
2
    let classes: Vec<String> = node.attributes.get_key("class")
2
        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
2
        .unwrap_or_default();
2
    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
2
    let blocks = get_css_blocks(css, &matcher);
2
    if !blocks.is_empty() {
        let inline_css = css_blocks_to_inline_string(&blocks);
        if !inline_css.is_empty() {
            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
            let _ = writeln!(out, "    {var} = AzDom_withCss({var}, AZ_STR(\"{esc}\"));");
        }
2
    }
2
    for id in &ids {
        let esc = id.replace('\\', "\\\\").replace('"', "\\\"");
        let _ = writeln!(out, "    {var} = AzDom_withId({var}, AZ_STR(\"{esc}\"));");
    }
2
    for class in &classes {
        let esc = class.replace('\\', "\\\\").replace('"', "\\\"");
        let _ = writeln!(out, "    {var} = AzDom_withClass({var}, AZ_STR(\"{esc}\"));");
    }
2
    let mut caption_skipped = false;
2
    for (child_idx, child) in node.children.as_ref().iter().enumerate() {
2
        match child {
            XmlNodeChild::Element(child_node) => {
                if ctor.skip_caption()
                    && !caption_skipped
                    && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
                {
                    caption_skipped = true;
                    continue;
                }
                let mut m = matcher.clone();
                m.path.push(CssPathSelector::Children);
                m.indices_in_parent.push(child_idx);
                m.children_length.push(node.children.len());
                let child_var = compile_node_c(child_node, component_map, css, m, counter, out)?;
                let _ = writeln!(out, "    AzDom_addChild(&{var}, {child_var});");
            }
2
            XmlNodeChild::Text(text) => {
2
                if ctor.consumes_text() {
1
                    continue;
1
                }
1
                let text = text.trim();
1
                if !text.is_empty() {
1
                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
1
                    let _ = writeln!(out,
1
                        "    AzDom_addChild(&{var}, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{esc}\")));"
1
                    );
1
                }
            }
        }
    }
2
    Ok(var)
2
}
/// Compile a full HTML page to a compilable **C** Azul app.
#[allow(clippy::result_large_err)] // returns a #[repr(C,u8)] FFI error enum; boxing a variant would break the C ABI/api.json
/// # Errors
///
/// Returns an error if the XML cannot be parsed or compiled to C code.
3
pub fn str_to_c_code<'a>(
3
    root_nodes: &'a [XmlNodeChild],
3
    component_map: &'a ComponentMap,
3
) -> Result<String, CompileError> {
3
    let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
2
    let mut body = String::new();
2
    let mut counter = 0usize;
    // Emit the body as the root node, then its children.
2
    let root = alloc::format!("n{counter}");
2
    counter += 1;
2
    let _ = writeln!(body, "    AzDom {root} = AzDom_createBody();");
2
    let mut matcher = body_matcher(body_node);
2
    matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
2
    let classes: Vec<String> = body_node.attributes.get_key("class")
2
        .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
2
        .unwrap_or_default();
2
    matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
2
    let blocks = get_css_blocks(&global_style, &matcher);
2
    if !blocks.is_empty() {
        let inline_css = css_blocks_to_inline_string(&blocks);
        if !inline_css.is_empty() {
            let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
            let _ = writeln!(body, "    {root} = AzDom_withCss({root}, AZ_STR(\"{esc}\"));");
        }
2
    }
2
    for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
2
        match child {
2
            XmlNodeChild::Element(child_node) => {
2
                let mut m = matcher.clone();
2
                m.path.push(CssPathSelector::Children);
2
                m.indices_in_parent.push(child_idx);
2
                m.children_length.push(body_node.children.len());
2
                let child_var = compile_node_c(child_node, component_map, &global_style, m, &mut counter, &mut body)?;
2
                let _ = writeln!(body, "    AzDom_addChild(&{root}, {child_var});");
            }
            XmlNodeChild::Text(text) => {
                let text = text.trim();
                if !text.is_empty() {
                    let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
                    let _ = writeln!(body,
                        "    AzDom_addChild(&{root}, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{esc}\")));"
                    );
                }
            }
        }
    }
2
    Ok(alloc::format!(
2
        "/* Auto-generated UI source code (C). Build:\n\
2
         *   clang -I <azul>/target/codegen main.c -lazul\n */\n\
2
         #include \"azul.h\"\n\
2
         #include <string.h>\n\
2
         #define AZ_STR(s) AzString_copyFromBytes((const uint8_t*)(s), 0, strlen(s))\n\n\
2
         AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n\
2
         {body}    return {root};\n}}\n\n\
2
         int main(void) {{\n    \
2
         AzString data_type = AZ_STR(\"Data\");\n    \
2
         AzRefAny data = AzRefAny_newC((AzGlVoidPtrConst){{ .ptr = NULL }}, 0, 1, 0, data_type, NULL, 0, 0);\n    \
2
         AzApp app = AzApp_create(data, AzAppConfig_create());\n    \
2
         AzWindowCreateOptions window = AzWindowCreateOptions_create(render);\n    \
2
         AzApp_run(&app, window);\n    \
2
         AzApp_delete(&app);\n    \
2
         return 0;\n}}\n"
2
    ))
3
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::dom::{Dom, NodeType};
    #[test]
1
    fn test_inline_span_parsing() {
        // This test verifies that HTML with inline spans is parsed correctly
        // The DOM structure should preserve text nodes before, inside, and after the span
1
        let html = r#"<p>Text before <span class="highlight">inline text</span> text after.</p>"#;
        // Expected DOM structure:
        // <p>
        //   ├─ TextNode: "Text before "
        //   ├─ <span class="highlight">
        //   │   └─ TextNode: "inline text"
        //   └─ TextNode: " text after."
        // For this test, we'll create the DOM structure manually
        // since we're testing the parsing logic
1
        let expected_dom = Dom::create_p().with_children(
1
            vec![
1
                Dom::create_text_do_not_use_without_block_level_wrapper("Text before "),
1
                Dom::create_node(NodeType::Span)
1
                    .with_children(vec![Dom::create_text_do_not_use_without_block_level_wrapper("inline text")].into()),
1
                Dom::create_text_do_not_use_without_block_level_wrapper(" text after."),
            ]
1
            .into(),
        );
        // Verify the structure has 3 children at the top level
1
        assert_eq!(expected_dom.children.as_ref().len(), 3);
        // Verify the middle child is a span
1
        match &expected_dom.children.as_ref()[1].root.node_type {
1
            NodeType::Span => {}
            other => panic!("Expected Span, got {:?}", other),
        }
        // Verify the span has 1 child (the text node)
1
        assert_eq!(expected_dom.children.as_ref()[1].children.as_ref().len(), 1);
1
        println!("Test passed: Inline span parsing structure is correct");
1
    }
    #[test]
1
    fn test_xml_node_structure() {
        // Test the basic XmlNode structure to ensure text content is preserved
        // Updated to use XmlNodeChild enum (Text/Element)
1
        let node = XmlNode {
1
            node_type: "p".into(),
1
            attributes: XmlAttributeMap {
1
                inner: StringPairVec::from_const_slice(&[]),
1
            },
1
            children: vec![
1
                XmlNodeChild::Text("Before ".into()),
1
                XmlNodeChild::Element(XmlNode {
1
                    node_type: "span".into(),
1
                    children: vec![XmlNodeChild::Text("inline".into())].into(),
1
                    ..Default::default()
1
                }),
1
                XmlNodeChild::Text(" after".into()),
1
            ]
1
            .into(),
1
        };
        // Verify structure
1
        assert_eq!(node.children.as_ref().len(), 3);
1
        assert_eq!(node.children.as_ref()[0].as_text(), Some("Before "));
1
        assert_eq!(
1
            node.children.as_ref()[1]
1
                .as_element()
1
                .unwrap()
1
                .node_type
1
                .as_str(),
            "span"
        );
1
        assert_eq!(node.children.as_ref()[2].as_text(), Some(" after"));
        // Verify span's child
1
        let span = node.children.as_ref()[1].as_element().unwrap();
1
        assert_eq!(span.children.as_ref().len(), 1);
1
        assert_eq!(span.children.as_ref()[0].as_text(), Some("inline"));
1
        println!("Test passed: XmlNode structure preserves text nodes correctly");
1
    }
    #[test]
1
    fn test_img_tag_becomes_image_node_with_src_tag() {
        // `<img src="cat.jpg" width="300" height="169">` must become a
        // `NodeType::Image` whose `NullImage` carries the `src` string as its
        // `tag` (so a renderer can resolve the bytes later), plus the declared
        // intrinsic size.
        use crate::resources::DecodedImage;
        use crate::window::{AzStringPair, StringPairVec};
1
        let img_node = XmlNode {
1
            node_type: "img".into(),
1
            attributes: XmlAttributeMap::from(StringPairVec::from_vec(alloc::vec![
1
                AzStringPair {
1
                    key: "src".into(),
1
                    value: "cat.jpg".into()
1
                },
1
                AzStringPair {
1
                    key: "width".into(),
1
                    value: "300".into()
1
                },
1
                AzStringPair {
1
                    key: "height".into(),
1
                    value: "169".into()
1
                },
1
            ])),
1
            children: Vec::new().into(),
1
        };
1
        let component_map = ComponentMap::default();
1
        let dom = xml_node_to_dom_fast(&img_node, &component_map, false, 0)
1
            .expect("xml_node_to_dom_fast for <img> should succeed");
1
        match dom.root.get_node_type() {
1
            NodeType::Image(image_ref) => match image_ref.as_ref().get_data() {
                DecodedImage::NullImage {
1
                    tag, width, height, ..
                } => {
1
                    assert_eq!(
1
                        core::str::from_utf8(tag).unwrap(),
                        "cat.jpg",
                        "image tag must carry the src string"
                    );
1
                    assert_eq!(*width, 300, "width attribute should set intrinsic width");
1
                    assert_eq!(*height, 169, "height attribute should set intrinsic height");
                }
                other => panic!("expected NullImage carrying the src tag, got {:?}", other),
            },
            other => panic!("expected NodeType::Image for <img>, got {:?}", other),
        }
1
        println!("Test passed: <img src=\"cat.jpg\"> -> NodeType::Image tagged \"cat.jpg\"");
1
    }
    #[test]
1
    fn test_icon_tag_builds_an_unnamed_icon_with_its_spec_as_text_child() {
        // `<icon>content_copy</icon>`: the DOM builders stay fully generic —
        // an un-named Icon node whose text child carries the spec. The icon
        // RESOLUTION pass (core::icon::resolve_icons_in_styled_dom) consumes
        // the text, exactly like a ligature icon font consumes glyph text.
1
        let text_form = XmlNode {
1
            node_type: "icon".into(),
1
            attributes: XmlAttributeMap::default(),
1
            children: alloc::vec![XmlNodeChild::Text(" content_copy ".into())].into(),
1
        };
1
        let component_map = ComponentMap::default();
1
        let dom = xml_node_to_dom_fast(&text_form, &component_map, false, 0)
1
            .expect("xml_node_to_dom_fast for <icon>text</icon> should succeed");
1
        match dom.root.get_node_type() {
1
            NodeType::Icon(name) => assert_eq!(
1
                name.as_ref().as_str(), "",
                "the builder must NOT interpret the spec — that's the resolver's job"
            ),
            other => panic!("expected NodeType::Icon for <icon>, got {other:?}"),
        }
1
        let children = dom.children.as_ref();
1
        assert_eq!(children.len(), 1, "the spec text child is preserved for the resolver");
1
        match children[0].root.get_node_type() {
1
            NodeType::Text(t) => assert_eq!(t.as_ref().as_str(), " content_copy "),
            other => panic!("expected the spec as a text child, got {other:?}"),
        }
1
    }
    #[test]
1
    fn test_tag_to_node_type_img_is_image() {
        // The bare tag mapping should also yield an Image (placeholder, empty tag).
1
        match tag_to_node_type("img") {
1
            NodeType::Image(_) => {}
            other => panic!("tag_to_node_type(\"img\") should be Image, got {:?}", other),
        }
1
    }
    /// Build a `<div>` nested `depth` levels deep, innermost first.
2
    fn nested_divs(depth: usize) -> XmlNode {
2
        let mut node = XmlNode {
2
            node_type: "div".into(),
2
            ..Default::default()
2
        };
4000
        for _ in 0..depth {
4000
            node = XmlNode {
4000
                node_type: "div".into(),
4000
                children: vec![XmlNodeChild::Element(node)].into(),
4000
                ..Default::default()
4000
            };
4000
        }
2
        node
2
    }
    /// AUDIT 2026-07-08: `extract_css_urls` used to slice the original string with
    /// a byte offset computed in a `to_lowercase()` temporary. On `'İ'` (whose
    /// lowercase is longer in bytes) that offset was misaligned. This must no
    /// longer panic and must still find the `@import` target.
    #[test]
1
    fn extract_css_urls_unicode_import_no_panic() {
1
        let mut res = Vec::new();
1
        Xml::extract_css_urls("İ@import 'x'", &mut res);
1
        assert_eq!(res.len(), 1, "should find the one @import target");
1
        assert_eq!(res[0].url.as_str(), "x");
1
    }
    /// The `url(` and `@import` scans are case-insensitive after the audit fix.
    #[test]
1
    fn extract_css_urls_is_case_insensitive() {
1
        let mut res = Vec::new();
1
        Xml::extract_css_urls("body { background: URL(http://e.com/a.png); }", &mut res);
1
        assert_eq!(res.len(), 1);
1
        assert_eq!(res[0].url.as_str(), "http://e.com/a.png");
1
        let mut res2 = Vec::new();
1
        Xml::extract_css_urls("@IMPORT \"theme.css\";", &mut res2);
1
        assert_eq!(res2.len(), 1);
1
        assert_eq!(res2[0].url.as_str(), "theme.css");
1
    }
    /// AUDIT 2026-07-08: the resource scan recurses per nesting level; deep markup
    /// must not overflow the stack (deeper-than-cap subtrees are just not scanned).
    #[test]
1
    fn scan_external_resources_deep_nesting_ok() {
1
        let xml = Xml {
1
            root: vec![XmlNodeChild::Element(nested_divs(2000))].into(),
1
        };
        // Must simply return (no stack overflow); no resources in a plain tree.
1
        drop(xml.scan_external_resources());
1
    }
    /// AUDIT 2026-07-08: the fast + tree DOM builders recurse per nesting level;
    /// deep markup must not overflow the stack (children beyond the cap are
    /// dropped, but the call returns `Ok`).
    #[test]
1
    fn xml_node_to_dom_fast_deep_nesting_ok() {
1
        let deep = nested_divs(2000);
1
        let component_map = ComponentMap::default();
1
        let dom = xml_node_to_dom_fast(&deep, &component_map, false, 0);
1
        assert!(dom.is_ok(), "deep DOM build must not overflow the stack");
1
        let mut builder = CompactDomBuilder::new();
1
        let fast = xml_node_to_fast_dom(&deep, &component_map, false, &mut builder, 0);
1
        assert!(fast.is_ok(), "deep FastDom build must not overflow the stack");
1
    }
    /// AUDIT 2026-07-08: `ComponentFieldType::parse` recurses through `Option<..>`
    /// / `Vec<..>` wrappers; an over-deep type string is rejected rather than
    /// overflowing the stack, while ordinary nesting still parses.
    #[test]
1
    fn component_field_type_parse_depth_capped() {
1
        let deep = format!("{}Bool{}", "Option<".repeat(4000), ">".repeat(4000));
1
        assert!(
1
            ComponentFieldType::parse(&deep).is_none(),
            "over-deep type string must be rejected, not overflow"
        );
1
        let shallow = format!("{}Bool{}", "Option<".repeat(8), ">".repeat(8));
1
        assert!(
1
            ComponentFieldType::parse(&shallow).is_some(),
            "ordinary nesting must still parse"
        );
1
    }
    /// AUDIT 2026-07-08: `prepare_string` now decodes the full common entity set
    /// plus numeric references, in a single pass so `&amp;` cannot double-decode.
    #[test]
1
    fn prepare_string_entity_decoding() {
1
        assert_eq!(prepare_string("a &amp; b"), "a & b");
        // `&amp;lt;` must yield the literal text "&lt;", not "<".
1
        assert_eq!(prepare_string("&amp;lt;"), "&lt;");
1
        assert_eq!(prepare_string("&quot;hi&quot;"), "\"hi\"");
1
        assert_eq!(prepare_string("&#65;&#66;"), "AB");
1
        assert_eq!(prepare_string("&#x41;"), "A");
        // Existing behavior preserved.
1
        assert_eq!(prepare_string("&lt;tag&gt;"), "<tag>");
1
    }
}
#[cfg(test)]
#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
mod autotest_generated {
    use super::*;
    use crate::dom::{NodeData, NodeType};
    use azul_css::css::{CssNthChildPattern, CssNthChildSelector};
    // ----------------------------------------------------------------- helpers
    fn attrs(kv: &[(&str, &str)]) -> XmlAttributeMap {
        XmlAttributeMap::from(StringPairVec::from_vec(
            kv.iter()
                .map(|(k, v)| AzStringPair {
                    key: AzString::from(*k),
                    value: AzString::from(*v),
                })
                .collect::<Vec<_>>(),
        ))
    }
    fn node(tag: &str, kv: &[(&str, &str)], children: Vec<XmlNodeChild>) -> XmlNode {
        XmlNode {
            node_type: tag.into(),
            attributes: attrs(kv),
            children: children.into(),
        }
    }
    fn txt(s: &str) -> XmlNodeChild {
        XmlNodeChild::Text(AzString::from(s))
    }
    fn elem(n: XmlNode) -> XmlNodeChild {
        XmlNodeChild::Element(n)
    }
    /// `<html><head><style>{css}</style></head><body>{children}</body></html>`
    fn doc(css: &str, body_children: Vec<XmlNodeChild>) -> Vec<XmlNodeChild> {
        let style = node("style", &[], vec![txt(css)]);
        let head = node("head", &[], vec![elem(style)]);
        let body = node("body", &[], body_children);
        vec![elem(node("html", &[], vec![elem(head), elem(body)]))]
    }
    fn no_args() -> ComponentArgumentVec {
        ComponentArgumentVec::from_const_slice(&[])
    }
    fn dm(name: &str, fields: Vec<ComponentDataField>) -> ComponentDataModel {
        ComponentDataModel {
            name: AzString::from(name),
            description: AzString::from_const_str(""),
            fields: fields.into(),
        }
    }
    fn user_def(css: &str, fields: Vec<ComponentDataField>) -> ComponentDef {
        ComponentDef {
            id: ComponentId::new("mylib", "widget"),
            display_name: AzString::from_const_str("Widget"),
            description: AzString::from_const_str(""),
            css: AzString::from(css),
            source: ComponentSource::UserDefined,
            data_model: dm("WidgetData", fields),
            render_fn: user_defined_render_fn,
            compile_fn: user_defined_compile_fn,
            render_fn_source: None.into(),
            compile_fn_source: None.into(),
        }
    }
    /// A string that is long enough to smoke out O(n^2) / allocation blowups but
    /// still finishes fast in a debug-profile test run.
    const LONG: usize = 200_000;
    // ================================================================
    // Xml::extract_url_value  (parser)
    // ================================================================
    #[test]
    fn extract_url_value_empty_and_whitespace() {
        assert_eq!(Xml::extract_url_value(""), None);
        assert_eq!(Xml::extract_url_value("   "), None);
        assert_eq!(Xml::extract_url_value("\t\n\r "), None);
    }
    #[test]
    fn extract_url_value_valid_minimal() {
        assert_eq!(
            Xml::extract_url_value("a.png)"),
            Some("a.png".to_string()),
            "unquoted url terminated by ')'"
        );
        assert_eq!(
            Xml::extract_url_value("\"a.png\")"),
            Some("a.png".to_string()),
            "double-quoted url"
        );
        assert_eq!(
            Xml::extract_url_value("'a.png')"),
            Some("a.png".to_string()),
            "single-quoted url"
        );
    }
    #[test]
    fn extract_url_value_leading_trailing_junk_is_trimmed() {
        // Leading whitespace is trimmed by `trim_start`, inner padding by `trim`.
        assert_eq!(
            Xml::extract_url_value("   a.png   )tail"),
            Some("a.png".to_string())
        );
    }
    #[test]
    fn extract_url_value_garbage_returns_none() {
        // Unterminated quote / no closing paren => None, never a panic.
        assert_eq!(Xml::extract_url_value("\"unterminated"), None);
        assert_eq!(Xml::extract_url_value("'unterminated"), None);
        assert_eq!(Xml::extract_url_value("no-closing-paren"), None);
        assert_eq!(Xml::extract_url_value("\u{0}\u{1}\u{7f}"), None);
    }
    #[test]
    fn extract_url_value_boundary_numbers() {
        for s in [
            "0)",
            "-0)",
            "9223372036854775807)",
            "-9223372036854775808)",
            "NaN)",
            "inf)",
            "1e400)",
        ] {
            let got = Xml::extract_url_value(s);
            assert!(got.is_some(), "numeric-looking url {s:?} is still a url");
        }
        assert_eq!(Xml::extract_url_value("0)"), Some("0".to_string()));
    }
    #[test]
    fn extract_url_value_unicode_no_panic() {
        // The ')' scan must land on a char boundary of the ORIGINAL string.
        assert_eq!(
            Xml::extract_url_value("\u{1F600}\u{0301})"),
            Some("\u{1F600}\u{0301}".to_string())
        );
        assert_eq!(Xml::extract_url_value("\"\u{130}\")"), Some("\u{130}".to_string()));
        assert_eq!(Xml::extract_url_value("\u{1F600}"), None);
    }
    #[test]
    fn extract_url_value_extremely_long_terminates() {
        let s = "a".repeat(LONG);
        assert_eq!(Xml::extract_url_value(&s), None, "no ')' anywhere => None");
        let s2 = format!("{})", "b".repeat(LONG));
        assert_eq!(Xml::extract_url_value(&s2).map(|v| v.len()), Some(LONG));
    }
    #[test]
    fn extract_url_value_nested_brackets_no_stack_overflow() {
        // Not recursive, but confirm deeply "nested" input is handled iteratively.
        let s = "(".repeat(10_000);
        assert_eq!(Xml::extract_url_value(&s), None);
        let s2 = format!("{}{}", "(".repeat(10_000), ")");
        assert_eq!(Xml::extract_url_value(&s2), Some("(".repeat(10_000)));
    }
    // ================================================================
    // Xml::extract_quoted_string  (parser)
    // ================================================================
    #[test]
    fn extract_quoted_string_empty_whitespace_garbage() {
        assert_eq!(Xml::extract_quoted_string(""), None);
        assert_eq!(Xml::extract_quoted_string("   "), None);
        assert_eq!(Xml::extract_quoted_string("\t\n"), None);
        assert_eq!(Xml::extract_quoted_string("bare"), None);
        // Leading whitespace is NOT trimmed here (unlike extract_url_value).
        assert_eq!(Xml::extract_quoted_string("  \"x\""), None);
    }
    #[test]
    fn extract_quoted_string_valid_minimal_and_empty_quotes() {
        assert_eq!(Xml::extract_quoted_string("\"x\""), Some("x".to_string()));
        assert_eq!(Xml::extract_quoted_string("'x'"), Some("x".to_string()));
        // An empty quoted string is Some(""), not None.
        assert_eq!(Xml::extract_quoted_string("\"\""), Some(String::new()));
        assert_eq!(Xml::extract_quoted_string("''"), Some(String::new()));
    }
    #[test]
    fn extract_quoted_string_unterminated_is_none() {
        assert_eq!(Xml::extract_quoted_string("\"abc"), None);
        assert_eq!(Xml::extract_quoted_string("'abc"), None);
        // Mismatched quotes do not pair up.
        assert_eq!(Xml::extract_quoted_string("\"abc'"), None);
    }
    #[test]
    fn extract_quoted_string_boundary_numbers_and_unicode() {
        assert_eq!(Xml::extract_quoted_string("\"0\""), Some("0".to_string()));
        assert_eq!(Xml::extract_quoted_string("\"-0\""), Some("-0".to_string()));
        assert_eq!(Xml::extract_quoted_string("\"NaN\""), Some("NaN".to_string()));
        assert_eq!(
            Xml::extract_quoted_string("\"\u{1F600}\u{0301}\""),
            Some("\u{1F600}\u{0301}".to_string())
        );
    }
    #[test]
    fn extract_quoted_string_extremely_long_terminates() {
        let unterminated = format!("\"{}", "x".repeat(LONG));
        assert_eq!(Xml::extract_quoted_string(&unterminated), None);
        let terminated = format!("\"{}\"", "x".repeat(LONG));
        assert_eq!(
            Xml::extract_quoted_string(&terminated).map(|s| s.len()),
            Some(LONG)
        );
    }
    // ================================================================
    // Xml::parse_srcset  (parser)
    // ================================================================
    #[test]
    fn parse_srcset_empty_and_whitespace_yield_no_urls() {
        assert!(Xml::parse_srcset("").is_empty());
        assert!(Xml::parse_srcset("   ").is_empty());
        assert!(Xml::parse_srcset("\t\n").is_empty());
        assert!(Xml::parse_srcset(",,,").is_empty(), "all-empty entries dropped");
    }
    #[test]
    fn parse_srcset_valid_minimal() {
        assert_eq!(
            Xml::parse_srcset("a.png 1x, b.png 2x"),
            vec!["a.png".to_string(), "b.png".to_string()]
        );
        // No descriptor at all is still a valid single entry.
        assert_eq!(Xml::parse_srcset("a.png"), vec!["a.png".to_string()]);
    }
    #[test]
    fn parse_srcset_garbage_and_boundary_numbers() {
        assert_eq!(Xml::parse_srcset("0, -0, NaN"), vec!["0", "-0", "NaN"]);
        // Garbage bytes still round out to "first whitespace-delimited token".
        assert_eq!(Xml::parse_srcset("\u{0}\u{7f} 1x"), vec!["\u{0}\u{7f}".to_string()]);
    }
    #[test]
    fn parse_srcset_unicode_no_panic() {
        assert_eq!(
            Xml::parse_srcset("\u{1F600}.png 1x, \u{130}.png 2x"),
            vec!["\u{1F600}.png".to_string(), "\u{130}.png".to_string()]
        );
    }
    #[test]
    fn parse_srcset_extremely_long_terminates() {
        let s = "a.png 1x,".repeat(20_000);
        assert_eq!(Xml::parse_srcset(&s).len(), 20_000);
        let one_huge = "a".repeat(LONG);
        assert_eq!(Xml::parse_srcset(&one_huge).len(), 1);
    }
    // ================================================================
    // Xml::looks_like_resource / guess_kind_from_url / guess_mime_from_url
    // ================================================================
    #[test]
    fn looks_like_resource_edges() {
        assert!(!Xml::looks_like_resource(""));
        assert!(!Xml::looks_like_resource("   "));
        assert!(!Xml::looks_like_resource("/about"));
        assert!(Xml::looks_like_resource("/a.PNG"), "case-insensitive");
        assert!(Xml::looks_like_resource("x.pdf"));
        // A query string defeats the extension check (documented consequence of
        // matching on `ends_with`).
        assert!(!Xml::looks_like_resource("x.png?v=1"));
        assert!(!Xml::looks_like_resource(&"a".repeat(LONG)));
    }
    #[test]
    fn guess_kind_from_url_covers_every_bucket() {
        use ExternalResourceKind::*;
        assert_eq!(Xml::guess_kind_from_url(""), Unknown);
        assert_eq!(Xml::guess_kind_from_url("a.PNG"), Image);
        assert_eq!(Xml::guess_kind_from_url("a.woff2"), Font);
        assert_eq!(Xml::guess_kind_from_url("a.css"), Stylesheet);
        assert_eq!(Xml::guess_kind_from_url("a.mjs"), Script);
        assert_eq!(Xml::guess_kind_from_url("a.webm"), Video);
        assert_eq!(Xml::guess_kind_from_url("a.flac"), Audio);
        assert_eq!(Xml::guess_kind_from_url("a.ico"), Icon);
        // Query strings ARE stripped here (unlike looks_like_resource).
        assert_eq!(Xml::guess_kind_from_url("a.png?v=1"), Image);
        assert_eq!(Xml::guess_kind_from_url("\u{1F600}"), Unknown);
    }
    #[test]
    fn guess_mime_from_url_empty_and_garbage() {
        assert_eq!(Xml::guess_mime_from_url("", ""), None);
        assert_eq!(Xml::guess_mime_from_url("   ", ""), None);
        assert_eq!(Xml::guess_mime_from_url("\u{0}\u{7f}", ""), None);
        assert_eq!(Xml::guess_mime_from_url("\u{1F600}", ""), None);
    }
    #[test]
    fn guess_mime_from_url_valid_minimal_and_category_fallback() {
        let m = Xml::guess_mime_from_url("a.PNG", "").expect("png is a known extension");
        assert_eq!(m.inner.as_str(), "image/png");
        let m = Xml::guess_mime_from_url("a.png?v=1", "").expect("query string stripped");
        assert_eq!(m.inner.as_str(), "image/png");
        // Unknown extension + a category hint => the category wildcard.
        let m = Xml::guess_mime_from_url("/no-ext", "image").expect("category fallback");
        assert_eq!(m.inner.as_str(), "image/*");
        // Unknown category => None.
        assert_eq!(Xml::guess_mime_from_url("/no-ext", "bogus"), None);
    }
    #[test]
    fn guess_mime_from_url_boundary_numbers_and_long() {
        assert_eq!(Xml::guess_mime_from_url("0", ""), None);
        assert_eq!(Xml::guess_mime_from_url("-0", ""), None);
        assert_eq!(Xml::guess_mime_from_url("NaN", ""), None);
        assert_eq!(Xml::guess_mime_from_url("inf", ""), None);
        let long = format!("{}.png", "a".repeat(LONG));
        assert_eq!(
            Xml::guess_mime_from_url(&long, "").map(|m| m.inner.as_str().to_string()),
            Some("image/png".to_string())
        );
    }
    // ================================================================
    // Xml::extract_css_urls / scan_node / scan_external_resources
    // ================================================================
    #[test]
    fn extract_css_urls_empty_and_garbage_no_panic() {
        let mut v = Vec::new();
        Xml::extract_css_urls("", &mut v);
        Xml::extract_css_urls("   ", &mut v);
        Xml::extract_css_urls("\u{0}\u{7f}\u{1F600}", &mut v);
        Xml::extract_css_urls("url(", &mut v);
        Xml::extract_css_urls("@import", &mut v);
        Xml::extract_css_urls("@import url(", &mut v);
        assert!(v.is_empty(), "no well-formed url in any of those inputs");
    }
    #[test]
    fn extract_css_urls_valid_minimal() {
        let mut v = Vec::new();
        Xml::extract_css_urls("a { background: url('x.png'); }", &mut v);
        assert_eq!(v.len(), 1);
        assert_eq!(v[0].url.as_str(), "x.png");
        assert_eq!(v[0].kind, ExternalResourceKind::Image);
        assert_eq!(v[0].source_attribute.as_str(), "url()");
    }
    #[test]
    fn extract_css_urls_import_is_tagged_as_stylesheet() {
        let mut v = Vec::new();
        Xml::extract_css_urls("@import url(theme.css);", &mut v);
        assert_eq!(v.len(), 1);
        assert_eq!(v[0].url.as_str(), "theme.css");
        assert_eq!(v[0].kind, ExternalResourceKind::Stylesheet);
        assert_eq!(v[0].source_attribute.as_str(), "@import");
    }
    #[test]
    fn extract_css_urls_multibyte_before_url_no_panic() {
        // ASCII-only lowercasing keeps byte offsets 1:1 with the original.
        let mut v = Vec::new();
        Xml::extract_css_urls("\u{130}\u{1F600} URL(\"a.css\") \u{0301}", &mut v);
        assert_eq!(v.len(), 1);
        assert_eq!(v[0].url.as_str(), "a.css");
    }
    #[test]
    fn extract_css_urls_extremely_long_terminates() {
        // Each iteration advances search_from past the "url(" it just matched, so
        // this must terminate (and not spin).
        let mut v = Vec::new();
        Xml::extract_css_urls(&"url(".repeat(2_000), &mut v);
        // No ')' anywhere => nothing extractable, but the scan still terminates.
        assert!(v.is_empty());
        let mut v2 = Vec::new();
        Xml::extract_css_urls(&"url(a.png)".repeat(2_000), &mut v2);
        assert_eq!(v2.len(), 2_000);
    }
    #[test]
    fn scan_node_on_empty_and_extreme_nodes_no_panic() {
        let mut v = Vec::new();
        Xml::scan_node(&XmlNode::default(), &mut v);
        Xml::scan_node(&node("", &[], vec![]), &mut v);
        Xml::scan_node(&node(&"a".repeat(10_000), &[("style", "url(x.png)")], vec![]), &mut v);
        assert_eq!(v.len(), 1, "only the inline style url()");
        assert_eq!(v[0].url.as_str(), "x.png");
    }
    #[test]
    fn scan_node_img_srcset_and_background() {
        let mut v = Vec::new();
        Xml::scan_node(
            &node(
                "IMG",
                &[("src", "a.png"), ("srcset", "b.png 1x, c.png 2x"), ("background", "d.gif")],
                vec![],
            ),
            &mut v,
        );
        let urls: Vec<&str> = v.iter().map(|r| r.url.as_str()).collect();
        assert_eq!(urls, vec!["a.png", "b.png", "c.png", "d.gif"]);
        assert!(v.iter().all(|r| r.kind == ExternalResourceKind::Image));
    }
    #[test]
    fn scan_external_resources_on_empty_document() {
        let xml = Xml {
            root: Vec::new().into(),
        };
        assert_eq!(xml.scan_external_resources().as_ref().len(), 0);
    }
    #[test]
    fn scan_external_resources_finds_every_element_kind() {
        let xml = Xml {
            root: vec![
                elem(node("img", &[("src", "i.png")], vec![])),
                elem(node("link", &[("href", "s.css"), ("rel", "stylesheet")], vec![])),
                elem(node("script", &[("src", "s.js")], vec![])),
                elem(node("video", &[("src", "v.mp4"), ("poster", "p.jpg")], vec![])),
                elem(node("audio", &[("src", "a.mp3")], vec![])),
                elem(node("a", &[("href", "f.pdf")], vec![])),
                elem(node("a", &[("href", "/page")], vec![])),
            ]
            .into(),
        };
        let res = xml.scan_external_resources();
        let mut urls: Vec<&str> = res.as_ref().iter().map(|r| r.url.as_str()).collect();
        urls.sort_unstable();
        assert_eq!(
            urls,
            vec!["a.mp3", "f.pdf", "i.png", "p.jpg", "s.css", "s.js", "v.mp4"],
            "`/page` is not a resource and must be skipped"
        );
    }
    // ================================================================
    // MimeTypeHint  (constructor)
    // ================================================================
    #[test]
    fn mime_type_hint_new_no_panic_and_fields_match_args() {
        for s in ["", "   ", "text/css", "\u{1F600}", "\u{0}"] {
            assert_eq!(MimeTypeHint::new(s).inner.as_str(), s, "new() stores verbatim");
        }
        let long = "x".repeat(LONG);
        assert_eq!(MimeTypeHint::new(&long).inner.as_str().len(), LONG);
    }
    #[test]
    fn mime_type_hint_from_extension_edges() {
        assert_eq!(
            MimeTypeHint::from_extension("").inner.as_str(),
            "application/octet-stream"
        );
        assert_eq!(
            MimeTypeHint::from_extension("PnG").inner.as_str(),
            "image/png",
            "extension match is case-insensitive"
        );
        assert_eq!(MimeTypeHint::from_extension("jpeg").inner.as_str(), "image/jpeg");
        assert_eq!(MimeTypeHint::from_extension("woff2").inner.as_str(), "font/woff2");
        assert_eq!(
            MimeTypeHint::from_extension("\u{1F600}").inner.as_str(),
            "application/octet-stream"
        );
        assert_eq!(
            MimeTypeHint::from_extension(&"z".repeat(LONG)).inner.as_str(),
            "application/octet-stream"
        );
    }
    // ================================================================
    // ComponentId  (constructor / getter)
    // ================================================================
    #[test]
    fn component_id_builtin_and_new_invariants() {
        let b = ComponentId::builtin("div");
        assert_eq!(b.collection.as_str(), "builtin");
        assert_eq!(b.name.as_str(), "div");
        let c = ComponentId::new("", "");
        assert_eq!(c.collection.as_str(), "");
        assert_eq!(c.name.as_str(), "");
        let u = ComponentId::new("\u{1F600}", "\u{130}");
        assert_eq!(u.collection.as_str(), "\u{1F600}");
        assert_eq!(u.name.as_str(), "\u{130}");
    }
    #[test]
    fn component_id_qualified_name_roundtrips_through_the_map_lookup() {
        assert_eq!(ComponentId::builtin("div").qualified_name(), "builtin:div");
        assert_eq!(ComponentId::new("", "").qualified_name(), ":");
        // A name that itself contains ':' makes the qualified name ambiguous —
        // pin the (lossy) behavior so a change is noticed.
        assert_eq!(ComponentId::new("a", "b:c").qualified_name(), "a:b:c");
    }
    // ================================================================
    // ComponentFieldTypeBox / ComponentFieldValueBox  (constructor / getter)
    // ================================================================
    #[test]
    fn component_field_type_box_new_as_ref_and_clone() {
        let b = ComponentFieldTypeBox::new(ComponentFieldType::Bool);
        assert!(!b.ptr.is_null());
        assert_eq!(*b.as_ref(), ComponentFieldType::Bool);
        let c = b.clone();
        assert_eq!(*c.as_ref(), ComponentFieldType::Bool);
        assert_ne!(b.ptr, c.ptr, "clone must deep-copy, not alias");
        assert_eq!(b, c, "PartialEq compares pointees");
        drop(c);
        assert_eq!(*b.as_ref(), ComponentFieldType::Bool, "original survives");
    }
    #[test]
    fn component_field_type_box_nested_deeply_drops_cleanly() {
        let mut t = ComponentFieldType::Bool;
        for _ in 0..64 {
            t = ComponentFieldType::OptionType(ComponentFieldTypeBox::new(t));
        }
        assert_eq!(t.format(), format!("{}Bool{}", "Option<".repeat(64), ">".repeat(64)));
        drop(t);
    }
    #[test]
    fn component_field_value_box_new_as_ref_and_clone() {
        let v = ComponentFieldValueBox::new(ComponentFieldValue::I32(i32::MIN));
        assert!(!v.ptr.is_null());
        assert_eq!(*v.as_ref(), ComponentFieldValue::I32(i32::MIN));
        let c = v.clone();
        assert_ne!(v.ptr, c.ptr);
        assert_eq!(v, c);
    }
    // ================================================================
    // ComponentFieldType::parse / parse_depth / format  (round-trip)
    // ================================================================
    #[test]
    fn component_field_type_parse_empty_whitespace_garbage() {
        assert_eq!(ComponentFieldType::parse(""), None);
        assert_eq!(ComponentFieldType::parse("   "), None);
        assert_eq!(ComponentFieldType::parse("\t\n"), None);
        assert_eq!(ComponentFieldType::parse("lowercase"), None);
        assert_eq!(ComponentFieldType::parse("\u{0}\u{7f}"), None);
        assert_eq!(ComponentFieldType::parse("Option<>"), None, "empty inner rejected");
        assert_eq!(ComponentFieldType::parse("Vec<>"), None);
        assert_eq!(ComponentFieldType::parse("Option<lowercase>"), None);
    }
    #[test]
    fn component_field_type_parse_valid_minimal_and_trimming() {
        assert_eq!(ComponentFieldType::parse("String"), Some(ComponentFieldType::String));
        assert_eq!(
            ComponentFieldType::parse("  String  "),
            Some(ComponentFieldType::String),
            "leading/trailing whitespace is trimmed"
        );
        assert_eq!(ComponentFieldType::parse("bool"), Some(ComponentFieldType::Bool));
        assert_eq!(ComponentFieldType::parse("usize"), Some(ComponentFieldType::Usize));
        assert_eq!(
            ComponentFieldType::parse("StructRef(Foo)"),
            Some(ComponentFieldType::StructRef(AzString::from("Foo")))
        );
        assert_eq!(
            ComponentFieldType::parse("EnumRef(Foo)"),
            Some(ComponentFieldType::EnumRef(AzString::from("Foo")))
        );
        assert_eq!(
            ComponentFieldType::parse("RefAny"),
            Some(ComponentFieldType::RefAny(AzString::from("")))
        );
    }
    #[test]
    fn component_field_type_parse_leading_trailing_junk_is_rejected_or_absorbed() {
        // Trailing junk after a known keyword falls through to the
        // "starts uppercase => StructRef" catch-all rather than being rejected.
        assert_eq!(
            ComponentFieldType::parse("String;garbage"),
            Some(ComponentFieldType::StructRef(AzString::from("String;garbage")))
        );
        // Lowercase junk has no uppercase first char => rejected.
        assert_eq!(ComponentFieldType::parse("string;garbage"), None);
    }
    #[test]
    fn component_field_type_parse_boundary_numbers() {
        for s in ["0", "-0", "9223372036854775807", "-9223372036854775808", "1e400", "inf"] {
            assert_eq!(
                ComponentFieldType::parse(s),
                None,
                "numeric literal {s:?} is not a type name"
            );
        }
        // ...but anything starting with an uppercase letter hits the StructRef
        // catch-all, so "NaN" parses as a struct reference rather than failing.
        assert_eq!(
            ComponentFieldType::parse("NaN"),
            Some(ComponentFieldType::StructRef(AzString::from("NaN")))
        );
    }
    #[test]
    fn component_field_type_parse_unicode_no_panic() {
        assert_eq!(ComponentFieldType::parse("\u{1F600}"), None, "emoji is not uppercase");
        // A real uppercase non-ASCII letter hits the StructRef catch-all.
        assert_eq!(
            ComponentFieldType::parse("\u{0391}bc"),
            Some(ComponentFieldType::StructRef(AzString::from("\u{0391}bc")))
        );
    }
    #[test]
    fn component_field_type_parse_depth_boundary_is_exact() {
        // MAX_TYPE_PARSE_DEPTH wrappers parse; one more is rejected.
        let ok = format!(
            "{}Bool{}",
            "Option<".repeat(MAX_TYPE_PARSE_DEPTH),
            ">".repeat(MAX_TYPE_PARSE_DEPTH)
        );
        assert!(
            ComponentFieldType::parse(&ok).is_some(),
            "exactly MAX_TYPE_PARSE_DEPTH wrappers must still parse"
        );
        let too_deep = format!(
            "{}Bool{}",
            "Option<".repeat(MAX_TYPE_PARSE_DEPTH + 1),
            ">".repeat(MAX_TYPE_PARSE_DEPTH + 1)
        );
        assert_eq!(
            ComponentFieldType::parse(&too_deep),
            None,
            "one wrapper past the cap must be rejected, not overflow"
        );
    }
    #[test]
    fn component_field_type_parse_depth_direct_call_honors_start_depth() {
        assert_eq!(
            ComponentFieldType::parse_depth("Bool", MAX_TYPE_PARSE_DEPTH),
            Some(ComponentFieldType::Bool),
            "depth == cap is still allowed"
        );
        assert_eq!(
            ComponentFieldType::parse_depth("Bool", MAX_TYPE_PARSE_DEPTH + 1),
            None
        );
        assert_eq!(
            ComponentFieldType::parse_depth("Bool", usize::MAX),
            None,
            "usize::MAX start depth must not overflow, just refuse"
        );
    }
    #[test]
    fn component_field_type_parse_nested_recursion_does_not_stack_overflow() {
        let bomb = format!("{}Bool{}", "Vec<".repeat(50_000), ">".repeat(50_000));
        assert_eq!(ComponentFieldType::parse(&bomb), None);
    }
    #[test]
    fn component_field_type_parse_extremely_long_terminates() {
        // A single 200k-char uppercase token becomes a StructRef of that name.
        let long = format!("A{}", "b".repeat(LONG));
        assert_eq!(
            ComponentFieldType::parse(&long),
            Some(ComponentFieldType::StructRef(AzString::from(long.as_str())))
        );
    }
    #[test]
    fn component_field_type_round_trip_representative() {
        let representative = vec![
            ComponentFieldType::String,
            ComponentFieldType::Bool,
            ComponentFieldType::I32,
            ComponentFieldType::I64,
            ComponentFieldType::U32,
            ComponentFieldType::U64,
            ComponentFieldType::Usize,
            ComponentFieldType::F32,
            ComponentFieldType::F64,
            ComponentFieldType::ColorU,
            ComponentFieldType::CssProperty,
            ComponentFieldType::ImageRef,
            ComponentFieldType::FontRef,
            ComponentFieldType::StyledDom,
            ComponentFieldType::StructRef(AzString::from("Foo")),
            ComponentFieldType::OptionType(ComponentFieldTypeBox::new(ComponentFieldType::Bool)),
            ComponentFieldType::VecType(ComponentFieldTypeBox::new(ComponentFieldType::I32)),
            ComponentFieldType::RefAny(AzString::from("")),
            ComponentFieldType::RefAny(AzString::from("Hint")),
            ComponentFieldType::Callback(ComponentCallbackSignature {
                return_type: AzString::from("Update"),
                args: Vec::new().into(),
            }),
        ];
        for x in representative {
            let s = x.format();
            assert_eq!(
                ComponentFieldType::parse(&s),
                Some(x.clone()),
                "parse(format({x:?})) must round-trip"
            );
        }
    }
    #[test]
    fn component_field_type_round_trip_edge_values() {
        // Empty / unicode-bearing payloads.
        for x in [
            ComponentFieldType::StructRef(AzString::from("\u{0391}\u{1F600}")),
            ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
                ComponentFieldType::VecType(ComponentFieldTypeBox::new(
                    ComponentFieldType::StructRef(AzString::from("Foo")),
                )),
            )),
        ] {
            assert_eq!(ComponentFieldType::parse(&x.format()), Some(x.clone()));
        }
    }
    #[test]
    fn component_field_type_format_is_an_idempotent_normalization() {
        // `EnumRef` and `StructRef` share the same canonical spelling, so parse()
        // collapses EnumRef -> StructRef. The normalization is still STABLE:
        // format(parse(format(x))) == format(x).
        let e = ComponentFieldType::EnumRef(AzString::from("Role"));
        let once = e.format();
        assert_eq!(once, "Role");
        let reparsed = ComponentFieldType::parse(&once).expect("parses");
        assert_eq!(
            reparsed,
            ComponentFieldType::StructRef(AzString::from("Role")),
            "EnumRef is lossy through format() — it comes back as StructRef"
        );
        assert_eq!(reparsed.format(), once, "but the normalization is idempotent");
    }
    #[test]
    fn component_field_type_display_matches_format() {
        let t = ComponentFieldType::OptionType(ComponentFieldTypeBox::new(ComponentFieldType::F64));
        assert_eq!(format!("{t}"), t.format());
        assert_eq!(format!("{t}"), "Option<F64>");
    }
    #[test]
    fn component_field_type_format_no_panic_on_empty_payloads() {
        let t = ComponentFieldType::Callback(ComponentCallbackSignature {
            return_type: AzString::from(""),
            args: Vec::new().into(),
        });
        assert_eq!(t.format(), "Callback()");
        // Callback() with an empty signature round-trips.
        assert_eq!(ComponentFieldType::parse("Callback()"), Some(t));
    }
    // ================================================================
    // ComponentFieldNamedValueVec::get_field / get_string  (parser-ish lookup)
    // ================================================================
    fn named(name: &str, v: ComponentFieldValue) -> ComponentFieldNamedValue {
        ComponentFieldNamedValue {
            name: AzString::from(name),
            value: v,
        }
    }
    fn named_vec() -> ComponentFieldNamedValueVec {
        vec![
            named("a", ComponentFieldValue::String(AzString::from("x"))),
            named("b", ComponentFieldValue::Bool(true)),
            named("", ComponentFieldValue::U64(u64::MAX)),
            named("\u{1F600}", ComponentFieldValue::String(AzString::from("emoji"))),
        ]
        .into()
    }
    #[test]
    fn named_value_vec_get_field_valid_minimal() {
        let v = named_vec();
        assert_eq!(
            v.get_field("a"),
            Some(&ComponentFieldValue::String(AzString::from("x")))
        );
        assert_eq!(v.get_field("b"), Some(&ComponentFieldValue::Bool(true)));
    }
    #[test]
    fn named_value_vec_get_field_empty_whitespace_garbage_unicode() {
        let v = named_vec();
        // An empty NAME is a legal key here — it matches the field literally named "".
        assert_eq!(v.get_field(""), Some(&ComponentFieldValue::U64(u64::MAX)));
        assert_eq!(v.get_field("   "), None);
        assert_eq!(v.get_field("\t\n"), None);
        assert_eq!(v.get_field("\u{0}\u{7f}"), None);
        assert!(v.get_field("\u{1F600}").is_some());
        assert_eq!(v.get_field(" a "), None, "no trimming: lookup is exact");
        assert_eq!(v.get_field("a;garbage"), None);
    }
    #[test]
    fn named_value_vec_get_field_on_empty_vec_and_long_key() {
        let empty = ComponentFieldNamedValueVec::from_const_slice(&[]);
        assert_eq!(empty.get_field("a"), None);
        assert_eq!(empty.get_string("a"), None);
        assert_eq!(named_vec().get_field(&"z".repeat(LONG)), None);
    }
    #[test]
    fn named_value_vec_get_string_only_matches_string_variant() {
        let v = named_vec();
        assert_eq!(v.get_string("a").map(AzString::as_str), Some("x"));
        assert_eq!(v.get_string("b"), None, "Bool is not a String");
        assert_eq!(v.get_string(""), None, "U64 is not a String");
        assert_eq!(v.get_string("missing"), None);
    }
    #[test]
    fn named_value_vec_boundary_numeric_keys() {
        let v: ComponentFieldNamedValueVec = vec![
            named("0", ComponentFieldValue::I32(0)),
            named("-0", ComponentFieldValue::I32(i32::MIN)),
            named("9223372036854775807", ComponentFieldValue::I64(i64::MAX)),
            named("NaN", ComponentFieldValue::F32(f32::NAN)),
        ]
        .into();
        assert_eq!(v.get_field("0"), Some(&ComponentFieldValue::I32(0)));
        assert_eq!(v.get_field("-0"), Some(&ComponentFieldValue::I32(i32::MIN)));
        assert_eq!(
            v.get_field("9223372036854775807"),
            Some(&ComponentFieldValue::I64(i64::MAX))
        );
        // NaN != NaN, so only check the variant, not equality.
        assert!(matches!(v.get_field("NaN"), Some(ComponentFieldValue::F32(f)) if f.is_nan()));
    }
    // ================================================================
    // ComponentDataModel::get_field / get_default_string / with_default
    // ================================================================
    fn model_with_text() -> ComponentDataModel {
        dm(
            "M",
            vec![
                data_field(
                    "text",
                    ComponentFieldType::String,
                    Some(ComponentDefaultValue::String(AzString::from("hi"))),
                    "",
                ),
                data_field("count", ComponentFieldType::U32, Some(ComponentDefaultValue::U32(3)), ""),
                data_field("required_one", ComponentFieldType::String, None, ""),
            ],
        )
    }
    #[test]
    fn data_model_get_field_valid_minimal_and_missing() {
        let m = model_with_text();
        assert!(m.get_field("text").is_some());
        assert!(m.get_field("count").is_some());
        assert!(m.get_field("missing").is_none());
        assert!(m.get_field("").is_none());
        assert!(m.get_field("   ").is_none());
        assert!(m.get_field(" text ").is_none(), "exact match, no trimming");
        assert!(m.get_field("\u{1F600}").is_none());
        assert!(m.get_field(&"z".repeat(LONG)).is_none());
    }
    #[test]
    fn data_model_get_field_on_empty_model() {
        let m = dm("Empty", Vec::new());
        assert!(m.get_field("anything").is_none());
        assert!(m.get_default_string("anything").is_none());
    }
    #[test]
    fn data_model_get_default_string_only_for_string_defaults() {
        let m = model_with_text();
        assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("hi"));
        assert_eq!(m.get_default_string("count"), None, "U32 default is not a String");
        assert_eq!(m.get_default_string("required_one"), None, "no default at all");
        assert_eq!(m.get_default_string("missing"), None);
    }
    #[test]
    fn data_model_required_flag_follows_default_presence() {
        let m = model_with_text();
        assert!(!m.get_field("text").unwrap().required);
        assert!(
            m.get_field("required_one").unwrap().required,
            "a field with no default must be marked required"
        );
    }
    #[test]
    fn data_model_with_default_overrides_and_preserves_len() {
        let m = model_with_text();
        let before = m.fields.as_ref().len();
        let m = m.with_default("text", ComponentDefaultValue::String(AzString::from("bye")));
        assert_eq!(m.fields.as_ref().len(), before, "len is preserved");
        assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("bye"));
    }
    #[test]
    fn data_model_with_default_on_missing_field_is_a_no_op() {
        let m = model_with_text();
        let m = m.with_default("nope", ComponentDefaultValue::Bool(true));
        assert_eq!(m.fields.as_ref().len(), 3);
        assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("hi"));
        assert!(m.get_field("nope").is_none(), "no field is inserted");
    }
    #[test]
    fn data_model_with_default_extreme_names_and_values_no_panic() {
        let m = model_with_text()
            .with_default("", ComponentDefaultValue::None)
            .with_default(&"z".repeat(10_000), ComponentDefaultValue::F64(f64::NAN))
            .with_default("count", ComponentDefaultValue::Usize(usize::MAX))
            .with_default("text", ComponentDefaultValue::I64(i64::MIN));
        assert_eq!(m.fields.as_ref().len(), 3);
        assert!(matches!(
            m.get_field("count").unwrap().default_value,
            OptionComponentDefaultValue::Some(ComponentDefaultValue::Usize(usize::MAX))
        ));
        assert_eq!(
            m.get_default_string("text"),
            None,
            "text is now an I64 default, no longer a String"
        );
    }
    #[test]
    fn data_model_with_default_fills_only_the_first_match() {
        // Duplicate field names: `with_default` breaks after the first hit.
        let m = dm(
            "Dup",
            vec![
                data_field("x", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from("1"))), ""),
                data_field("x", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from("2"))), ""),
            ],
        )
        .with_default("x", ComponentDefaultValue::String(AzString::from("3")));
        let vals: Vec<&str> = m
            .fields
            .as_ref()
            .iter()
            .filter_map(|f| match &f.default_value {
                OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s.as_str()),
                _ => None,
            })
            .collect();
        assert_eq!(vals, vec!["3", "2"], "only the first duplicate is overridden");
    }
    // ================================================================
    // ComponentSource / ComponentMap  (constructor / getter / lookup)
    // ================================================================
    #[test]
    fn component_source_create_is_user_defined() {
        assert_eq!(ComponentSource::create(), ComponentSource::UserDefined);
        assert_eq!(ComponentSource::default(), ComponentSource::UserDefined);
    }
    #[test]
    fn component_map_create_is_empty_and_all_lookups_return_none() {
        let m = ComponentMap::create();
        assert_eq!(m.libraries.as_ref().len(), 0);
        assert!(m.get("builtin", "div").is_none());
        assert!(m.get_unqualified("div").is_none());
        assert!(m.get_by_qualified_name("builtin:div").is_none());
        assert!(m.all_components().is_empty());
        assert!(m.get_exportable_libraries().is_empty());
    }
    #[test]
    fn component_map_with_builtin_invariants() {
        let m = ComponentMap::with_builtin();
        assert_eq!(m.libraries.as_ref().len(), 1);
        let lib = &m.libraries.as_ref()[0];
        assert_eq!(lib.name.as_str(), "builtin");
        assert!(!lib.exportable, "builtins must never be exportable");
        assert!(!lib.modifiable, "builtins must never be modifiable");
        assert_eq!(
            m.all_components().len(),
            lib.components.as_ref().len(),
            "all_components must see every registered component"
        );
        assert!(
            m.get_exportable_libraries().is_empty(),
            "the builtin library is not exportable"
        );
    }
    #[test]
    fn component_map_get_valid_minimal() {
        let m = ComponentMap::with_builtin();
        assert!(m.get("builtin", "div").is_some());
        assert!(m.get("builtin", "if").is_some());
        assert!(m.get("builtin", "for").is_some());
        assert!(m.get("builtin", "map").is_some());
        assert_eq!(
            m.get("builtin", "div").unwrap().id.qualified_name(),
            "builtin:div"
        );
    }
    #[test]
    fn component_map_get_empty_whitespace_garbage_unicode() {
        let m = ComponentMap::with_builtin();
        assert!(m.get("", "").is_none());
        assert!(m.get("builtin", "").is_none());
        assert!(m.get("", "div").is_none());
        assert!(m.get("builtin", "   ").is_none());
        assert!(m.get("builtin", " div ").is_none(), "no trimming");
        assert!(m.get("builtin", "DIV").is_none(), "lookup is case-sensitive");
        assert!(m.get("builtin", "\u{1F600}").is_none());
        assert!(m.get("builtin", "\u{0}\u{7f}").is_none());
        assert!(m.get("builtin", "div;garbage").is_none());
    }
    #[test]
    fn component_map_get_extremely_long_name_terminates() {
        let m = ComponentMap::with_builtin();
        assert!(m.get(&"a".repeat(LONG), &"b".repeat(LONG)).is_none());
        assert!(m.get_unqualified(&"b".repeat(LONG)).is_none());
        assert!(m.get_by_qualified_name(&"c".repeat(LONG)).is_none());
    }
    #[test]
    fn component_map_get_unqualified_only_searches_builtin() {
        let lib = ComponentLibrary {
            name: AzString::from("mylib"),
            version: AzString::from("1.0.0"),
            description: AzString::from(""),
            components: vec![user_def("", Vec::new())].into(),
            exportable: true,
            modifiable: true,
            data_models: Vec::new().into(),
            enum_models: Vec::new().into(),
        };
        let libs: ComponentLibraryVec = vec![lib].into();
        let m = ComponentMap::from_libraries(&libs);
        assert!(m.get("mylib", "widget").is_some());
        assert!(
            m.get_unqualified("widget").is_none(),
            "unqualified lookup must NOT reach non-builtin libraries"
        );
        assert!(m.get_by_qualified_name("mylib:widget").is_some());
        assert_eq!(m.get_exportable_libraries().len(), 1);
        assert_eq!(m.all_components().len(), 1);
    }
    #[test]
    fn component_map_get_by_qualified_name_boundary_forms() {
        let m = ComponentMap::with_builtin();
        // No colon => falls back to the builtin library.
        assert!(m.get_by_qualified_name("div").is_some());
        // Exactly one colon.
        assert!(m.get_by_qualified_name("builtin:div").is_some());
        // Splits on the FIRST colon, so the remainder (incl. colons) is the name.
        assert!(m.get_by_qualified_name("builtin:div:extra").is_none());
        assert!(m.get_by_qualified_name(":").is_none());
        assert!(m.get_by_qualified_name(":div").is_none());
        assert!(m.get_by_qualified_name("builtin:").is_none());
        assert!(m.get_by_qualified_name("").is_none());
        assert!(m.get_by_qualified_name("   ").is_none());
    }
    #[test]
    fn component_map_from_libraries_clones_without_losing_entries() {
        let src = ComponentMap::with_builtin();
        let copy = ComponentMap::from_libraries(&src.libraries);
        assert_eq!(copy.all_components().len(), src.all_components().len());
        assert!(copy.get_unqualified("div").is_some());
    }
    #[test]
    fn register_builtin_components_is_stable_across_calls() {
        let a = register_builtin_components();
        let b = register_builtin_components();
        assert_eq!(a.components.as_ref().len(), b.components.as_ref().len());
        assert!(a.components.as_ref().len() > 50);
        assert_eq!(a.name.as_str(), "builtin");
        assert_eq!(a.version.as_str(), "1.0.0");
        // Every component must be namespaced into "builtin".
        assert!(a
            .components
            .as_ref()
            .iter()
            .all(|c| c.id.collection.as_str() == "builtin"));
        assert!(a
            .components
            .as_ref()
            .iter()
            .all(|c| c.source == ComponentSource::Builtin));
    }
    // ================================================================
    // XmlNodeChild / XmlNode  (getter / predicate / constructor)
    // ================================================================
    #[test]
    fn xml_node_child_as_text_and_as_element_are_mutually_exclusive() {
        let t = txt("hello");
        assert_eq!(t.as_text(), Some("hello"));
        assert!(t.as_element().is_none());
        let e = elem(node("div", &[], vec![]));
        assert!(e.as_text().is_none());
        assert_eq!(e.as_element().map(|n| n.node_type.as_str()), Some("div"));
    }
    #[test]
    fn xml_node_child_as_text_edge_values() {
        assert_eq!(txt("").as_text(), Some(""), "an empty text node is still text");
        assert_eq!(txt("   ").as_text(), Some("   "), "no trimming in the getter");
        assert_eq!(txt("\u{1F600}\u{0301}").as_text(), Some("\u{1F600}\u{0301}"));
        assert_eq!(txt("\u{0}").as_text(), Some("\u{0}"));
    }
    #[test]
    fn xml_node_child_as_element_mut_allows_mutation_and_rejects_text() {
        let mut e = elem(node("div", &[], vec![]));
        e.as_element_mut().expect("is an element").node_type = "span".into();
        assert_eq!(e.as_element().map(|n| n.node_type.as_str()), Some("span"));
        let mut t = txt("x");
        assert!(t.as_element_mut().is_none(), "text nodes have no element");
    }
    #[test]
    fn xml_node_create_and_with_children_invariants() {
        let n = XmlNode::create("div");
        assert_eq!(n.node_type.as_str(), "div");
        assert_eq!(n.children.as_ref().len(), 0);
        assert_eq!(n.attributes.as_ref().len(), 0);
        let n = n.with_children(vec![txt("a"), elem(XmlNode::create("b"))]);
        assert_eq!(n.children.as_ref().len(), 2);
        assert_eq!(n.node_type.as_str(), "div", "tag survives with_children");
        // with_children REPLACES, it does not append.
        let n = n.with_children(Vec::new());
        assert_eq!(n.children.as_ref().len(), 0);
    }
    #[test]
    fn xml_node_create_extreme_tag_names_no_panic() {
        assert_eq!(XmlNode::create("").node_type.as_str(), "");
        assert_eq!(XmlNode::create("\u{1F600}").node_type.as_str(), "\u{1F600}");
        assert_eq!(
            XmlNode::create(&*"a".repeat(10_000)).node_type.as_str().len(),
            10_000
        );
    }
    #[test]
    fn xml_node_get_text_content_concatenates_only_direct_text() {
        let n = node(
            "p",
            &[],
            vec![
                txt("a "),
                elem(node("span", &[], vec![txt("IGNORED")])),
                txt("b"),
            ],
        );
        assert_eq!(
            n.get_text_content(),
            "a b",
            "only DIRECT text children, nested element text is not included"
        );
        assert_eq!(XmlNode::default().get_text_content(), "");
        assert_eq!(node("p", &[], vec![txt(""), txt("")]).get_text_content(), "");
    }
    #[test]
    fn xml_node_has_only_text_children_true_false_and_empty() {
        assert!(
            XmlNode::default().has_only_text_children(),
            "vacuously true for a childless node — callers must pair this with a text check"
        );
        assert!(node("p", &[], vec![txt("a"), txt("b")]).has_only_text_children());
        assert!(!node("p", &[], vec![txt("a"), elem(XmlNode::create("b"))]).has_only_text_children());
        assert!(!node("p", &[], vec![elem(XmlNode::create("b"))]).has_only_text_children());
    }
    // ================================================================
    // get_html_node / get_body_node / find_node_by_type / find_attribute
    // ================================================================
    #[test]
    fn get_html_node_empty_input_is_no_html_node() {
        assert_eq!(get_html_node(&[]), Err(DomXmlParseError::NoHtmlNode));
        assert_eq!(get_html_node(&[txt("just text")]), Err(DomXmlParseError::NoHtmlNode));
        assert_eq!(
            get_html_node(&[elem(XmlNode::create("div"))]),
            Err(DomXmlParseError::NoHtmlNode)
        );
    }
    #[test]
    fn get_html_node_valid_minimal_and_case_insensitive() {
        let roots = vec![elem(XmlNode::create("HTML"))];
        assert!(get_html_node(&roots).is_ok(), "tag casing is normalized");
    }
    #[test]
    fn get_html_node_rejects_multiple_roots() {
        let roots = vec![elem(XmlNode::create("html")), elem(XmlNode::create("html"))];
        assert_eq!(
            get_html_node(&roots),
            Err(DomXmlParseError::MultipleHtmlRootNodes)
        );
    }
    #[test]
    fn get_body_node_empty_and_missing() {
        assert_eq!(get_body_node(&[]), Err(DomXmlParseError::NoBodyInHtml));
        assert_eq!(
            get_body_node(&[elem(XmlNode::create("head"))]),
            Err(DomXmlParseError::NoBodyInHtml)
        );
    }
    #[test]
    fn get_body_node_direct_and_nested() {
        let direct = vec![elem(XmlNode::create("body"))];
        assert!(get_body_node(&direct).is_ok());
        // Malformed markup: <body> buried inside <head>.
        let nested = vec![elem(node(
            "head",
            &[],
            vec![elem(node("div", &[], vec![elem(XmlNode::create("BODY"))]))],
        ))];
        assert!(
            get_body_node(&nested).is_ok(),
            "the recursive fallback finds a nested body"
        );
    }
    /// Build a `<div>` chain `depth` levels deep with `inner` at the bottom.
    fn wrap_divs(depth: usize, inner: XmlNode) -> XmlNode {
        let mut n = inner;
        for _ in 0..depth {
            n = node("div", &[], vec![elem(n)]);
        }
        n
    }
    #[test]
    fn get_body_node_deep_nesting_is_depth_capped_not_stack_overflowing() {
        // Body sits just inside the cap => found.
        let ok = vec![elem(wrap_divs(
            MAX_XML_NESTING_DEPTH - 2,
            XmlNode::create("body"),
        ))];
        assert!(get_body_node(&ok).is_ok(), "body within the depth cap is found");
        // Body far below the cap => reported missing, but MUST NOT overflow.
        let too_deep = vec![elem(wrap_divs(2_000, XmlNode::create("body")))];
        assert_eq!(
            get_body_node(&too_deep),
            Err(DomXmlParseError::NoBodyInHtml),
            "past MAX_XML_NESTING_DEPTH the search gives up instead of crashing"
        );
    }
    #[test]
    fn find_node_by_type_empty_garbage_unicode() {
        assert!(find_node_by_type(&[], "div").is_none());
        assert!(find_node_by_type(&[txt("x")], "div").is_none());
        let roots = vec![elem(XmlNode::create("div"))];
        assert!(find_node_by_type(&roots, "").is_none());
        assert!(find_node_by_type(&roots, "   ").is_none());
        assert!(find_node_by_type(&roots, "\u{1F600}").is_none());
        assert!(find_node_by_type(&roots, &"z".repeat(LONG)).is_none());
        // Tag matching is ASCII case-insensitive (HTML tags are), so "DIV" matches a
        // <div> node. (It used to compare against the normalize_casing'd tag, which was
        // case-sensitive on the needle AND mangled uppercase tags to "d_i_v".)
        assert!(find_node_by_type(&roots, "DIV").is_some());
    }
    #[test]
    fn find_node_by_type_valid_minimal_and_recursive() {
        let roots = vec![elem(node(
            "html",
            &[],
            vec![elem(node("head", &[], vec![elem(XmlNode::create("STYLE"))]))],
        ))];
        assert!(find_node_by_type(&roots, "html").is_some());
        assert!(
            find_node_by_type(&roots, "style").is_some(),
            "search recurses into the whole tree"
        );
        assert!(find_node_by_type(&roots, "body").is_none());
    }
    #[test]
    fn find_node_by_type_prefers_the_shallowest_match() {
        let roots = vec![
            elem(node("div", &[], vec![elem(XmlNode::create("span"))])),
            elem(node("span", &[("id", "shallow")], vec![])),
        ];
        let found = find_node_by_type(&roots, "span").expect("found");
        assert_eq!(
            found.attributes.get_key("id").map(AzString::as_str),
            Some("shallow"),
            "direct children are scanned before recursing"
        );
    }
    #[test]
    fn find_attribute_valid_minimal_and_missing() {
        let n = node("a", &[("href", "x"), ("id", "y")], vec![]);
        assert_eq!(find_attribute(&n, "href").map(AzString::as_str), Some("x"));
        assert_eq!(find_attribute(&n, "id").map(AzString::as_str), Some("y"));
        assert!(find_attribute(&n, "missing").is_none());
        assert!(find_attribute(&n, "").is_none());
        assert!(find_attribute(&n, "   ").is_none());
        assert!(find_attribute(&XmlNode::default(), "href").is_none());
        assert!(find_attribute(&n, &"z".repeat(LONG)).is_none());
    }
    #[test]
    fn find_attribute_compares_against_the_normalized_key() {
        // `normalize_casing` turns `aria-label` into `aria_label`, so callers must
        // pass the NORMALIZED spelling — the raw HTML spelling does not match.
        let n = node("button", &[("aria-label", "Save")], vec![]);
        assert_eq!(
            find_attribute(&n, "aria_label").map(AzString::as_str),
            Some("Save")
        );
        assert!(
            find_attribute(&n, "aria-label").is_none(),
            "the hyphenated spelling never matches (keys are normalized first)"
        );
    }
    #[test]
    fn find_attribute_unicode_keys_no_panic() {
        let n = node("div", &[("\u{130}", "v"), ("\u{1F600}", "w")], vec![]);
        assert!(find_attribute(&n, "\u{1F600}").is_some(), "emoji keys pass through");
        // 'İ' lowercases to 2 chars, so the normalized key is not 'İ'.
        assert!(find_attribute(&n, "\u{130}").is_none());
    }
    // ================================================================
    // normalize_casing
    // ================================================================
    #[test]
    fn normalize_casing_documented_forms() {
        assert_eq!(normalize_casing("abcDef"), "abc_def");
        assert_eq!(normalize_casing("AbcDef"), "abc_def");
        assert_eq!(normalize_casing("abc-def"), "abc_def");
        assert_eq!(normalize_casing("abc_def"), "abc_def");
    }
    #[test]
    fn normalize_casing_edge_inputs() {
        assert_eq!(normalize_casing(""), "");
        assert_eq!(normalize_casing("---"), "", "separators alone produce no words");
        assert_eq!(normalize_casing("___"), "");
        assert_eq!(normalize_casing("A"), "a");
        assert_eq!(
            normalize_casing("ABC"),
            "a_b_c",
            "every uppercase char starts a new word"
        );
        assert_eq!(normalize_casing("h1"), "h1");
        assert_eq!(normalize_casing("   "), "   ", "whitespace is not a separator");
    }
    #[test]
    fn normalize_casing_unicode_and_long_no_panic() {
        // 'İ' (U+0130) lowercases to TWO chars — the fn must not slice bytes.
        assert_eq!(normalize_casing("\u{130}"), "i\u{307}");
        assert_eq!(normalize_casing("\u{1F600}"), "\u{1F600}");
        assert_eq!(normalize_casing(&"a".repeat(50_000)).len(), 50_000);
        // 50k uppercase chars => 50k single-char words joined by '_'.
        assert_eq!(normalize_casing(&"A".repeat(50_000)).len(), 50_000 * 2 - 1);
    }
    // ================================================================
    // get_item / get_item_internal
    // ================================================================
    #[test]
    fn get_item_empty_hierarchy_returns_the_root() {
        let mut root = node("div", &[("id", "root")], vec![]);
        let got = get_item(&[], &mut root).expect("empty hierarchy => root");
        assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("root"));
    }
    #[test]
    fn get_item_walks_nested_elements() {
        let mut root = node(
            "a",
            &[],
            vec![elem(node("b", &[], vec![elem(node("c", &[("id", "deep")], vec![]))]))],
        );
        let got = get_item(&[0, 0], &mut root).expect("a > b > c");
        assert_eq!(got.node_type.as_str(), "c");
        assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("deep"));
    }
    #[test]
    fn get_item_out_of_bounds_and_text_nodes_return_none() {
        let mut root = node("a", &[], vec![txt("hello"), elem(XmlNode::create("b"))]);
        assert!(get_item(&[5], &mut root).is_none(), "out of bounds => None");
        assert!(
            get_item(&[usize::MAX], &mut root).is_none(),
            "usize::MAX index must not panic"
        );
        assert!(
            get_item(&[0], &mut root).is_none(),
            "index 0 is a TEXT node — not traversable"
        );
        assert!(get_item(&[1], &mut root).is_some());
        assert!(
            get_item(&[1, 0], &mut root).is_none(),
            "descending past a leaf => None"
        );
    }
    #[test]
    fn get_item_deep_hierarchy_terminates() {
        // 400 levels: deep, but bounded by the (short) hierarchy vec, not by markup.
        let mut root = wrap_divs(400, node("div", &[("id", "bottom")], vec![]));
        let path = vec![0usize; 400];
        let got = get_item(&path, &mut root).expect("reaches the bottom");
        assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("bottom"));
    }
    // ================================================================
    // decode_numeric_entity  (parser)
    // ================================================================
    #[test]
    fn decode_numeric_entity_valid_minimal() {
        assert_eq!(decode_numeric_entity("#65"), Some('A'));
        assert_eq!(decode_numeric_entity("#x41"), Some('A'));
        assert_eq!(decode_numeric_entity("#X41"), Some('A'), "uppercase X accepted");
        assert_eq!(decode_numeric_entity("#x1F600"), Some('\u{1F600}'));
    }
    #[test]
    fn decode_numeric_entity_empty_whitespace_garbage() {
        assert_eq!(decode_numeric_entity(""), None);
        assert_eq!(decode_numeric_entity("   "), None);
        assert_eq!(decode_numeric_entity("\t\n"), None);
        assert_eq!(decode_numeric_entity("amp"), None, "named entity is not numeric");
        assert_eq!(decode_numeric_entity("#"), None);
        assert_eq!(decode_numeric_entity("#x"), None);
        assert_eq!(decode_numeric_entity("#zz"), None);
        assert_eq!(decode_numeric_entity("# 65"), None);
        assert_eq!(decode_numeric_entity("#65junk"), None);
        assert_eq!(decode_numeric_entity("\u{1F600}"), None);
    }
    #[test]
    fn decode_numeric_entity_boundary_code_points() {
        assert_eq!(decode_numeric_entity("#0"), Some('\u{0}'), "NUL is a valid char");
        assert_eq!(decode_numeric_entity("#x10FFFF"), Some('\u{10FFFF}'), "max scalar");
        assert_eq!(
            decode_numeric_entity("#x110000"),
            None,
            "one past the max scalar value"
        );
        assert_eq!(
            decode_numeric_entity("#xD800"),
            None,
            "a lone surrogate is not a char"
        );
        assert_eq!(
            decode_numeric_entity("#4294967295"),
            None,
            "u32::MAX is not a scalar value"
        );
        assert_eq!(
            decode_numeric_entity("#4294967296"),
            None,
            "one past u32::MAX must not wrap — it must fail to parse"
        );
        assert_eq!(decode_numeric_entity("#-1"), None, "negative is rejected by u32");
        assert_eq!(
            decode_numeric_entity("#xFFFFFFFFFFFF"),
            None,
            "hex overflow is rejected, not truncated"
        );
    }
    #[test]
    fn decode_numeric_entity_extremely_long_terminates() {
        let s = format!("#{}", "9".repeat(LONG));
        assert_eq!(s.len(), LONG + 1);
        assert_eq!(decode_numeric_entity(&s), None, "overflows u32 => None");
    }
    // ================================================================
    // decode_entities / prepare_string
    // ================================================================
    #[test]
    fn decode_entities_leaves_unrecognized_sequences_verbatim() {
        assert_eq!(decode_entities(""), "");
        assert_eq!(decode_entities("&"), "&", "a bare '&' at EOF must not panic");
        assert_eq!(decode_entities("&;"), "&;", "empty entity body is not decoded");
        assert_eq!(decode_entities("&bogus;"), "&bogus;");
        assert_eq!(decode_entities("&nbsp;"), "&nbsp;", "&nbsp; is deliberately kept");
        // A ';' further away than MAX_ENTITY_BODY is not treated as an entity end.
        assert_eq!(
            decode_entities("&averyveryverylongbody;"),
            "&averyveryverylongbody;"
        );
    }
    #[test]
    fn decode_entities_single_pass_prevents_double_decoding() {
        assert_eq!(decode_entities("&amp;lt;"), "&lt;", "&amp; must not re-open an entity");
        assert_eq!(decode_entities("&lt;&gt;&amp;&quot;&apos;"), "<>&\"'");
    }
    #[test]
    fn decode_entities_unicode_and_long_input_terminate() {
        assert_eq!(decode_entities("\u{1F600}\u{0301}\u{130}"), "\u{1F600}\u{0301}\u{130}");
        // NOTE: each '&' triggers a `find(';')` over the whole remaining suffix, so
        // this is quadratic in the number of '&'. Keep the input modest.
        let amps = "&".repeat(20_000);
        assert_eq!(decode_entities(&amps).len(), 20_000);
    }
    #[test]
    fn prepare_string_empty_and_whitespace() {
        assert_eq!(prepare_string(""), "");
        assert_eq!(prepare_string("   "), "");
        assert_eq!(prepare_string("\t\n\r  \n"), "");
    }
    #[test]
    fn prepare_string_nbsp_becomes_a_space_that_survives_trim() {
        assert_eq!(prepare_string("&nbsp;"), " ");
        assert_eq!(prepare_string("a&nbsp;b"), "a b");
    }
    #[test]
    fn prepare_string_collapses_a_blank_line_into_a_single_return() {
        assert_eq!(prepare_string("a\n\nb"), "a\nb");
        assert_eq!(prepare_string("a\n\n\n\nb"), "a\nb", "runs of blanks collapse");
    }
    #[test]
    fn prepare_string_unicode_and_long_input_no_panic() {
        assert_eq!(prepare_string("\u{1F600}"), "\u{1F600}");
        assert_eq!(prepare_string("  \u{130}\u{0301}  "), "\u{130}\u{0301}");
        assert_eq!(prepare_string(&"x".repeat(LONG)).len(), LONG);
    }
    /// BUG (reported): a soft-wrapped multi-line text node loses the word break
    /// on the FINAL line, because `prepare_string` skips the joining space when
    /// `line_idx == line_len - 1`. HTML must collapse the newline into a space.
    #[test]
    fn prepare_string_joins_wrapped_lines_with_a_space() {
        assert_eq!(
            prepare_string("Hello\nworld"),
            "Hello world",
            "a single newline between words must collapse to a space, not vanish"
        );
        assert_eq!(prepare_string("a\nb\nc"), "a b c");
    }
    // ================================================================
    // parse_bool  (parser)
    // ================================================================
    #[test]
    fn parse_bool_valid_minimal_and_everything_else_is_none() {
        assert_eq!(parse_bool("true"), Some(true));
        assert_eq!(parse_bool("false"), Some(false));
        for s in [
            "", "   ", "\t\n", " true", "true ", "TRUE", "True", "FALSE", "1", "0", "-0", "yes",
            "no", "NaN", "inf", "9223372036854775807", "\u{1F600}", "true;garbage",
        ] {
            assert_eq!(parse_bool(s), None, "{s:?} must not parse as a bool");
        }
        assert_eq!(parse_bool(&"a".repeat(LONG)), None);
    }
    // ================================================================
    // split_dynamic_string / format_args_dynamic
    // ================================================================
    fn args(kv: &[(&str, &str)]) -> ComponentArgumentVec {
        kv.iter()
            .map(|(n, t)| ComponentArgument {
                name: AzString::from(*n),
                arg_type: AzString::from(*t),
            })
            .collect::<Vec<_>>()
            .into()
    }
    #[test]
    fn split_dynamic_string_empty_and_plain() {
        assert!(split_dynamic_string("").is_empty());
        assert_eq!(
            split_dynamic_string("abc"),
            vec![DynamicItem::Str("abc".to_string())]
        );
    }
    #[test]
    fn split_dynamic_string_format_spec_is_split_on_the_first_colon() {
        assert_eq!(
            split_dynamic_string("{a:?}"),
            vec![DynamicItem::Var {
                name: "a".to_string(),
                format_spec: Some("?".to_string()),
            }]
        );
        assert_eq!(
            split_dynamic_string("{a:x:y}"),
            vec![DynamicItem::Var {
                name: "a".to_string(),
                format_spec: Some("x:y".to_string()),
            }],
            "only the FIRST colon separates name from spec"
        );
    }
    #[test]
    fn split_dynamic_string_unterminated_var_stays_literal() {
        // No closing brace => the scan runs to EOF and the text stays a literal.
        assert_eq!(
            split_dynamic_string("{unterminated"),
            vec![DynamicItem::Str("{unterminated".to_string())]
        );
        // A whitespace inside the braces aborts the variable scan.
        assert_eq!(
            split_dynamic_string("{a b}"),
            vec![DynamicItem::Str("{a b}".to_string())]
        );
    }
    #[test]
    fn split_dynamic_string_unicode_and_long_input_terminate() {
        assert_eq!(
            split_dynamic_string("\u{1F600}{v}\u{130}"),
            vec![
                DynamicItem::Str("\u{1F600}".to_string()),
                DynamicItem::Var {
                    name: "v".to_string(),
                    format_spec: None
                },
                DynamicItem::Str("\u{130}".to_string()),
            ]
        );
        // The failed-variable scan advances the cursor by the amount it scanned,
        // so this stays linear rather than quadratic.
        let bomb = "{a".repeat(50_000);
        let out = split_dynamic_string(&bomb);
        assert!(out.len() <= 2, "a never-closed variable collapses, got {}", out.len());
        let braces = "{".repeat(100_000);
        assert!(split_dynamic_string(&braces).len() <= 1);
    }
    #[test]
    fn format_args_dynamic_documented_example() {
        let vars = args(&[("a", "value1"), ("b", "value2")]);
        assert_eq!(
            format_args_dynamic("hello {a}, {b}{{ {c} }}", &vars),
            "hello value1, value2{ {c} }"
        );
    }
    #[test]
    fn format_args_dynamic_unknown_var_is_preserved_verbatim() {
        let empty = no_args();
        assert_eq!(format_args_dynamic("{c}", &empty), "{c}");
        assert_eq!(
            format_args_dynamic("{c:?}", &empty),
            "{c:?}",
            "the format spec is re-attached when the var is unresolved"
        );
        // Escaped braces round-trip to themselves.
        assert_eq!(format_args_dynamic("{{}}", &empty), "{{}}");
    }
    #[test]
    fn format_args_dynamic_variable_names_are_normalized() {
        let vars = args(&[("my_var", "V")]);
        assert_eq!(format_args_dynamic("{myVar}", &vars), "V", "camelCase is normalized");
        assert_eq!(format_args_dynamic("{ my-var }", &vars), "{ my-var }",
            "whitespace inside the braces aborts the variable scan entirely");
        assert_eq!(format_args_dynamic("{my-var}", &vars), "V");
    }
    #[test]
    fn format_args_dynamic_edge_values_no_panic() {
        let empty = no_args();
        assert_eq!(format_args_dynamic("", &empty), "");
        assert_eq!(format_args_dynamic("   ", &empty), "   ");
        assert_eq!(format_args_dynamic("\u{1F600}", &empty), "\u{1F600}");
        assert_eq!(format_args_dynamic(&"x".repeat(50_000), &empty).len(), 50_000);
    }
    #[test]
    fn combine_and_replace_dynamic_items_on_empty_input() {
        assert_eq!(combine_and_replace_dynamic_items(&[], &no_args()), "");
    }
    // ================================================================
    // compile_and_format_dynamic_items / format_args_for_rust_code
    // ================================================================
    #[test]
    fn format_args_for_rust_code_empty_and_single_items() {
        assert_eq!(format_args_for_rust_code(""), "AzString::from_const_str(\"\")");
        assert_eq!(format_args_for_rust_code("hi"), "AzString::from_const_str(\"hi\")");
        assert_eq!(format_args_for_rust_code("{a}"), "a", "a lone var becomes a bare ident");
        assert_eq!(
            format_args_for_rust_code("{a:?}"),
            "format!(\"{:?}\", a).into()"
        );
    }
    #[test]
    fn format_args_for_rust_code_multi_item_builds_a_format_call() {
        assert_eq!(
            format_args_for_rust_code("x={a} y={b}"),
            "format!(\"x={a} y={b}\", a, b).into()"
        );
    }
    #[test]
    fn format_args_for_rust_code_escapes_quotes_in_literals() {
        let out = format_args_for_rust_code("say \"hi\" {a}");
        assert!(
            out.contains("say \\\"hi\\\""),
            "double quotes must be escaped for the emitted literal, got {out}"
        );
    }
    #[test]
    fn compile_and_format_dynamic_items_edge_values() {
        assert_eq!(
            compile_and_format_dynamic_items(&[]),
            "AzString::from_const_str(\"\")"
        );
        assert_eq!(
            compile_and_format_dynamic_items(&[DynamicItem::Str(String::new())]),
            "AzString::from_const_str(\"\")"
        );
        assert_eq!(
            compile_and_format_dynamic_items(&[DynamicItem::Var {
                name: "  spaced  ".to_string(),
                format_spec: None,
            }]),
            "spaced",
            "the var name is trimmed + normalized"
        );
    }
    // ================================================================
    // cap_first / camel_to_snake / esc_lit / c_creator_suffix
    // ================================================================
    #[test]
    fn cap_first_edge_inputs() {
        assert_eq!(cap_first(""), "", "empty input must not panic");
        assert_eq!(cap_first("h1"), "H1");
        assert_eq!(cap_first("button"), "Button");
        assert_eq!(cap_first("A"), "A", "already-uppercase is idempotent");
        assert_eq!(cap_first("\u{1F600}x"), "\u{1F600}x", "emoji has no uppercase form");
        // 'ß' uppercases to TWO chars — the fn must not assume 1:1.
        assert_eq!(cap_first("\u{df}x"), "SSx");
        assert_eq!(cap_first(&"a".repeat(10_000)).len(), 10_000);
    }
    #[test]
    fn camel_to_snake_documented_forms() {
        assert_eq!(camel_to_snake("ButtonNoA11y"), "button_no_a11y");
        assert_eq!(camel_to_snake("PWithText"), "p_with_text");
        assert_eq!(camel_to_snake("ANoA11y"), "a_no_a11y");
        assert_eq!(camel_to_snake("H1WithText"), "h1_with_text");
        assert_eq!(camel_to_snake("Div"), "div");
    }
    #[test]
    fn camel_to_snake_edge_inputs() {
        assert_eq!(camel_to_snake(""), "");
        assert_eq!(camel_to_snake("A"), "a");
        assert_eq!(camel_to_snake("AB"), "ab", "an all-caps run is not split");
        assert_eq!(camel_to_snake("ABc"), "a_bc", "a caps run splits before the last cap");
        assert_eq!(camel_to_snake("\u{1F600}"), "\u{1F600}");
        assert_eq!(camel_to_snake(&"a".repeat(10_000)).len(), 10_000);
    }
    #[test]
    fn esc_lit_escapes_backslash_before_quote() {
        assert_eq!(esc_lit(""), "");
        assert_eq!(esc_lit("plain"), "plain");
        assert_eq!(esc_lit("a\"b"), "a\\\"b");
        assert_eq!(esc_lit("a\\b"), "a\\\\b");
        // The backslash pass must run FIRST so an escaped quote is not double-escaped.
        assert_eq!(esc_lit("\\\""), "\\\\\\\"");
        assert_eq!(esc_lit("\u{1F600}"), "\u{1F600}");
    }
    #[test]
    fn c_creator_suffix_edge_inputs() {
        assert_eq!(c_creator_suffix(""), "Div", "empty debug name falls back to Div");
        assert_eq!(c_creator_suffix("Div"), "Div");
        assert_eq!(c_creator_suffix("H1"), "H1");
        assert_eq!(c_creator_suffix("BlockQuote"), "Blockquote");
        assert_eq!(c_creator_suffix("FigCaption"), "Figcaption");
        assert_eq!(c_creator_suffix("\u{1F600}"), "\u{1F600}");
    }
    // ================================================================
    // safe_container_tag
    // ================================================================
    #[test]
    fn safe_container_tag_falls_back_to_div_for_arg_taking_widgets() {
        assert_eq!(safe_container_tag(""), "Div");
        assert_eq!(safe_container_tag("Div"), "Div");
        assert_eq!(safe_container_tag("Span"), "Span");
        assert_eq!(safe_container_tag("H1"), "H1");
        // Interactive / arg-taking elements deliberately degrade to a container.
        assert_eq!(safe_container_tag("Button"), "Div");
        assert_eq!(safe_container_tag("Input"), "Div");
        assert_eq!(safe_container_tag("A"), "Div");
        assert_eq!(safe_container_tag("\u{1F600}"), "Div");
        assert_eq!(safe_container_tag(&"z".repeat(10_000)), "Div");
    }
    /// BUG (reported): `SAFE_CONTAINER_TAGS` is documented as holding the
    /// `NodeType`/`NodeTypeTag` **debug names**, and `safe_container_tag` compares
    /// against `format!("{:?}", tag_to_node_type(tag))`. But six entries are spelled
    /// with a different inner capitalization than the actual variant, so the
    /// comparison never matches and `<blockquote>`/`<figcaption>`/`<thead>`/`<tbody>`
    /// /`<tfoot>`/`<colgroup>` silently compile down to a plain `div`.
    #[test]
    fn safe_container_tag_matches_the_real_nodetype_debug_names() {
        for tag in [
            "blockquote",
            "figcaption",
            "thead",
            "tbody",
            "tfoot",
            "colgroup",
        ] {
            let dbg = format!("{:?}", tag_to_node_type(tag));
            assert_ne!(
                safe_container_tag(&dbg),
                "Div",
                "<{tag}> (NodeType debug name {dbg:?}) is a pure container and must keep \
                 its own creator instead of degrading to a div"
            );
        }
    }
    // ================================================================
    // fmt_f32_lit  (numeric)
    // ================================================================
    #[test]
    fn fmt_f32_lit_zero_and_negative() {
        assert_eq!(fmt_f32_lit(0.0), "0.0", "an integral value gains a decimal point");
        assert_eq!(fmt_f32_lit(-0.0), "-0.0");
        assert_eq!(fmt_f32_lit(-1.0), "-1.0");
        assert_eq!(fmt_f32_lit(1.5), "1.5");
        assert_eq!(fmt_f32_lit(-1.5), "-1.5");
    }
    #[test]
    fn fmt_f32_lit_min_max_stay_parseable_float_literals() {
        for f in [f32::MAX, f32::MIN, f32::MIN_POSITIVE, f32::EPSILON] {
            let s = fmt_f32_lit(f);
            assert_eq!(
                s.parse::<f32>(),
                Ok(f),
                "{f:e} must round-trip through its emitted literal ({s})"
            );
        }
    }
    #[test]
    fn fmt_f32_lit_nan_inf_produce_a_defined_result_and_do_not_panic() {
        // NOTE: these are NOT valid Rust/C float literals — a page with
        // `<progress value="NaN">` emits `create_progress_no_a11y(NaN, 1.0)`.
        // Pinned so a fix (e.g. clamping to 0.0) is a visible change.
        assert_eq!(fmt_f32_lit(f32::NAN), "NaN");
        assert_eq!(fmt_f32_lit(f32::INFINITY), "inf");
        assert_eq!(fmt_f32_lit(f32::NEG_INFINITY), "-inf");
    }
    // ================================================================
    // node_direct_text / node_aria_label / node_attr_or / node_attr_f32
    // first_caption_text
    // ================================================================
    #[test]
    fn node_direct_text_trims_and_skips_elements() {
        assert_eq!(node_direct_text(&XmlNode::default()), "");
        assert_eq!(node_direct_text(&node("p", &[], vec![txt("  Go  ")])), "Go");
        assert_eq!(
            node_direct_text(&node(
                "p",
                &[],
                vec![txt("a"), elem(node("b", &[], vec![txt("IGNORED")])), txt("b")]
            )),
            "a b",
            "direct text children are joined with a single space"
        );
        assert_eq!(
            node_direct_text(&node("p", &[], vec![txt("   "), txt("\t\n")])),
            "",
            "whitespace-only children are dropped"
        );
    }
    #[test]
    fn node_aria_label_ignores_empty_and_whitespace() {
        assert_eq!(node_aria_label(&XmlNode::default()), None);
        assert_eq!(node_aria_label(&node("b", &[("aria-label", "")], vec![])), None);
        assert_eq!(node_aria_label(&node("b", &[("aria-label", "   ")], vec![])), None);
        assert_eq!(
            node_aria_label(&node("b", &[("aria-label", "  Save  ")], vec![])),
            Some("Save".to_string())
        );
    }
    #[test]
    fn node_attr_or_returns_the_default_when_absent() {
        let n = node("a", &[("href", "/x"), ("empty", "")], vec![]);
        assert_eq!(node_attr_or(&n, "href", "FALLBACK"), "/x");
        assert_eq!(node_attr_or(&n, "missing", "FALLBACK"), "FALLBACK");
        assert_eq!(
            node_attr_or(&n, "empty", "FALLBACK"),
            "",
            "a present-but-empty attribute wins over the default"
        );
        assert_eq!(node_attr_or(&XmlNode::default(), "x", ""), "");
    }
    #[test]
    fn node_attr_f32_zero_negative_and_defaults() {
        let n = node(
            "meter",
            &[("zero", "0"), ("negzero", "-0"), ("neg", "-2.5"), ("pad", "  1.5  ")],
            vec![],
        );
        assert_eq!(node_attr_f32(&n, "zero", 9.0), 0.0);
        assert!(node_attr_f32(&n, "negzero", 9.0).is_sign_negative());
        assert_eq!(node_attr_f32(&n, "neg", 9.0), -2.5);
        assert_eq!(node_attr_f32(&n, "pad", 9.0), 1.5, "the value is trimmed first");
        assert_eq!(node_attr_f32(&n, "missing", 9.0), 9.0);
    }
    #[test]
    fn node_attr_f32_unparsable_falls_back_and_min_max_saturate() {
        let n = node(
            "meter",
            &[
                ("junk", "abc"),
                ("empty", ""),
                ("huge", "1e400"),
                ("tiny", "-1e400"),
                ("big", "340282350000000000000000000000000000000"),
            ],
            vec![],
        );
        assert_eq!(node_attr_f32(&n, "junk", 7.0), 7.0);
        assert_eq!(node_attr_f32(&n, "empty", 7.0), 7.0);
        assert!(
            node_attr_f32(&n, "huge", 7.0).is_infinite(),
            "an out-of-range literal saturates to inf, it does not panic"
        );
        assert_eq!(node_attr_f32(&n, "tiny", 7.0), f32::NEG_INFINITY);
        assert_eq!(node_attr_f32(&n, "big", 7.0), f32::MAX);
    }
    #[test]
    fn node_attr_f32_accepts_nan_and_inf_spellings() {
        // Rust's f32 FromStr accepts "NaN"/"inf", so hostile markup can inject a
        // non-finite value straight into codegen (see fmt_f32_lit above).
        let n = node("progress", &[("value", "NaN"), ("max", "inf")], vec![]);
        assert!(node_attr_f32(&n, "value", 0.0).is_nan());
        assert_eq!(node_attr_f32(&n, "max", 1.0), f32::INFINITY);
    }
    #[test]
    fn node_attr_f32_nan_default_is_returned_verbatim() {
        assert!(node_attr_f32(&XmlNode::default(), "x", f32::NAN).is_nan());
        assert_eq!(node_attr_f32(&XmlNode::default(), "x", f32::INFINITY), f32::INFINITY);
    }
    #[test]
    fn first_caption_text_edges() {
        assert_eq!(first_caption_text(&XmlNode::default()), None);
        assert_eq!(
            first_caption_text(&node("table", &[], vec![elem(node("caption", &[], vec![]))])),
            None,
            "an empty caption yields None"
        );
        assert_eq!(
            first_caption_text(&node(
                "table",
                &[],
                vec![elem(node("CAPTION", &[], vec![txt("  Hi  ")]))]
            )),
            Some("Hi".to_string()),
            "the tag match is ASCII-case-insensitive and the text is trimmed"
        );
    }
    // ================================================================
    // analyze_node_ctor / CtorArg / NodeCtor
    // ================================================================
    #[test]
    fn analyze_node_ctor_plain_for_unknown_and_empty_tags() {
        assert!(matches!(analyze_node_ctor("div", &XmlNode::default()), NodeCtor::Plain));
        assert!(matches!(analyze_node_ctor("", &XmlNode::default()), NodeCtor::Plain));
        assert!(matches!(
            analyze_node_ctor("\u{1F600}", &XmlNode::default()),
            NodeCtor::Plain
        ));
        let plain = analyze_node_ctor("div", &XmlNode::default());
        assert_eq!(plain.render_rust(), None);
        assert_eq!(plain.render_c(), None);
        assert_eq!(plain.render_fluent(&CompileTarget::Cpp), None);
        assert!(!plain.consumes_text());
        assert!(!plain.skip_caption());
    }
    #[test]
    fn analyze_node_ctor_with_text_tier_requires_actual_text() {
        // Empty <p> stays a plain container (has_only_text_children() is vacuously
        // true for a childless node, so `has_text` is the real gate).
        assert!(matches!(analyze_node_ctor("p", &XmlNode::default()), NodeCtor::Plain));
        // <p> with an element child is not "pure text" either.
        let mixed = node("p", &[], vec![txt("a"), elem(XmlNode::create("span"))]);
        assert!(matches!(analyze_node_ctor("p", &mixed), NodeCtor::Plain));
        let pure = node("p", &[], vec![txt("  Hello  ")]);
        let ctor = analyze_node_ctor("p", &pure);
        assert!(ctor.consumes_text(), "the text is folded into the constructor");
        assert_eq!(
            ctor.render_rust().as_deref(),
            Some("Dom::create_p_with_text(AzString::from(\"Hello\"))")
        );
        assert_eq!(
            ctor.render_c().as_deref(),
            Some("AzDom_createPWithText(AZ_STR(\"Hello\"))")
        );
        assert_eq!(
            ctor.render_fluent(&CompileTarget::Python).as_deref(),
            Some("azul.Dom.create_p_with_text(\"Hello\")")
        );
    }
    #[test]
    fn analyze_node_ctor_button_with_and_without_aria() {
        let plain_btn = node("button", &[], vec![txt("Go")]);
        assert_eq!(
            analyze_node_ctor("button", &plain_btn).render_rust().as_deref(),
            Some("Dom::create_button_no_a11y(AzString::from(\"Go\"))")
        );
        let aria_btn = node("button", &[("aria-label", "Save")], vec![txt("Go")]);
        assert_eq!(
            analyze_node_ctor("button", &aria_btn).render_rust().as_deref(),
            Some(
                "Dom::create_button(AzString::from(\"Go\"), \
                 SmallAriaInfo::label(AzString::from(\"Save\")))"
            )
        );
    }
    #[test]
    fn analyze_node_ctor_escapes_quotes_and_backslashes_in_text() {
        let btn = node("button", &[], vec![txt("say \"hi\"\\now")]);
        let rust = analyze_node_ctor("button", &btn).render_rust().expect("semantic");
        assert!(
            rust.contains("say \\\"hi\\\"\\\\now"),
            "quotes and backslashes must be escaped for the literal, got {rust}"
        );
    }
    #[test]
    fn analyze_node_ctor_anchor_uses_option_string_when_it_has_no_text() {
        let bare = node("a", &[], vec![]);
        assert_eq!(
            analyze_node_ctor("a", &bare).render_rust().as_deref(),
            Some("Dom::create_a_no_a11y(AzString::from(\"\"), OptionString::None)"),
            "a missing href defaults to an empty string, missing text to OptionString::None"
        );
        let full = node("a", &[("href", "/x")], vec![txt("Home")]);
        assert_eq!(
            analyze_node_ctor("a", &full).render_rust().as_deref(),
            Some(
                "Dom::create_a_no_a11y(AzString::from(\"/x\"), \
                 OptionString::Some(AzString::from(\"Home\")))"
            )
        );
        assert_eq!(
            analyze_node_ctor("a", &full).render_c().as_deref(),
            Some("AzDom_createANoA11y(AZ_STR(\"/x\"), AzOptionString_some(AZ_STR(\"Home\")))")
        );
    }
    #[test]
    fn analyze_node_ctor_table_aria_form_skips_the_literal_caption() {
        let t = node(
            "table",
            &[("aria-label", "Prices")],
            vec![elem(node("caption", &[], vec![txt("Q1")]))],
        );
        let ctor = analyze_node_ctor("table", &t);
        assert!(ctor.skip_caption(), "the aria form injects its own caption child");
        assert_eq!(
            ctor.render_rust().as_deref(),
            Some(
                "Dom::create_table(AzString::from(\"Q1\"), \
                 SmallAriaInfo::label(AzString::from(\"Prices\")))"
            )
        );
        let plain = analyze_node_ctor("table", &node("table", &[], vec![]));
        assert!(!plain.skip_caption());
        assert_eq!(plain.render_rust().as_deref(), Some("Dom::create_table_no_a11y()"));
    }
    #[test]
    fn analyze_node_ctor_scalar_widgets_use_defaults_and_emit_float_literals() {
        let p = node("progress", &[], vec![]);
        assert_eq!(
            analyze_node_ctor("progress", &p).render_rust().as_deref(),
            Some("Dom::create_progress_no_a11y(0.0, 1.0)"),
            "missing value/max fall back to 0.0 / 1.0 as float literals"
        );
        let m = node("meter", &[("value", "5"), ("min", "-1"), ("max", "10")], vec![]);
        assert_eq!(
            analyze_node_ctor("meter", &m).render_c().as_deref(),
            Some("AzDom_createMeterNoA11y(5.0f, -1.0f, 10.0f)")
        );
    }
    /// Non-finite attribute values flow straight into the emitted literal. Pinned
    /// so that a fix (clamping / rejecting them) shows up as a change.
    #[test]
    fn analyze_node_ctor_non_finite_attributes_emit_non_finite_literals() {
        let p = node("progress", &[("value", "NaN"), ("max", "inf")], vec![]);
        let rust = analyze_node_ctor("progress", &p).render_rust().expect("semantic");
        assert_eq!(rust, "Dom::create_progress_no_a11y(NaN, inf)");
    }
    #[test]
    fn ctor_arg_render_targets_are_distinct() {
        let s = CtorArg::Str("a\"b".to_string());
        assert_eq!(s.render_rust(), "AzString::from(\"a\\\"b\")");
        assert_eq!(s.render_c(), "AZ_STR(\"a\\\"b\")");
        assert_eq!(s.render_cpp(), "String(\"a\\\"b\")");
        assert_eq!(s.render_python(), "\"a\\\"b\"");
        assert_eq!(CtorArg::OptNone.render_rust(), "OptionString::None");
        assert_eq!(CtorArg::OptNone.render_c(), "AzOptionString_none()");
        assert_eq!(CtorArg::OptNone.render_cpp(), "OptionString::none()");
        assert_eq!(CtorArg::OptNone.render_python(), "azul.OptionString.none()");
        assert_eq!(CtorArg::Float(0.0).render_rust(), "0.0");
        assert_eq!(CtorArg::Float(0.0).render_c(), "0.0f");
        assert_eq!(CtorArg::Float(f32::NAN).render_c(), "NaNf");
    }
    #[test]
    fn node_ctor_render_fluent_returns_none_for_non_fluent_targets() {
        let ctor = analyze_node_ctor("p", &node("p", &[], vec![txt("x")]));
        assert!(ctor.render_fluent(&CompileTarget::Rust).is_none());
        assert!(ctor.render_fluent(&CompileTarget::C).is_none());
        assert!(ctor.render_fluent(&CompileTarget::Cpp).is_some());
        assert!(ctor.render_fluent(&CompileTarget::Python).is_some());
    }
    // ================================================================
    // format_component_args / compile_component / compile_components
    // ================================================================
    #[test]
    fn format_component_args_empty_and_ordering() {
        assert_eq!(format_component_args(&no_args()), "");
        // Args are sorted DESCENDING by their rendered "name: type" string.
        assert_eq!(
            format_component_args(&args(&[("a", "u32"), ("b", "String")])),
            "b: String, a: u32"
        );
    }
    #[test]
    fn format_component_args_edge_values_no_panic() {
        let a = args(&[("", ""), ("\u{1F600}", "\u{130}")]);
        let out = format_component_args(&a);
        assert!(out.contains(": "), "still emits `name: type` pairs, got {out:?}");
    }
    #[test]
    fn compile_component_emits_a_render_fn() {
        let ca = ComponentArguments {
            args: args(&[("count", "u32")]),
            accepts_text: false,
        };
        let out = compile_component("MyWidget", &ca, "Dom::create_div()");
        assert!(out.contains("pub fn render(count: u32) -> Dom {"), "got:\n{out}");
        assert!(out.contains("#[inline]"), "a one-line body is inlined");
    }
    #[test]
    fn compile_component_accepts_text_prepends_the_text_param() {
        let ca = ComponentArguments {
            args: args(&[("count", "u32")]),
            accepts_text: true,
        };
        let out = compile_component("my-widget", &ca, "Dom::create_div()");
        assert!(
            out.contains("pub fn render(text: AzString, count: u32) -> Dom {"),
            "got:\n{out}"
        );
        let ca_no_args = ComponentArguments {
            args: no_args(),
            accepts_text: true,
        };
        let out = compile_component("w", &ca_no_args, "Dom::create_div()");
        assert!(
            out.contains("pub fn render(text: AzString) -> Dom {"),
            "no trailing comma when there are no extra args, got:\n{out}"
        );
    }
    #[test]
    fn compile_component_empty_name_and_body_no_panic() {
        let ca = ComponentArguments::default();
        let out = compile_component("", &ca, "");
        assert!(out.contains("pub fn render() -> Dom {"), "got:\n{out}");
    }
    #[test]
    fn compile_components_of_an_empty_list_is_empty() {
        assert_eq!(compile_components(Vec::new()), "");
    }
    // ================================================================
    // parse_svg_float / parse_svg_points  (parser / numeric)
    // ================================================================
    #[test]
    fn parse_svg_float_none_empty_whitespace_garbage() {
        assert_eq!(parse_svg_float(None), None);
        let empty = AzString::from("");
        assert_eq!(parse_svg_float(Some(&empty)), None);
        let ws = AzString::from("   \t\n");
        assert_eq!(parse_svg_float(Some(&ws)), None);
        let junk = AzString::from("10px");
        assert_eq!(parse_svg_float(Some(&junk)), None, "units are not stripped");
        let uni = AzString::from("\u{1F600}");
        assert_eq!(parse_svg_float(Some(&uni)), None);
    }
    #[test]
    fn parse_svg_float_valid_and_boundary_numbers() {
        let padded = AzString::from("  1.5  ");
        assert_eq!(parse_svg_float(Some(&padded)), Some(1.5), "value is trimmed");
        let zero = AzString::from("0");
        assert_eq!(parse_svg_float(Some(&zero)), Some(0.0));
        let negzero = AzString::from("-0");
        assert!(parse_svg_float(Some(&negzero)).unwrap().is_sign_negative());
        let huge = AzString::from("1e400");
        assert!(
            parse_svg_float(Some(&huge)).unwrap().is_infinite(),
            "overflow saturates to inf rather than erroring"
        );
        let nan = AzString::from("NaN");
        assert!(parse_svg_float(Some(&nan)).unwrap().is_nan());
        let inf = AzString::from("-inf");
        assert_eq!(parse_svg_float(Some(&inf)), Some(f32::NEG_INFINITY));
    }
    #[test]
    fn parse_svg_points_rejects_degenerate_input() {
        assert!(parse_svg_points("", false).is_none());
        assert!(parse_svg_points("   ", false).is_none());
        assert!(parse_svg_points("garbage", false).is_none());
        assert!(parse_svg_points("1 2", false).is_none(), "a single point is not a line");
        assert!(
            parse_svg_points("1 2 3", false).is_none(),
            "an odd coordinate count is rejected"
        );
        assert!(parse_svg_points("\u{1F600}", false).is_none());
    }
    #[test]
    fn parse_svg_points_valid_minimal_and_close() {
        let open = parse_svg_points("0,0 10,0", false).expect("two points => one line");
        assert_eq!(open.rings.as_ref().len(), 1);
        assert_eq!(open.rings.as_ref()[0].items.as_ref().len(), 1);
        // `close` adds a segment back to the first point when it differs.
        let closed = parse_svg_points("0,0 10,0 10,10", true).expect("triangle");
        assert_eq!(
            closed.rings.as_ref()[0].items.as_ref().len(),
            3,
            "2 segments + 1 closing segment"
        );
        // Already-closed rings do not get a duplicate closing segment.
        let already = parse_svg_points("0,0 10,0 0,0", true).expect("closed ring");
        assert_eq!(already.rings.as_ref()[0].items.as_ref().len(), 2);
    }
    #[test]
    fn parse_svg_points_skips_unparsable_tokens_and_handles_boundaries() {
        // Unparsable coordinates are silently dropped, which can shift the pairing.
        let p = parse_svg_points("0,0 junk 10,0", false).expect("junk token dropped");
        assert_eq!(p.rings.as_ref()[0].items.as_ref().len(), 1);
        let nan = parse_svg_points("NaN,0 1,1", false).expect("NaN is a parseable f32");
        assert_eq!(nan.rings.as_ref()[0].items.as_ref().len(), 1);
    }
    #[test]
    fn parse_svg_points_extremely_long_terminates() {
        let pts = "1,2 ".repeat(20_000);
        let p = parse_svg_points(&pts, false).expect("20k points");
        assert_eq!(p.rings.as_ref()[0].items.as_ref().len(), 19_999);
    }
    // ================================================================
    // CompactDomBuilder  (constructor / numeric)
    // ================================================================
    #[test]
    fn compact_dom_builder_new_and_with_capacity_start_empty() {
        for b in [
            CompactDomBuilder::new(),
            CompactDomBuilder::with_capacity(0),
            CompactDomBuilder::with_capacity(1),
            CompactDomBuilder::with_capacity(4096),
        ] {
            let fd = b.finish();
            assert_eq!(fd.node_hierarchy.as_ref().len(), 0);
            assert_eq!(fd.node_data.as_ref().len(), 0);
            assert_eq!(fd.css.as_ref().len(), 0);
        }
        assert_eq!(
            CompactDomBuilder::default().finish().node_data.as_ref().len(),
            0
        );
    }
    #[test]
    fn compact_dom_builder_close_node_on_an_empty_stack_is_a_no_op() {
        let mut b = CompactDomBuilder::new();
        b.close_node();
        b.close_node();
        assert_eq!(b.finish().node_hierarchy.as_ref().len(), 0, "no panic, no nodes");
    }
    #[test]
    fn compact_dom_builder_keeps_hierarchy_and_data_parallel() {
        let mut b = CompactDomBuilder::new();
        b.open_node(NodeData::create_node(NodeType::Html));
        b.add_leaf(NodeData::create_text_do_not_use_without_block_level_wrapper("a"));
        b.add_leaf(NodeData::create_text_do_not_use_without_block_level_wrapper("b"));
        b.close_node();
        let fd = b.finish();
        assert_eq!(fd.node_data.as_ref().len(), 3);
        assert_eq!(
            fd.node_hierarchy.as_ref().len(),
            fd.node_data.as_ref().len(),
            "the two arenas must stay the same length"
        );
    }
    #[test]
    fn compact_dom_builder_unclosed_node_still_finishes() {
        let mut b = CompactDomBuilder::new();
        b.open_node(NodeData::create_node(NodeType::Div));
        // Deliberately NOT closed.
        let fd = b.finish();
        assert_eq!(fd.node_hierarchy.as_ref().len(), 1);
        assert_eq!(
            fd.node_hierarchy.as_ref()[0].last_child, 0,
            "last_child stays unset when close_node() is never called"
        );
    }
    #[test]
    fn compact_dom_builder_add_css_accepts_zero_and_usize_max_node_ids() {
        let mut b = CompactDomBuilder::new();
        b.add_css(0, Css::empty());
        b.add_css(usize::MAX, Css::empty());
        let fd = b.finish();
        assert_eq!(fd.css.as_ref().len(), 2);
        assert_eq!(fd.css.as_ref()[0].node_id, 0);
        assert_eq!(
            fd.css.as_ref()[1].node_id,
            usize::MAX,
            "an out-of-range node id is stored verbatim (no bounds check, no panic)"
        );
    }
    // ================================================================
    // xml_node_to_dom_fast / xml_node_to_fast_dom  (numeric: depth)
    // ================================================================
    #[test]
    fn xml_node_to_dom_fast_depth_zero_builds_children() {
        let map = ComponentMap::default();
        let n = node("div", &[], vec![txt("hi"), elem(XmlNode::create("span"))]);
        let dom = xml_node_to_dom_fast(&n, &map, false, 0).expect("ok");
        assert_eq!(dom.children.as_ref().len(), 2);
    }
    #[test]
    fn xml_node_to_dom_fast_at_and_past_the_depth_cap_truncates_instead_of_panicking() {
        let map = ComponentMap::default();
        let n = node("div", &[], vec![txt("hi")]);
        let at_cap = xml_node_to_dom_fast(&n, &map, false, MAX_XML_NESTING_DEPTH).expect("ok");
        assert!(
            at_cap.children.as_ref().is_empty(),
            "at the cap the node is emitted without children"
        );
        let saturated = xml_node_to_dom_fast(&n, &map, false, usize::MAX)
            .expect("usize::MAX depth must not overflow when computing depth + 1");
        assert!(saturated.children.as_ref().is_empty());
        let below = xml_node_to_dom_fast(&n, &map, false, MAX_XML_NESTING_DEPTH - 1).expect("ok");
        assert_eq!(below.children.as_ref().len(), 1, "one below the cap still recurses");
    }
    #[test]
    fn xml_node_to_fast_dom_at_the_depth_cap_still_emits_the_node() {
        let map = ComponentMap::default();
        let n = node("div", &[], vec![txt("hi")]);
        let mut b = CompactDomBuilder::new();
        xml_node_to_fast_dom(&n, &map, false, &mut b, usize::MAX).expect("no overflow");
        let fd = b.finish();
        assert_eq!(
            fd.node_data.as_ref().len(),
            1,
            "the node itself is still opened+closed, only its children are dropped"
        );
        let mut b2 = CompactDomBuilder::new();
        xml_node_to_fast_dom(&n, &map, false, &mut b2, 0).expect("ok");
        assert_eq!(b2.finish().node_data.as_ref().len(), 2, "node + text child");
    }
    #[test]
    fn apply_xml_node_attributes_extreme_tabindex_does_not_panic() {
        let map = ComponentMap::default();
        for v in [
            "0",
            "-1",
            "2147483647",
            "9223372036854775807",
            "-9223372036854775808",
            "99999999999999999999999999999999",
            "abc",
            "",
            "\u{1F600}",
        ] {
            let n = node("div", &[("tabindex", v), ("focusable", "true")], vec![]);
            assert!(
                xml_node_to_dom_fast(&n, &map, false, 0).is_ok(),
                "tabindex={v:?} must not panic"
            );
        }
    }
    #[test]
    fn apply_xml_node_attributes_img_width_height_garbage_falls_back_to_zero() {
        let map = ComponentMap::default();
        let n = node(
            "img",
            &[("src", "a.png"), ("width", "-5"), ("height", "not-a-number")],
            vec![],
        );
        let dom = xml_node_to_dom_fast(&n, &map, false, 0).expect("ok");
        match dom.root.get_node_type() {
            NodeType::Image(_) => {}
            other => panic!("expected an Image node, got {other:?}"),
        }
    }
    // ================================================================
    // set_stringified_attributes  (numeric: tabs / tabindex)
    // ================================================================
    #[test]
    fn set_stringified_attributes_zero_tabs_and_empty_attrs() {
        let mut s = String::new();
        set_stringified_attributes(&mut s, &attrs(&[]), &no_args(), 0);
        assert_eq!(s, "", "nothing to emit for an attribute-less node");
    }
    #[test]
    fn set_stringified_attributes_splits_ids_and_classes_on_whitespace() {
        let mut s = String::new();
        set_stringified_attributes(
            &mut s,
            &attrs(&[("id", "a  b"), ("class", "c\td")]),
            &no_args(),
            0,
        );
        assert!(s.contains(".with_id(\"a\")"), "got {s:?}");
        assert!(s.contains(".with_id(\"b\")"));
        assert!(s.contains(".with_class(\"c\")"));
        assert!(s.contains(".with_class(\"d\")"));
    }
    #[test]
    fn set_stringified_attributes_tabindex_boundaries() {
        let cases: &[(&str, &str)] = &[
            ("0", "TabIndex::Auto"),
            ("5", "TabIndex::OverrideInParent(5)"),
            ("-1", "TabIndex::NoKeyboardFocus"),
        ];
        for (val, expected) in cases {
            let mut s = String::new();
            set_stringified_attributes(&mut s, &attrs(&[("tabindex", val)]), &no_args(), 0);
            assert!(s.contains(expected), "tabindex={val:?} => {s:?}");
        }
        // Unparsable / overflowing values emit nothing rather than panicking.
        for val in ["abc", "", "99999999999999999999999999999999", "1.5"] {
            let mut s = String::new();
            set_stringified_attributes(&mut s, &attrs(&[("tabindex", val)]), &no_args(), 0);
            assert!(
                !s.contains("TabIndex"),
                "tabindex={val:?} must be ignored, got {s:?}"
            );
        }
    }
    #[test]
    fn set_stringified_attributes_focusable_only_accepts_exact_true_false() {
        let mut s = String::new();
        set_stringified_attributes(&mut s, &attrs(&[("focusable", "true")]), &no_args(), 0);
        assert!(s.contains("TabIndex::Auto"));
        let mut s = String::new();
        set_stringified_attributes(&mut s, &attrs(&[("focusable", "false")]), &no_args(), 0);
        assert!(s.contains("TabIndex::NoKeyboardFocus"));
        let mut s = String::new();
        set_stringified_attributes(&mut s, &attrs(&[("focusable", "TRUE")]), &no_args(), 0);
        assert!(s.is_empty(), "casing other than `true`/`false` is ignored, got {s:?}");
    }
    #[test]
    fn set_stringified_attributes_large_tab_depth_does_not_overflow() {
        // `tabs` becomes `"    ".repeat(tabs)`; a large-but-sane nesting depth must
        // stay linear and allocate without panicking.
        let mut s = String::new();
        set_stringified_attributes(&mut s, &attrs(&[("id", "x")]), &no_args(), 1_000);
        assert!(s.contains(".with_id(\"x\")"));
        assert!(s.len() > 4_000, "the 1000-level indent is actually emitted");
    }
    // ================================================================
    // group_matches / CssMatcher  (numeric: indices)
    // ================================================================
    fn refs(v: &[CssPathSelector]) -> Vec<&CssPathSelector> {
        v.iter().collect()
    }
    #[test]
    fn group_matches_global_matches_at_any_index() {
        let a = vec![CssPathSelector::Global];
        assert!(group_matches(&refs(&a), &[], 0, 0));
        assert!(
            group_matches(&refs(&a), &[], usize::MAX, usize::MAX),
            "usize::MAX indices must not overflow"
        );
    }
    #[test]
    fn group_matches_type_class_id() {
        let div = vec![CssPathSelector::Type(NodeTypeTag::Div)];
        let p = vec![CssPathSelector::Type(NodeTypeTag::P)];
        assert!(group_matches(&refs(&div), &refs(&div), 0, 1));
        assert!(!group_matches(&refs(&div), &refs(&p), 0, 1));
        assert!(!group_matches(&refs(&div), &[], 0, 1), "an empty haystack never matches");
        let cls = vec![CssPathSelector::Class(AzString::from("x"))];
        assert!(group_matches(&refs(&cls), &refs(&cls), 0, 1));
        let id = vec![CssPathSelector::Id(AzString::from("x"))];
        assert!(!group_matches(&refs(&id), &refs(&cls), 0, 1), "an id is not a class");
    }
    #[test]
    fn group_matches_first_and_last_pseudo_at_boundaries() {
        let first = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::First)];
        assert!(group_matches(&refs(&first), &[], 0, 10));
        assert!(!group_matches(&refs(&first), &[], 1, 10));
        let last = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::Last)];
        assert!(group_matches(&refs(&last), &[], 9, 10));
        assert!(!group_matches(&refs(&last), &[], 8, 10));
        assert!(
            group_matches(&refs(&last), &[], 0, 0),
            "parent_children == 0 saturates to 0, so index 0 counts as last"
        );
    }
    #[test]
    fn group_matches_nth_child_even_odd_and_number() {
        let even = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
            CssNthChildSelector::Even,
        ))];
        assert!(group_matches(&refs(&even), &[], 0, 0));
        assert!(!group_matches(&refs(&even), &[], 1, 0));
        assert!(
            !group_matches(&refs(&even), &[], usize::MAX, 0),
            "usize::MAX is odd"
        );
        let odd = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
            CssNthChildSelector::Odd,
        ))];
        assert!(group_matches(&refs(&odd), &[], 1, 0));
        assert!(!group_matches(&refs(&odd), &[], 2, 0));
        let n = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
            CssNthChildSelector::Number(u32::MAX),
        ))];
        assert!(group_matches(&refs(&n), &[], u32::MAX as usize, 0));
        assert!(!group_matches(&refs(&n), &[], 0, 0));
    }
    #[test]
    fn group_matches_nth_child_pattern_zero_repeat_does_not_divide_by_zero() {
        let zero = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
            CssNthChildSelector::Pattern(CssNthChildPattern {
                pattern_repeat: 0,
                offset: 0,
            }),
        ))];
        // `is_multiple_of(0)` is `self == 0` — no division by zero.
        assert!(group_matches(&refs(&zero), &[], 0, 0));
        assert!(!group_matches(&refs(&zero), &[], 5, 0));
        let offset_past = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
            CssNthChildSelector::Pattern(CssNthChildPattern {
                pattern_repeat: 2,
                offset: u32::MAX,
            }),
        ))];
        assert!(
            group_matches(&refs(&offset_past), &[], 0, 0),
            "index - offset saturates to 0 rather than underflowing"
        );
    }
    #[test]
    fn group_matches_structural_combinators_never_match() {
        for sel in [
            CssPathSelector::Children,
            CssPathSelector::DirectChildren,
            CssPathSelector::AdjacentSibling,
            CssPathSelector::GeneralSibling,
        ] {
            let a = vec![sel.clone()];
            assert!(
                !group_matches(&refs(&a), &refs(&a), 0, 1),
                "{sel:?} is a combinator, not a matchable group member"
            );
        }
    }
    #[test]
    fn css_matcher_empty_path_never_matches() {
        let m = CssMatcher {
            path: Vec::new(),
            indices_in_parent: vec![0],
            children_length: vec![0],
        };
        let path = CssPath {
            selectors: vec![CssPathSelector::Type(NodeTypeTag::Body)].into(),
        };
        assert!(!m.matches(&path), "an empty matcher path can never match");
        let m2 = CssMatcher {
            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
            indices_in_parent: vec![0],
            children_length: vec![0],
        };
        let empty_path = CssPath {
            selectors: Vec::new().into(),
        };
        assert!(!m2.matches(&empty_path), "an empty CSS path can never match");
    }
    #[test]
    fn css_matcher_get_hash_is_deterministic_and_path_sensitive() {
        let a = CssMatcher {
            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
            indices_in_parent: vec![0],
            children_length: vec![0],
        };
        let b = CssMatcher {
            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
            indices_in_parent: vec![9],
            children_length: vec![9],
        };
        let c = CssMatcher {
            path: vec![CssPathSelector::Type(NodeTypeTag::Div)],
            indices_in_parent: vec![0],
            children_length: vec![0],
        };
        assert_eq!(a.get_hash(), a.get_hash(), "stable across calls");
        assert_eq!(
            a.get_hash(),
            b.get_hash(),
            "the hash covers only `path`, not the sibling indices"
        );
        assert_ne!(a.get_hash(), c.get_hash());
        let empty = CssMatcher {
            path: Vec::new(),
            indices_in_parent: Vec::new(),
            children_length: Vec::new(),
        };
        let _ = empty.get_hash(); // must not panic
    }
    #[test]
    fn css_matcher_mismatched_bookkeeping_vec_lengths_bail_out() {
        // `indices_in_parent` / `children_length` must be as long as the group list.
        let m = CssMatcher {
            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
            indices_in_parent: Vec::new(),
            children_length: Vec::new(),
        };
        let path = CssPath {
            selectors: vec![CssPathSelector::Type(NodeTypeTag::Body)].into(),
        };
        assert!(
            !m.matches(&path),
            "a desynced matcher must return false, not index out of bounds"
        );
    }
    #[test]
    fn get_css_blocks_and_inline_string_on_empty_css() {
        let m = CssMatcher {
            path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
            indices_in_parent: vec![0],
            children_length: vec![0],
        };
        assert!(get_css_blocks(&Css::empty(), &m).is_empty());
        assert_eq!(css_blocks_to_inline_string(&[]), "");
    }
    // ================================================================
    // str_to_dom / str_to_dom_unstyled / parse_page_style_and_body / body_matcher
    // ================================================================
    #[test]
    fn str_to_dom_rejects_documents_without_html_or_body() {
        let map = ComponentMap::with_builtin();
        assert_eq!(
            str_to_dom(&[], &map, None).unwrap_err(),
            DomXmlParseError::NoHtmlNode
        );
        let html_only = vec![elem(XmlNode::create("html"))];
        assert_eq!(
            str_to_dom(&html_only, &map, None).unwrap_err(),
            DomXmlParseError::NoBodyInHtml
        );
        assert!(str_to_dom_unstyled(&[], &map).is_err());
    }
    #[test]
    fn str_to_dom_valid_minimal() {
        let map = ComponentMap::with_builtin();
        let d = doc("body { color: red; }", vec![elem(node("div", &[("id", "x")], vec![]))]);
        assert!(str_to_dom(&d, &map, None).is_ok());
        assert!(str_to_dom_unstyled(&d, &map).is_ok());
    }
    #[test]
    fn str_to_dom_max_width_edge_values_do_not_panic() {
        let map = ComponentMap::with_builtin();
        let d = doc("", vec![elem(XmlNode::create("div"))]);
        for w in [
            Some(0.0f32),
            Some(-0.0),
            Some(-1.0),
            Some(f32::MAX),
            Some(f32::MIN),
            Some(f32::NAN),
            Some(f32::INFINITY),
            Some(f32::NEG_INFINITY),
            None,
        ] {
            assert!(
                str_to_dom(&d, &map, w).is_ok(),
                "max_width={w:?} is formatted straight into a CSS string and must not panic"
            );
        }
    }
    #[test]
    fn str_to_dom_deeply_nested_body_is_depth_capped_not_stack_overflowing() {
        let map = ComponentMap::with_builtin();
        let deep = wrap_divs(2_000, node("div", &[("id", "bottom")], vec![]));
        let d = doc("", vec![elem(deep)]);
        assert!(
            str_to_dom(&d, &map, None).is_ok(),
            "children past MAX_XML_NESTING_DEPTH are dropped, not crashed on"
        );
    }
    #[test]
    fn parse_page_style_and_body_and_body_matcher() {
        let d = doc("body { color: red; }", vec![elem(XmlNode::create("div"))]);
        let (css, body) = parse_page_style_and_body(&d).expect("well-formed page");
        assert_eq!(body.node_type.as_str(), "body");
        assert!(!css.rules.as_ref().is_empty(), "the <style> block is parsed");
        let m = body_matcher(body);
        assert!(m.path.is_empty(), "the matcher starts with an empty path");
        assert_eq!(m.indices_in_parent, vec![0]);
        assert_eq!(m.children_length, vec![body.children.as_ref().len()]);
    }
    #[test]
    fn parse_page_style_and_body_with_no_style_block() {
        let head = node("head", &[], vec![]);
        let body = node("body", &[], vec![]);
        let d = vec![elem(node("html", &[], vec![elem(head), elem(body)]))];
        let (css, body) = parse_page_style_and_body(&d).expect("ok");
        assert!(css.rules.as_ref().is_empty());
        assert_eq!(body.children.as_ref().len(), 0);
    }
    // ================================================================
    // str_to_rust_code / str_to_c_code / str_to_cpp_code / str_to_python_code
    // ================================================================
    #[test]
    fn str_to_rust_code_empty_input_is_an_error_not_a_panic() {
        let map = ComponentMap::with_builtin();
        assert!(matches!(
            str_to_rust_code(&[], "", &map),
            Err(CompileError::Xml(DomXmlParseError::NoHtmlNode))
        ));
        assert!(str_to_c_code(&[], &map).is_err());
        assert!(str_to_cpp_code(&[], &map).is_err());
        assert!(str_to_python_code(&[], &map).is_err());
    }
    #[test]
    fn str_to_rust_code_whitespace_and_text_only_roots_are_errors() {
        let map = ComponentMap::with_builtin();
        for roots in [vec![txt("   ")], vec![txt("\t\n")], vec![txt("garbage")]] {
            assert!(
                str_to_rust_code(&roots, "", &map).is_err(),
                "a document with no <html> element must be rejected"
            );
        }
    }
    #[test]
    fn str_to_rust_code_valid_minimal() {
        let map = ComponentMap::with_builtin();
        let d = doc("", vec![elem(node("p", &[], vec![txt("Hi")]))]);
        let src = str_to_rust_code(&d, "// imports", &map).expect("compiles");
        assert!(src.contains("Dom::create_body()"), "got:\n{src}");
        assert!(src.contains("Dom::create_p_with_text(AzString::from(\"Hi\"))"));
        assert!(src.contains("// imports"), "the imports blob is spliced in");
        assert!(src.contains("fn main()"));
    }
    #[test]
    fn str_to_c_cpp_python_code_valid_minimal() {
        let map = ComponentMap::with_builtin();
        let d = doc("", vec![elem(node("p", &[], vec![txt("Hi")]))]);
        let c = str_to_c_code(&d, &map).expect("compiles");
        assert!(c.contains("#include \"azul.h\""), "got:\n{c}");
        assert!(c.contains("AzDom n0 = AzDom_createBody();"));
        assert!(c.contains("AzDom_createPWithText(AZ_STR(\"Hi\"))"));
        let cpp = str_to_cpp_code(&d, &map).expect("compiles");
        assert!(cpp.contains("#include \"azul20.hpp\""), "got:\n{cpp}");
        assert!(cpp.contains("Dom::create_p_with_text(String(\"Hi\"))"));
        let py = str_to_python_code(&d, &map).expect("compiles");
        assert!(py.contains("import azul"), "got:\n{py}");
        assert!(py.contains("azul.Dom.create_p_with_text(\"Hi\")"));
    }
    #[test]
    fn compile_targets_escape_quotes_in_text_content() {
        let map = ComponentMap::with_builtin();
        let d = doc("", vec![elem(node("div", &[], vec![txt("say \"hi\"")]))]);
        let rust = str_to_rust_code(&d, "", &map).expect("compiles");
        assert!(rust.contains("say \\\"hi\\\""), "got:\n{rust}");
        let c = str_to_c_code(&d, &map).expect("compiles");
        assert!(c.contains("say \\\"hi\\\""), "got:\n{c}");
    }
    #[test]
    fn compile_body_node_to_rust_code_on_an_empty_body() {
        let map = ComponentMap::with_builtin();
        let body = node("body", &[], vec![]);
        let mut extra = VecContents::default();
        let mut blocks = BTreeMap::new();
        let out = compile_body_node_to_rust_code(
            &body,
            &map,
            &mut extra,
            &mut blocks,
            &Css::empty(),
            body_matcher(&body),
        )
        .expect("ok");
        assert_eq!(out, "Dom::create_body()", "no children => no .with_children()");
    }
    #[test]
    fn compile_body_node_to_rust_code_skips_whitespace_only_text_children() {
        let map = ComponentMap::with_builtin();
        let body = node("body", &[], vec![txt("   \n\t ")]);
        let mut extra = VecContents::default();
        let mut blocks = BTreeMap::new();
        let out = compile_body_node_to_rust_code(
            &body,
            &map,
            &mut extra,
            &mut blocks,
            &Css::empty(),
            body_matcher(&body),
        )
        .expect("ok");
        assert!(
            !out.contains("create_text"),
            "a whitespace-only text child emits nothing, got:\n{out}"
        );
    }
    // ================================================================
    // builtin_render_fn / builtin_compile_fn  (numeric: indent)
    // ================================================================
    #[test]
    fn builtin_render_fn_for_a_text_and_a_textless_element() {
        let map = ComponentMap::with_builtin();
        let div = map.get_unqualified("div").expect("builtin div");
        assert!(matches!(
            builtin_render_fn(div, &div.data_model, &map),
            ResultStyledDomRenderDomError::Ok(_)
        ));
        let p = map.get_unqualified("p").expect("builtin p");
        assert!(matches!(
            builtin_render_fn(p, &p.data_model, &map),
            ResultStyledDomRenderDomError::Ok(_)
        ));
    }
    #[test]
    fn builtin_compile_fn_ignores_indent_so_usize_max_is_safe() {
        let map = ComponentMap::with_builtin();
        let div = map.get_unqualified("div").expect("builtin div");
        for indent in [0usize, 1, 1024, usize::MAX] {
            match builtin_compile_fn(div, &CompileTarget::Rust, &div.data_model, indent) {
                ResultStringCompileError::Ok(s) => assert_eq!(
                    s.as_str(),
                    "Dom::create_node(NodeType::Div)",
                    "indent is unused by builtin_compile_fn (indent={indent})"
                ),
                ResultStringCompileError::Err(e) => panic!("unexpected error: {e:?}"),
            }
        }
    }
    #[test]
    fn builtin_compile_fn_emits_text_and_escapes_it() {
        let map = ComponentMap::with_builtin();
        let p = map.get_unqualified("p").expect("builtin p");
        let data = p
            .data_model
            .clone()
            .with_default("text", ComponentDefaultValue::String(AzString::from("a\"b\\c")));
        match builtin_compile_fn(p, &CompileTarget::Rust, &data, 0) {
            ResultStringCompileError::Ok(s) => {
                assert!(s.as_str().contains("a\\\"b\\\\c"), "got {}", s.as_str());
            }
            ResultStringCompileError::Err(e) => panic!("unexpected error: {e:?}"),
        }
    }
    #[test]
    fn builtin_compile_fn_covers_every_target() {
        let map = ComponentMap::with_builtin();
        let div = map.get_unqualified("div").expect("builtin div");
        for target in [
            CompileTarget::Rust,
            CompileTarget::C,
            CompileTarget::Cpp,
            CompileTarget::Python,
        ] {
            match builtin_compile_fn(div, &target, &div.data_model, 0) {
                ResultStringCompileError::Ok(s) => {
                    assert!(!s.as_str().is_empty(), "{target:?} emitted nothing");
                }
                ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
            }
        }
    }
    // ================================================================
    // user_defined_render_fn / user_defined_compile_fn
    // ================================================================
    fn every_default_kind() -> Vec<ComponentDataField> {
        use ComponentDefaultValue as D;
        vec![
            data_field("s", ComponentFieldType::String, Some(D::String(AzString::from("txt"))), ""),
            data_field("b", ComponentFieldType::Bool, Some(D::Bool(true)), ""),
            data_field("i32", ComponentFieldType::I32, Some(D::I32(i32::MIN)), ""),
            data_field("i64", ComponentFieldType::I64, Some(D::I64(i64::MIN)), ""),
            data_field("u32", ComponentFieldType::U32, Some(D::U32(u32::MAX)), ""),
            data_field("u64", ComponentFieldType::U64, Some(D::U64(u64::MAX)), ""),
            data_field("us", ComponentFieldType::Usize, Some(D::Usize(usize::MAX)), ""),
            data_field("f32", ComponentFieldType::F32, Some(D::F32(f32::NAN)), ""),
            data_field("f64", ComponentFieldType::F64, Some(D::F64(f64::INFINITY)), ""),
            data_field(
                "c",
                ComponentFieldType::ColorU,
                Some(D::ColorU(ColorU { r: 0, g: 0, b: 0, a: 0 })),
                "",
            ),
            data_field("cb", ComponentFieldType::StyledDom, Some(D::CallbackFnPointer(AzString::from("on_click"))), ""),
            data_field("j", ComponentFieldType::String, Some(D::Json(AzString::from("{}"))), ""),
            data_field("none", ComponentFieldType::String, Some(D::None), ""),
            data_field("missing", ComponentFieldType::String, None, ""),
        ]
    }
    #[test]
    fn user_defined_render_fn_handles_every_default_value_kind() {
        let map = ComponentMap::with_builtin();
        let def = user_def("", every_default_kind());
        assert!(matches!(
            user_defined_render_fn(&def, &def.data_model, &map),
            ResultStyledDomRenderDomError::Ok(_)
        ));
    }
    #[test]
    fn user_defined_render_fn_on_an_empty_model_and_with_css() {
        let map = ComponentMap::with_builtin();
        let empty = user_def("", Vec::new());
        assert!(matches!(
            user_defined_render_fn(&empty, &empty.data_model, &map),
            ResultStyledDomRenderDomError::Ok(_)
        ));
        let styled = user_def(".widget { color: red; }", Vec::new());
        assert!(matches!(
            user_defined_render_fn(&styled, &styled.data_model, &map),
            ResultStyledDomRenderDomError::Ok(_)
        ));
    }
    #[test]
    fn user_defined_render_fn_unknown_sub_component_renders_a_placeholder() {
        let map = ComponentMap::create(); // empty: no library can resolve the instance
        let def = user_def(
            "",
            vec![data_field(
                "child",
                ComponentFieldType::StyledDom,
                Some(ComponentDefaultValue::ComponentInstance(ComponentInstanceDefault {
                    library: AzString::from("nope"),
                    component: AzString::from("missing"),
                    field_overrides: Vec::new().into(),
                })),
                "",
            )],
        );
        assert!(
            matches!(
                user_defined_render_fn(&def, &def.data_model, &map),
                ResultStyledDomRenderDomError::Ok(_)
            ),
            "an unresolvable sub-component must render a placeholder, not error out"
        );
    }
    #[test]
    fn user_defined_compile_fn_indent_zero_and_every_target() {
        let def = user_def("", every_default_kind());
        for target in [
            CompileTarget::Rust,
            CompileTarget::C,
            CompileTarget::Cpp,
            CompileTarget::Python,
        ] {
            match user_defined_compile_fn(&def, &target, &def.data_model, 0) {
                ResultStringCompileError::Ok(s) => {
                    assert!(!s.as_str().is_empty(), "{target:?} emitted nothing");
                }
                ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
            }
        }
    }
    #[test]
    fn user_defined_compile_fn_indent_scales_the_leading_whitespace() {
        // NOTE: `indent` is used as `" ".repeat(indent * 4)`, so it is NOT safe at
        // usize::MAX (the multiply overflows). Exercise the realistic range.
        let def = user_def("", Vec::new());
        let mut prev = 0usize;
        for indent in [0usize, 1, 2, 8] {
            match user_defined_compile_fn(&def, &CompileTarget::Rust, &def.data_model, indent) {
                ResultStringCompileError::Ok(s) => {
                    let len = s.as_str().len();
                    assert!(len > prev, "indent={indent} must widen the output");
                    prev = len;
                }
                ResultStringCompileError::Err(e) => panic!("indent={indent}: {e:?}"),
            }
        }
    }
    #[test]
    fn user_defined_compile_fn_escapes_string_defaults() {
        let def = user_def(
            "",
            vec![data_field(
                "s",
                ComponentFieldType::String,
                Some(ComponentDefaultValue::String(AzString::from("a\"b\\c"))),
                "",
            )],
        );
        match user_defined_compile_fn(&def, &CompileTarget::Rust, &def.data_model, 0) {
            ResultStringCompileError::Ok(s) => {
                assert!(s.as_str().contains("a\\\"b\\\\c"), "got:\n{}", s.as_str());
            }
            ResultStringCompileError::Err(e) => panic!("{e:?}"),
        }
    }
    #[test]
    fn push_scalar_field_appends_one_div_per_call() {
        let mut children: Vec<Dom> = Vec::new();
        push_scalar_field(&mut children, "n", &i64::MIN);
        push_scalar_field(&mut children, "", &f32::NAN);
        push_scalar_field(&mut children, "\u{1F600}", &usize::MAX);
        assert_eq!(children.len(), 3);
    }
    // ================================================================
    // Structural builtins: if / for / map
    // ================================================================
    #[test]
    fn builtin_if_for_map_component_defs_are_well_formed() {
        for (def, model, field) in [
            (builtin_if_component(), "IfData", "condition"),
            (builtin_for_component(), "ForData", "count"),
            (builtin_map_component(), "MapData", "data_json"),
        ] {
            assert_eq!(def.id.collection.as_str(), "builtin");
            assert_eq!(def.data_model.name.as_str(), model);
            assert!(
                def.data_model.get_field(field).is_some(),
                "{model} must expose `{field}`"
            );
        }
    }
    #[test]
    fn builtin_if_render_fn_defaults_to_the_else_branch() {
        let map = ComponentMap::create();
        let def = builtin_if_component();
        // Missing / wrongly-typed condition => false, no panic.
        let empty = dm("IfData", Vec::new());
        assert!(matches!(
            builtin_if_render_fn(&def, &empty, &map),
            ResultStyledDomRenderDomError::Ok(_)
        ));
        let truthy = def
            .data_model
            .clone()
            .with_default("condition", ComponentDefaultValue::Bool(true));
        assert!(matches!(
            builtin_if_render_fn(&def, &truthy, &map),
            ResultStyledDomRenderDomError::Ok(_)
        ));
    }
    #[test]
    fn builtin_for_render_fn_handles_zero_and_a_wrongly_typed_count() {
        let map = ComponentMap::create();
        let def = builtin_for_component();
        let zero = def
            .data_model
            .clone()
            .with_default("count", ComponentDefaultValue::U32(0));
        assert!(matches!(
            builtin_for_render_fn(&def, &zero, &map),
            ResultStyledDomRenderDomError::Ok(_)
        ));
        // A non-U32 default falls back to the documented default of 3.
        let wrong_type = def
            .data_model
            .clone()
            .with_default("count", ComponentDefaultValue::String(AzString::from("9")));
        assert!(matches!(
            builtin_for_render_fn(&def, &wrong_type, &map),
            ResultStyledDomRenderDomError::Ok(_)
        ));
    }
    #[test]
    fn builtin_map_render_fn_defaults_to_an_empty_json_array() {
        let map = ComponentMap::create();
        let def = builtin_map_component();
        assert!(matches!(
            builtin_map_render_fn(&def, &dm("MapData", Vec::new()), &map),
            ResultStyledDomRenderDomError::Ok(_)
        ));
        let garbage = def
            .data_model
            .clone()
            .with_default("data_json", ComponentDefaultValue::String(AzString::from("{{{")));
        assert!(
            matches!(
                builtin_map_render_fn(&def, &garbage, &map),
                ResultStyledDomRenderDomError::Ok(_)
            ),
            "malformed JSON must not panic — it is only echoed into a label"
        );
    }
    #[test]
    fn structural_builtin_compile_fns_ignore_indent_entirely() {
        let cases: [(ComponentDef, ComponentCompileFn); 3] = [
            (builtin_if_component(), builtin_if_compile_fn),
            (builtin_for_component(), builtin_for_compile_fn),
            (builtin_map_component(), builtin_map_compile_fn),
        ];
        for (def, f) in cases {
            for target in [
                CompileTarget::Rust,
                CompileTarget::C,
                CompileTarget::Cpp,
                CompileTarget::Python,
            ] {
                for indent in [0usize, usize::MAX] {
                    match f(&def, &target, &def.data_model, indent) {
                        ResultStringCompileError::Ok(s) => {
                            assert!(!s.as_str().is_empty(), "{target:?}/{indent} emitted nothing");
                        }
                        ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
                    }
                }
            }
        }
    }
    // ================================================================
    // data_field / builtin_data_model / builtin_component_def
    // ================================================================
    #[test]
    fn data_field_required_is_the_inverse_of_having_a_default() {
        let with = data_field(
            "x",
            ComponentFieldType::String,
            Some(ComponentDefaultValue::String(AzString::from("v"))),
            "d",
        );
        assert!(!with.required);
        assert_eq!(with.description.as_str(), "d");
        let without = data_field("x", ComponentFieldType::String, None, "");
        assert!(without.required);
        assert!(matches!(
            without.default_value,
            OptionComponentDefaultValue::None
        ));
    }
    #[test]
    fn builtin_data_model_unknown_tag_is_empty() {
        assert!(builtin_data_model("").is_empty());
        assert!(builtin_data_model("div").is_empty());
        assert!(builtin_data_model("\u{1F600}").is_empty());
        assert!(builtin_data_model(&"z".repeat(10_000)).is_empty());
    }
    #[test]
    fn builtin_data_model_known_tags_expose_their_attributes() {
        let a = builtin_data_model("a");
        assert!(
            a.iter().any(|f| f.name.as_str() == "href"),
            "<a> must expose href"
        );
        // `src` on <img> is required (it has no default value).
        let img = builtin_data_model("img");
        let src = img
            .iter()
            .find(|f| f.name.as_str() == "src")
            .expect("img has src");
        assert!(src.required, "<img src> must be a required field");
        // `img` and `image` share the same model.
        assert_eq!(builtin_data_model("image").len(), img.len());
    }
    #[test]
    fn builtin_component_def_default_text_controls_the_text_field() {
        let with_text = builtin_component_def("p", "Paragraph", Some("Hi"), "");
        assert_eq!(
            with_text.data_model.get_default_string("text").map(AzString::as_str),
            Some("Hi")
        );
        assert_eq!(with_text.data_model.name.as_str(), "ParagraphData");
        assert_eq!(with_text.id.qualified_name(), "builtin:p");
        let no_text = builtin_component_def("div", "Div", None, "");
        assert!(
            no_text.data_model.get_field("text").is_none(),
            "a `None` default_text means the element has no text field at all"
        );
        // An empty-string default still creates the field.
        let empty_text = builtin_component_def("span", "Span", Some(""), "");
        assert!(empty_text.data_model.get_field("text").is_some());
        assert_eq!(
            empty_text.data_model.get_default_string("text").map(AzString::as_str),
            Some("")
        );
    }
    // ================================================================
    // xml_attrs_to_data_model
    // ================================================================
    #[test]
    fn xml_attrs_to_data_model_overrides_defaults_from_attributes() {
        let base = builtin_component_def("a", "Link", Some("Link text"), "").data_model;
        let model = xml_attrs_to_data_model(&base, &attrs(&[("href", "/x")]), None);
        assert_eq!(
            model.get_default_string("href").map(AzString::as_str),
            Some("/x")
        );
        assert_eq!(
            model.get_default_string("text").map(AzString::as_str),
            Some("Link text"),
            "un-supplied fields keep their defaults"
        );
        assert_eq!(
            model.fields.as_ref().len(),
            base.fields.as_ref().len(),
            "no field is added or dropped"
        );
    }
    #[test]
    fn xml_attrs_to_data_model_text_content_is_prepared_and_empty_text_is_ignored() {
        let base = builtin_component_def("a", "Link", Some("Link text"), "").data_model;
        let with_text = xml_attrs_to_data_model(&base, &attrs(&[]), Some("  Hello &amp; bye  "));
        assert_eq!(
            with_text.get_default_string("text").map(AzString::as_str),
            Some("Hello & bye"),
            "text content is trimmed and entity-decoded"
        );
        let blank = xml_attrs_to_data_model(&base, &attrs(&[]), Some("   \n\t "));
        assert_eq!(
            blank.get_default_string("text").map(AzString::as_str),
            Some("Link text"),
            "whitespace-only text content leaves the default intact"
        );
    }
    #[test]
    fn xml_attrs_to_data_model_ignores_unknown_attributes() {
        let base = builtin_component_def("a", "Link", Some(""), "").data_model;
        let before = base.fields.as_ref().len();
        let model = xml_attrs_to_data_model(
            &base,
            &attrs(&[("data-nonsense", "1"), ("", ""), ("\u{1F600}", "x")]),
            None,
        );
        assert_eq!(
            model.fields.as_ref().len(),
            before,
            "unknown attributes must not create fields"
        );
    }
    // ================================================================
    // DomXml
    // ================================================================
    #[test]
    fn dom_xml_into_styled_dom_matches_the_from_impl() {
        let via_method: StyledDom = DomXml::default().into_styled_dom();
        let via_from: StyledDom = DomXml::default().into();
        assert_eq!(
            via_method, via_from,
            "into_styled_dom() must be exactly the From<DomXml> impl"
        );
    }
    // ================================================================
    // Display impls  (serializer)
    // ================================================================
    fn pos() -> XmlTextPos {
        XmlTextPos {
            row: u32::MAX,
            col: 0,
        }
    }
    #[test]
    fn xml_text_pos_display_is_non_empty_for_edge_values() {
        assert_eq!(
            format!("{}", XmlTextPos { row: 0, col: 0 }),
            "line 0:0",
            "a zero position is still rendered"
        );
        assert_eq!(
            format!(
                "{}",
                XmlTextPos {
                    row: u32::MAX,
                    col: u32::MAX
                }
            ),
            "line 4294967295:4294967295"
        );
    }
    #[test]
    fn xml_stream_error_display_covers_every_variant() {
        let variants = vec![
            XmlStreamError::UnexpectedEndOfStream,
            XmlStreamError::InvalidName,
            XmlStreamError::NonXmlChar(NonXmlCharError {
                ch: u32::MAX,
                pos: pos(),
            }),
            XmlStreamError::InvalidChar(InvalidCharError {
                expected: u8::MAX,
                got: 0,
                pos: pos(),
            }),
            XmlStreamError::InvalidCharMultiple(InvalidCharMultipleError {
                expected: 0,
                got: Vec::<u8>::new().into(),
                pos: pos(),
            }),
            XmlStreamError::InvalidQuote(InvalidQuoteError { got: 0, pos: pos() }),
            XmlStreamError::InvalidSpace(InvalidSpaceError { got: 0, pos: pos() }),
            XmlStreamError::InvalidString(InvalidStringError {
                got: AzString::from(""),
                pos: pos(),
            }),
            XmlStreamError::InvalidReference,
            XmlStreamError::InvalidExternalID,
            XmlStreamError::InvalidCommentData,
            XmlStreamError::InvalidCommentEnd,
            XmlStreamError::InvalidCharacterData,
        ];
        for v in &variants {
            let s = format!("{v}");
            assert!(!s.is_empty(), "{v:?} must render a non-empty message");
        }
        // `char::from_u32(u32::MAX)` is None — the formatter must not unwrap it.
        assert!(format!("{}", variants[2]).contains("None"));
    }
    #[test]
    fn xml_parse_error_display_covers_every_variant() {
        let te = XmlTextError {
            stream_error: XmlStreamError::InvalidName,
            pos: pos(),
        };
        let variants = vec![
            XmlParseError::InvalidDeclaration(te.clone()),
            XmlParseError::InvalidComment(te.clone()),
            XmlParseError::InvalidPI(te.clone()),
            XmlParseError::InvalidDoctype(te.clone()),
            XmlParseError::InvalidEntity(te.clone()),
            XmlParseError::InvalidElement(te.clone()),
            XmlParseError::InvalidAttribute(te.clone()),
            XmlParseError::InvalidCdata(te.clone()),
            XmlParseError::InvalidCharData(te),
            XmlParseError::UnknownToken(pos()),
        ];
        for v in &variants {
            assert!(!format!("{v}").is_empty(), "{v:?} must render");
        }
    }
    #[test]
    fn xml_error_display_covers_the_non_css_variants() {
        let variants = vec![
            XmlError::NoParserAvailable,
            XmlError::InvalidXmlPrefixUri(pos()),
            XmlError::UnexpectedXmlUri(pos()),
            XmlError::UnexpectedXmlnsUri(pos()),
            XmlError::InvalidElementNamePrefix(pos()),
            XmlError::DuplicatedNamespace(DuplicatedNamespaceError {
                ns: AzString::from(""),
                pos: pos(),
            }),
            XmlError::UnknownNamespace(UnknownNamespaceError {
                ns: AzString::from("\u{1F600}"),
                pos: pos(),
            }),
            XmlError::UnexpectedCloseTag(UnexpectedCloseTagError {
                expected: AzString::from("a"),
                actual: AzString::from("b"),
                pos: pos(),
            }),
            XmlError::UnexpectedEntityCloseTag(pos()),
            XmlError::UnknownEntityReference(UnknownEntityReferenceError {
                entity: AzString::from("x"),
                pos: pos(),
            }),
            XmlError::MalformedEntityReference(pos()),
            XmlError::EntityReferenceLoop(pos()),
            XmlError::InvalidAttributeValue(pos()),
            XmlError::DuplicatedAttribute(DuplicatedAttributeError {
                attribute: AzString::from("id"),
                pos: pos(),
            }),
            XmlError::NoRootNode,
            XmlError::SizeLimit,
            XmlError::DtdDetected,
            XmlError::MalformedHierarchy(MalformedHierarchyError {
                expected: AzString::from("app"),
                got: AzString::from("p"),
            }),
            XmlError::ParserError(XmlParseError::UnknownToken(pos())),
            XmlError::UnclosedRootNode,
            XmlError::UnexpectedDeclaration(pos()),
            XmlError::NodesLimitReached,
            XmlError::AttributesLimitReached,
            XmlError::NamespacesLimitReached,
            XmlError::InvalidName(pos()),
            XmlError::NonXmlChar(pos()),
            XmlError::InvalidChar(pos()),
            XmlError::InvalidChar2(pos()),
            XmlError::InvalidString(pos()),
            XmlError::InvalidExternalID(pos()),
            XmlError::InvalidComment(pos()),
            XmlError::InvalidCharacterData(pos()),
            XmlError::UnknownToken(pos()),
            XmlError::UnexpectedEndOfStream,
        ];
        for v in &variants {
            assert!(!format!("{v}").is_empty(), "{v:?} must render");
        }
    }
    #[test]
    fn component_and_render_and_compile_error_display() {
        let unknown = ComponentError::UnknownComponent(AzString::from("\u{1F600}"));
        assert!(format!("{unknown}").contains("Unknown component"));
        let useless = ComponentError::UselessFunctionArgument(UselessFunctionArgumentError {
            component_name: AzString::from("c"),
            argument_name: AzString::from("a"),
            valid_args: Vec::<AzString>::new().into(),
        });
        assert!(!format!("{useless}").is_empty());
        let render: RenderDomError = unknown.clone().into();
        assert!(!format!("{render}").is_empty());
        let compile: CompileError = render.clone().into();
        assert!(!format!("{compile}").is_empty());
        let dom_xml: DomXmlParseError = render.into();
        assert!(!format!("{dom_xml}").is_empty());
        let compile2: CompileError = dom_xml.into();
        assert!(!format!("{compile2}").is_empty());
    }
    #[test]
    fn dom_xml_parse_error_display_covers_the_non_css_variants() {
        let variants = vec![
            DomXmlParseError::NoHtmlNode,
            DomXmlParseError::MultipleHtmlRootNodes,
            DomXmlParseError::NoBodyInHtml,
            DomXmlParseError::MultipleBodyNodes,
            DomXmlParseError::Xml(XmlError::NoRootNode),
            DomXmlParseError::MalformedHierarchy(MalformedHierarchyError {
                expected: AzString::from("app"),
                got: AzString::from("p"),
            }),
            DomXmlParseError::RenderDom(RenderDomError::Component(
                ComponentError::UnknownComponent(AzString::from("x")),
            )),
            DomXmlParseError::Component(ComponentParseError::NotAComponent),
        ];
        for v in &variants {
            assert!(!format!("{v}").is_empty(), "{v:?} must render");
        }
    }
    #[test]
    fn component_parse_error_display_covers_the_non_css_variants() {
        let variants = vec![
            ComponentParseError::NotAComponent,
            ComponentParseError::UnnamedComponent,
            ComponentParseError::MissingName(usize::MAX),
            ComponentParseError::MissingType(MissingTypeError {
                arg_pos: 0,
                arg_name: AzString::from(""),
            }),
            ComponentParseError::WhiteSpaceInComponentName(WhiteSpaceInComponentNameError {
                arg_pos: usize::MAX,
                arg_name: AzString::from("a b"),
            }),
            ComponentParseError::WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError {
                arg_pos: 0,
                arg_name: AzString::from("a"),
                arg_type: AzString::from("b c"),
            }),
        ];
        for v in &variants {
            assert!(!format!("{v}").is_empty(), "{v:?} must render");
        }
    }
    // ================================================================
    // serde-json gated: ComponentDataModel::to_json / from_json
    // ================================================================
    #[cfg(feature = "serde-json")]
    #[test]
    fn data_model_to_json_round_trips() {
        let m = model_with_text();
        let json = m.to_json().expect("serializes");
        let back = ComponentDataModel::from_json(&json).expect("deserializes");
        assert_eq!(back.name.as_str(), m.name.as_str());
        assert_eq!(back.fields.as_ref().len(), m.fields.as_ref().len());
        assert_eq!(back.get_default_string("text").map(AzString::as_str), Some("hi"));
    }
    #[cfg(feature = "serde-json")]
    #[test]
    fn data_model_from_json_rejects_garbage_without_panicking() {
        for s in [
            "",
            "   ",
            "\t\n",
            "not json",
            "{",
            "[]",
            "null",
            "0",
            "-0",
            "9223372036854775807",
            "NaN",
            "\u{1F600}",
        ] {
            assert!(
                ComponentDataModel::from_json(s).is_err(),
                "{s:?} is not a data model"
            );
        }
    }
    #[cfg(feature = "serde-json")]
    #[test]
    fn data_model_from_json_deeply_nested_input_does_not_stack_overflow() {
        let bomb = format!("{}{}", "[".repeat(10_000), "]".repeat(10_000));
        assert!(
            ComponentDataModel::from_json(&bomb).is_err(),
            "serde_json must reject the nesting bomb, not crash"
        );
    }
    #[cfg(feature = "serde-json")]
    #[test]
    fn data_model_to_json_on_an_empty_model() {
        let m = dm("Empty", Vec::new());
        let json = m.to_json().expect("serializes");
        assert!(json.contains("\"fields\""), "got {json}");
        assert!(ComponentDataModel::from_json(&json).is_ok());
    }
}