Skip to main content

azul_layout/xml/
mod.rs

1#![allow(unused_variables)]
2
3use alloc::{boxed::Box, collections::BTreeMap, string::String, vec::Vec};
4use core::fmt;
5#[cfg(feature = "std")]
6use std::path::Path;
7
8#[cfg(feature = "svg")]
9pub mod svg;
10
11/// Decodes XML/HTML entities in a string.
12/// Handles standard XML entities: < > & ' "
13/// and numeric character references: < <
14fn decode_xml_entities(s: &str) -> String {
15    let mut result = String::with_capacity(s.len());
16    let mut chars = s.chars().peekable();
17    
18    while let Some(c) = chars.next() {
19        if c == '&' {
20            // Collect the entity reference
21            let mut entity = String::new();
22            let mut found_semicolon = false;
23            
24            while let Some(&next) = chars.peek() {
25                if next == ';' {
26                    chars.next();
27                    found_semicolon = true;
28                    break;
29                }
30                if !next.is_alphanumeric() && next != '#' {
31                    break;
32                }
33                entity.push(chars.next().unwrap());
34                if entity.len() > 10 {
35                    // Entity too long, not a valid entity
36                    break;
37                }
38            }
39            
40            if found_semicolon {
41                // Try to decode the entity
42                match entity.as_str() {
43                    "lt" => result.push('<'),
44                    "gt" => result.push('>'),
45                    "amp" => result.push('&'),
46                    "apos" => result.push('\''),
47                    "quot" => result.push('"'),
48                    "nbsp" => result.push('\u{00A0}'),
49                    s if s.starts_with('#') => {
50                        // Numeric character reference
51                        let num_str = &s[1..];
52                        let code_point = if num_str.starts_with('x') || num_str.starts_with('X') {
53                            // Hexadecimal
54                            u32::from_str_radix(&num_str[1..], 16).ok()
55                        } else {
56                            // Decimal
57                            num_str.parse::<u32>().ok()
58                        };
59                        if let Some(cp) = code_point {
60                            if let Some(ch) = char::from_u32(cp) {
61                                result.push(ch);
62                            } else {
63                                // Invalid code point, keep original
64                                result.push('&');
65                                result.push_str(&entity);
66                                result.push(';');
67                            }
68                        } else {
69                            // Parse failed, keep original
70                            result.push('&');
71                            result.push_str(&entity);
72                            result.push(';');
73                        }
74                    }
75                    _ => {
76                        // Unknown entity, keep original
77                        result.push('&');
78                        result.push_str(&entity);
79                        result.push(';');
80                    }
81                }
82            } else {
83                // No semicolon found, not a valid entity reference
84                result.push('&');
85                result.push_str(&entity);
86            }
87        } else {
88            result.push(c);
89        }
90    }
91    
92    result
93}
94
95pub use azul_core::xml::*;
96use azul_core::{dom::Dom, impl_from, styled_dom::StyledDom, window::StringPairVec};
97#[cfg(feature = "parser")]
98use azul_css::parser2::CssParseError;
99use azul_css::{css::Css, AzString, OptionString, U8Vec};
100use xmlparser::Tokenizer;
101
102#[cfg(feature = "xml")]
103pub fn domxml_from_str(xml: &str, component_map: &mut XmlComponentMap) -> DomXml {
104    let error_css = Css::empty();
105
106    let parsed = match parse_xml_string(&xml) {
107        Ok(parsed) => parsed,
108        Err(e) => {
109            return DomXml {
110                parsed_dom: Dom::create_body()
111                    .with_children(vec![Dom::create_text(format!("{}", e))].into())
112                    .style(error_css.clone()),
113            };
114        }
115    };
116
117    let parsed_dom = match str_to_dom(parsed.as_ref(), component_map, None) {
118        Ok(o) => o,
119        Err(e) => {
120            return DomXml {
121                parsed_dom: Dom::create_body()
122                    .with_children(vec![Dom::create_text(format!("{}", e))].into())
123                    .style(error_css.clone()),
124            };
125        }
126    };
127
128    DomXml { parsed_dom }
129}
130
131/// Loads, parses and builds a DOM from an XML file
132///
133/// **Warning**: The file is reloaded from disk on every function call - do not
134/// use this in release builds! This function deliberately never fails: In an error case,
135/// the error gets rendered as a `NodeType::Label`.
136#[cfg(all(feature = "std", feature = "xml"))]
137pub fn domxml_from_file<I: AsRef<Path>>(
138    file_path: I,
139    component_map: &mut XmlComponentMap,
140) -> DomXml {
141    use std::fs;
142
143    let error_css = Css::empty();
144
145    let xml = match fs::read_to_string(file_path.as_ref()) {
146        Ok(xml) => xml,
147        Err(e) => {
148            return DomXml {
149                parsed_dom: Dom::create_body()
150                    .with_children(
151                        vec![Dom::create_text(format!(
152                            "Error reading: \"{}\": {}",
153                            file_path.as_ref().to_string_lossy(),
154                            e
155                        ))]
156                        .into(),
157                    )
158                    .style(error_css.clone()),
159            };
160        }
161    };
162
163    domxml_from_str(&xml, component_map)
164}
165
166/// Parses the XML string into an XML tree, returns
167/// the root `<app></app>` node, with the children attached to it.
168///
169/// Since the XML allows multiple root nodes, this function returns
170/// a `Vec<XmlNode>` - which are the "root" nodes, containing all their
171/// children recursively.
172#[cfg(feature = "xml")]
173pub fn parse_xml_string(xml: &str) -> Result<Vec<XmlNodeChild>, XmlError> {
174    use xmlparser::{ElementEnd::*, Token::*, Tokenizer};
175
176    use self::XmlParseError::*;
177
178    let mut root_node = XmlNode::default();
179
180    // Search for "<?xml" and "?>" tags and delete them from the XML
181    let mut xml = xml.trim();
182    if xml.starts_with("<?") {
183        let pos = xml.find("?>").ok_or(XmlError::MalformedHierarchy(
184            azul_core::xml::MalformedHierarchyError {
185                expected: "<?xml".into(),
186                got: "?>".into(),
187            },
188        ))?;
189        xml = &xml[(pos + 2)..];
190    }
191
192    // Delete <!DOCTYPE ...> if necessary (case-insensitive)
193    let mut xml = xml.trim();
194    if xml.len() > 9 && xml[..9].to_ascii_lowercase().starts_with("<!doctype") {
195        let pos = xml.find(">").ok_or(XmlError::MalformedHierarchy(
196            azul_core::xml::MalformedHierarchyError {
197                expected: "<!DOCTYPE".into(),
198                got: ">".into(),
199            },
200        ))?;
201        xml = &xml[(pos + 1)..];
202    } else if xml.starts_with("<!--") {
203        // Skip HTML comments at the start
204        if let Some(end) = xml.find("-->") {
205            xml = &xml[(end + 3)..];
206            xml = xml.trim();
207        }
208    }
209
210    let tokenizer = Tokenizer::from_fragment(xml, 0..xml.len());
211
212    // OPTIMIZED: Use a stack of raw pointers to avoid O(n*d) traversal on every token.
213    // This is safe because:
214    // 1. All pointers point into `root_node` which is owned and not moved
215    // 2. We never hold multiple mutable references simultaneously
216    // 3. The stack is only used within this function
217    let mut node_stack: Vec<*mut XmlNode> = vec![&mut root_node as *mut XmlNode];
218
219    // HTML5-lite parser: List of void elements that should auto-close
220    // See: https://developer.mozilla.org/en-US/docs/Glossary/Void_element
221    const VOID_ELEMENTS: &[&str] = &[
222        "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
223        "source", "track", "wbr",
224    ];
225
226    // HTML5-lite parser: Elements that auto-close when certain other elements are encountered
227    // Format: (element_name, closes_when_encountering)
228    const AUTO_CLOSE_RULES: &[(&str, &[&str])] = &[
229        // List items close when encountering another list item or when parent closes
230        ("li", &["li"]),
231        // Table cells/rows have complex closing rules
232        ("td", &["td", "th", "tr"]),
233        ("th", &["td", "th", "tr"]),
234        ("tr", &["tr"]),
235        // Paragraphs close on block-level elements
236        (
237            "p",
238            &[
239                "address",
240                "article",
241                "aside",
242                "blockquote",
243                "div",
244                "dl",
245                "fieldset",
246                "footer",
247                "form",
248                "h1",
249                "h2",
250                "h3",
251                "h4",
252                "h5",
253                "h6",
254                "header",
255                "hr",
256                "main",
257                "nav",
258                "ol",
259                "p",
260                "pre",
261                "section",
262                "table",
263                "ul",
264            ],
265        ),
266        // Option closes on another option or optgroup
267        ("option", &["option", "optgroup"]),
268        ("optgroup", &["optgroup"]),
269        // DD/DT close on each other
270        ("dd", &["dd", "dt"]),
271        ("dt", &["dd", "dt"]),
272    ];
273
274    // Track which hierarchy level is a void element (shouldn't be pushed to hierarchy)
275    let mut last_was_void = false;
276
277    for token in tokenizer {
278        let token = token.map_err(|e| XmlError::ParserError(translate_xmlparser_error(e)))?;
279        match token {
280            ElementStart { local, .. } => {
281                let tag_name = local.to_string();
282                let is_void_element = VOID_ELEMENTS.contains(&tag_name.as_str());
283
284                // HTML5-lite: If last element was a void element (like <img src="...">),
285                // pop it from hierarchy before processing the new element
286                if last_was_void {
287                    node_stack.pop();
288                    last_was_void = false;
289                }
290
291                // HTML5-lite: Check if we need to auto-close the current element
292                if node_stack.len() > 1 {
293                    // SAFETY: We only access the last element, which is valid
294                    let current_element = unsafe { &*node_stack[node_stack.len() - 1] };
295                    let current_tag = current_element.node_type.as_str();
296
297                    // Check if current element should auto-close when encountering this new tag
298                    for (element, closes_on) in AUTO_CLOSE_RULES {
299                        if current_tag == *element && closes_on.contains(&tag_name.as_str()) {
300                            // Auto-close the current element
301                            node_stack.pop();
302                            break;
303                        }
304                    }
305                }
306
307                // SAFETY: We access the last element which is valid
308                if let Some(&current_parent_ptr) = node_stack.last() {
309                    let current_parent = unsafe { &mut *current_parent_ptr };
310                    
311                    current_parent.children.push(XmlNodeChild::Element(XmlNode {
312                        node_type: tag_name.into(),
313                        attributes: StringPairVec::new().into(),
314                        children: Vec::new().into(),
315                    }));
316
317                    // Get pointer to the newly added child
318                    let children_len = current_parent.children.len();
319                    if let Some(XmlNodeChild::Element(ref mut new_child)) = current_parent.children.as_mut().get_mut(children_len - 1) {
320                        node_stack.push(new_child as *mut XmlNode);
321                    }
322                    
323                    last_was_void = is_void_element;
324                }
325            }
326            ElementEnd { end: Empty, .. } => {
327                // Pop hierarchy for all elements (including void elements after their attributes)
328                if node_stack.len() > 1 {
329                    node_stack.pop();
330                }
331                last_was_void = false;
332            }
333            ElementEnd {
334                end: Close(_, close_value),
335                ..
336            } => {
337                // HTML5-lite: If last element was a void element, pop it first
338                if last_was_void {
339                    node_stack.pop();
340                    last_was_void = false;
341                }
342
343                // HTML5-lite: Check if this is a void element - if so, ignore the closing tag
344                let is_void_element = VOID_ELEMENTS.contains(&close_value.as_str());
345                if is_void_element {
346                    // Void elements shouldn't have closing tags, but tolerate them
347                    continue;
348                }
349
350                // HTML5-lite: Auto-close any elements that should be closed
351                // Walk up the hierarchy and auto-close elements until we find a match
352                let close_value_str = close_value.as_str();
353
354                // Find matching element in stack (skip root at index 0)
355                let mut found_idx = None;
356                for i in (1..node_stack.len()).rev() {
357                    // SAFETY: All pointers in stack are valid
358                    let node = unsafe { &*node_stack[i] };
359                    if node.node_type.as_str() == close_value_str {
360                        found_idx = Some(i);
361                        break;
362                    }
363                }
364
365                if let Some(idx) = found_idx {
366                    // Pop all elements from current position to the matching element (inclusive)
367                    node_stack.truncate(idx);
368                }
369                // If no match found, just ignore (lenient HTML parsing)
370
371                last_was_void = false;
372            }
373            Attribute { local, value, .. } => {
374                // SAFETY: Last element in stack is valid
375                if let Some(&last_ptr) = node_stack.last() {
376                    let last = unsafe { &mut *last_ptr };
377                    // NOTE: Only lowercase the key ("local"), not the value!
378                    // Decode XML entities in attribute values as well
379                    last.attributes.push(azul_core::window::AzStringPair {
380                        key: local.to_string().into(),
381                        value: decode_xml_entities(value.as_str()).into(),
382                    });
383                }
384            }
385            Text { text } => {
386                // HTML5-lite: If last element was a void element, pop it before adding text
387                if last_was_void {
388                    node_stack.pop();
389                    last_was_void = false;
390                }
391
392                // IMPORTANT: Preserve ALL text nodes including whitespace-only nodes.
393                // Whether whitespace is significant depends on the CSS `white-space` property,
394                // which is determined during layout, not during parsing.
395                // 
396                // For example: <pre><span>    </span></pre> must preserve the 4 spaces.
397                // 
398                // We only skip completely EMPTY text nodes (zero-length strings).
399                let text_str = text.as_str();
400
401                if !text_str.is_empty() {
402                    // SAFETY: Last element in stack is valid
403                    if let Some(&current_parent_ptr) = node_stack.last() {
404                        let current_parent = unsafe { &mut *current_parent_ptr };
405                        // Decode XML entities (e.g., &lt; -> <, &gt; -> >, etc.)
406                        let decoded_text = decode_xml_entities(text_str);
407                        // Add text as a child node
408                        current_parent
409                            .children
410                            .push(XmlNodeChild::Text(decoded_text.into()));
411                    }
412                }
413            }
414            _ => {}
415        }
416    }
417
418    // Clean up: if we ended with a void element, pop it
419    if last_was_void {
420        node_stack.pop();
421    }
422
423    Ok(root_node.children.into())
424}
425
426#[cfg(feature = "xml")]
427pub fn parse_xml(s: &str) -> Result<Xml, XmlError> {
428    Ok(Xml {
429        root: parse_xml_string(s)?.into(),
430    })
431}
432
433#[cfg(not(feature = "xml"))]
434pub fn parse_xml(s: &str) -> Result<Xml, XmlError> {
435    Err(XmlError::NoParserAvailable)
436}
437
438// to_string(&self) -> String
439
440#[cfg(feature = "xml")]
441pub fn translate_roxmltree_expandedname<'a, 'b>(
442    e: roxmltree::ExpandedName<'a, 'b>,
443) -> XmlQualifiedName {
444    let ns: Option<AzString> = e.namespace().map(|e| e.to_string().into());
445    XmlQualifiedName {
446        local_name: e.name().to_string().into(),
447        namespace: ns.into(),
448    }
449}
450
451#[cfg(feature = "xml")]
452fn translate_roxmltree_attribute(e: roxmltree::Attribute) -> XmlQualifiedName {
453    XmlQualifiedName {
454        local_name: e.name().to_string().into(),
455        namespace: e.namespace().map(|e| e.to_string().into()).into(),
456    }
457}
458
459#[cfg(feature = "xml")]
460fn translate_xmlparser_streamerror(e: xmlparser::StreamError) -> XmlStreamError {
461    match e {
462        xmlparser::StreamError::UnexpectedEndOfStream => XmlStreamError::UnexpectedEndOfStream,
463        xmlparser::StreamError::InvalidName => XmlStreamError::InvalidName,
464        xmlparser::StreamError::InvalidReference => XmlStreamError::InvalidReference,
465        xmlparser::StreamError::InvalidExternalID => XmlStreamError::InvalidExternalID,
466        xmlparser::StreamError::InvalidCommentData => XmlStreamError::InvalidCommentData,
467        xmlparser::StreamError::InvalidCommentEnd => XmlStreamError::InvalidCommentEnd,
468        xmlparser::StreamError::InvalidCharacterData => XmlStreamError::InvalidCharacterData,
469        xmlparser::StreamError::NonXmlChar(c, tp) => XmlStreamError::NonXmlChar(NonXmlCharError {
470            ch: c.into(),
471            pos: translate_xmlparser_textpos(tp),
472        }),
473        xmlparser::StreamError::InvalidChar(a, b, tp) => {
474            XmlStreamError::InvalidChar(InvalidCharError {
475                expected: a,
476                got: b,
477                pos: translate_xmlparser_textpos(tp),
478            })
479        }
480        xmlparser::StreamError::InvalidCharMultiple(a, b, tp) => {
481            XmlStreamError::InvalidCharMultiple(InvalidCharMultipleError {
482                expected: a,
483                got: b.to_vec().into(),
484                pos: translate_xmlparser_textpos(tp),
485            })
486        }
487        xmlparser::StreamError::InvalidQuote(a, tp) => {
488            XmlStreamError::InvalidQuote(InvalidQuoteError {
489                got: a.into(),
490                pos: translate_xmlparser_textpos(tp),
491            })
492        }
493        xmlparser::StreamError::InvalidSpace(a, tp) => {
494            XmlStreamError::InvalidSpace(InvalidSpaceError {
495                got: a.into(),
496                pos: translate_xmlparser_textpos(tp),
497            })
498        }
499        xmlparser::StreamError::InvalidString(a, tp) => {
500            XmlStreamError::InvalidString(InvalidStringError {
501                got: a.to_string().into(),
502                pos: translate_xmlparser_textpos(tp),
503            })
504        }
505    }
506}
507
508#[cfg(feature = "xml")]
509fn translate_xmlparser_error(e: xmlparser::Error) -> XmlParseError {
510    match e {
511        xmlparser::Error::InvalidDeclaration(se, tp) => {
512            XmlParseError::InvalidDeclaration(XmlTextError {
513                stream_error: translate_xmlparser_streamerror(se),
514                pos: translate_xmlparser_textpos(tp),
515            })
516        }
517        xmlparser::Error::InvalidComment(se, tp) => XmlParseError::InvalidComment(XmlTextError {
518            stream_error: translate_xmlparser_streamerror(se),
519            pos: translate_xmlparser_textpos(tp),
520        }),
521        xmlparser::Error::InvalidPI(se, tp) => XmlParseError::InvalidPI(XmlTextError {
522            stream_error: translate_xmlparser_streamerror(se),
523            pos: translate_xmlparser_textpos(tp),
524        }),
525        xmlparser::Error::InvalidDoctype(se, tp) => XmlParseError::InvalidDoctype(XmlTextError {
526            stream_error: translate_xmlparser_streamerror(se),
527            pos: translate_xmlparser_textpos(tp),
528        }),
529        xmlparser::Error::InvalidEntity(se, tp) => XmlParseError::InvalidEntity(XmlTextError {
530            stream_error: translate_xmlparser_streamerror(se),
531            pos: translate_xmlparser_textpos(tp),
532        }),
533        xmlparser::Error::InvalidElement(se, tp) => XmlParseError::InvalidElement(XmlTextError {
534            stream_error: translate_xmlparser_streamerror(se),
535            pos: translate_xmlparser_textpos(tp),
536        }),
537        xmlparser::Error::InvalidAttribute(se, tp) => {
538            XmlParseError::InvalidAttribute(XmlTextError {
539                stream_error: translate_xmlparser_streamerror(se),
540                pos: translate_xmlparser_textpos(tp),
541            })
542        }
543        xmlparser::Error::InvalidCdata(se, tp) => XmlParseError::InvalidCdata(XmlTextError {
544            stream_error: translate_xmlparser_streamerror(se),
545            pos: translate_xmlparser_textpos(tp),
546        }),
547        xmlparser::Error::InvalidCharData(se, tp) => XmlParseError::InvalidCharData(XmlTextError {
548            stream_error: translate_xmlparser_streamerror(se),
549            pos: translate_xmlparser_textpos(tp),
550        }),
551        xmlparser::Error::UnknownToken(tp) => {
552            XmlParseError::UnknownToken(translate_xmlparser_textpos(tp))
553        }
554    }
555}
556
557#[cfg(feature = "xml")]
558pub fn translate_roxmltree_error(e: roxmltree::Error) -> XmlError {
559    match e {
560        roxmltree::Error::InvalidXmlPrefixUri(s) => {
561            XmlError::InvalidXmlPrefixUri(translate_roxml_textpos(s))
562        }
563        roxmltree::Error::UnexpectedXmlUri(s) => {
564            XmlError::UnexpectedXmlUri(translate_roxml_textpos(s))
565        }
566        roxmltree::Error::UnexpectedXmlnsUri(s) => {
567            XmlError::UnexpectedXmlnsUri(translate_roxml_textpos(s))
568        }
569        roxmltree::Error::InvalidElementNamePrefix(s) => {
570            XmlError::InvalidElementNamePrefix(translate_roxml_textpos(s))
571        }
572        roxmltree::Error::DuplicatedNamespace(s, tp) => {
573            XmlError::DuplicatedNamespace(DuplicatedNamespaceError {
574                ns: s.into(),
575                pos: translate_roxml_textpos(tp),
576            })
577        }
578        roxmltree::Error::UnknownNamespace(s, tp) => {
579            XmlError::UnknownNamespace(UnknownNamespaceError {
580                ns: s.into(),
581                pos: translate_roxml_textpos(tp),
582            })
583        }
584        roxmltree::Error::UnexpectedCloseTag(expected, actual, pos) => {
585            XmlError::UnexpectedCloseTag(UnexpectedCloseTagError {
586                expected: expected.into(),
587                actual: actual.into(),
588                pos: translate_roxml_textpos(pos),
589            })
590        }
591        roxmltree::Error::UnexpectedEntityCloseTag(s) => {
592            XmlError::UnexpectedEntityCloseTag(translate_roxml_textpos(s))
593        }
594        roxmltree::Error::UnknownEntityReference(s, tp) => {
595            XmlError::UnknownEntityReference(UnknownEntityReferenceError {
596                entity: s.into(),
597                pos: translate_roxml_textpos(tp),
598            })
599        }
600        roxmltree::Error::MalformedEntityReference(s) => {
601            XmlError::MalformedEntityReference(translate_roxml_textpos(s))
602        }
603        roxmltree::Error::EntityReferenceLoop(s) => {
604            XmlError::EntityReferenceLoop(translate_roxml_textpos(s))
605        }
606        roxmltree::Error::InvalidAttributeValue(s) => {
607            XmlError::InvalidAttributeValue(translate_roxml_textpos(s))
608        }
609        roxmltree::Error::DuplicatedAttribute(s, tp) => {
610            XmlError::DuplicatedAttribute(DuplicatedAttributeError {
611                attribute: s.into(),
612                pos: translate_roxml_textpos(tp),
613            })
614        }
615        roxmltree::Error::NoRootNode => XmlError::NoRootNode,
616        roxmltree::Error::DtdDetected => XmlError::DtdDetected,
617        roxmltree::Error::UnclosedRootNode => XmlError::UnclosedRootNode,
618        roxmltree::Error::UnexpectedDeclaration(tp) => {
619            XmlError::UnexpectedDeclaration(translate_roxml_textpos(tp))
620        }
621        roxmltree::Error::NodesLimitReached => XmlError::NodesLimitReached,
622        roxmltree::Error::AttributesLimitReached => XmlError::AttributesLimitReached,
623        roxmltree::Error::NamespacesLimitReached => XmlError::NamespacesLimitReached,
624        roxmltree::Error::InvalidName(tp) => XmlError::InvalidName(translate_roxml_textpos(tp)),
625        roxmltree::Error::NonXmlChar(_, tp) => XmlError::NonXmlChar(translate_roxml_textpos(tp)),
626        roxmltree::Error::InvalidChar(_, _, tp) => {
627            XmlError::InvalidChar(translate_roxml_textpos(tp))
628        }
629        roxmltree::Error::InvalidChar2(_, _, tp) => {
630            XmlError::InvalidChar2(translate_roxml_textpos(tp))
631        }
632        roxmltree::Error::InvalidString(_, tp) => {
633            XmlError::InvalidString(translate_roxml_textpos(tp))
634        }
635        roxmltree::Error::InvalidExternalID(tp) => {
636            XmlError::InvalidExternalID(translate_roxml_textpos(tp))
637        }
638        roxmltree::Error::InvalidComment(tp) => {
639            XmlError::InvalidComment(translate_roxml_textpos(tp))
640        }
641        roxmltree::Error::InvalidCharacterData(tp) => {
642            XmlError::InvalidCharacterData(translate_roxml_textpos(tp))
643        }
644        roxmltree::Error::UnknownToken(tp) => XmlError::UnknownToken(translate_roxml_textpos(tp)),
645        roxmltree::Error::UnexpectedEndOfStream => XmlError::UnexpectedEndOfStream,
646        roxmltree::Error::EntityResolver(tp, s) => {
647            // New in roxmltree 0.21: EntityResolver error variant
648            // For now, treat as a generic entity reference error
649            XmlError::UnknownEntityReference(UnknownEntityReferenceError {
650                entity: s.into(),
651                pos: translate_roxml_textpos(tp),
652            })
653        }
654    }
655}
656
657#[cfg(feature = "xml")]
658#[inline(always)]
659const fn translate_xmlparser_textpos(o: xmlparser::TextPos) -> XmlTextPos {
660    XmlTextPos {
661        row: o.row,
662        col: o.col,
663    }
664}
665
666#[cfg(feature = "xml")]
667#[inline(always)]
668const fn translate_roxml_textpos(o: roxmltree::TextPos) -> XmlTextPos {
669    XmlTextPos {
670        row: o.row,
671        col: o.col,
672    }
673}
674
675/// Extension trait to add XML parsing capabilities to Dom
676///
677/// This trait provides methods to parse XML/XHTML strings and convert them
678/// into Azul DOM trees. It's implemented as a trait to avoid circular dependencies
679/// between azul-core and azul-layout.
680#[cfg(feature = "xml")]
681pub trait DomXmlExt {
682    /// Parse XML/XHTML string into a DOM tree
683    ///
684    /// This method parses the XML string and converts it to an Azul StyledDom.
685    /// On error, it returns a StyledDom displaying the error message.
686    ///
687    /// # Arguments
688    /// * `xml` - The XML/XHTML string to parse
689    ///
690    /// # Returns
691    /// A `StyledDom` tree representing the parsed XML, or an error DOM on parse failure
692    fn from_xml_string<S: AsRef<str>>(xml: S) -> StyledDom;
693}
694
695#[cfg(feature = "xml")]
696impl DomXmlExt for Dom {
697    fn from_xml_string<S: AsRef<str>>(xml: S) -> StyledDom {
698        let mut component_map = XmlComponentMap::default();
699        let dom_xml = domxml_from_str(xml.as_ref(), &mut component_map);
700        dom_xml.parsed_dom
701    }
702}