Skip to main content

orinium_browser/engine/html/
xml.rs

1//! Minimal XML parser that turns well-formed XML markup into a generic
2//! element tree.
3//!
4//! This is deliberately *not* a full XML spec implementation. It covers the
5//! structural subset that embedded documents (notably SVG) actually use:
6//! element/attribute/name constructs, self-closing tags, processing
7//! instructions (`<?...?>`), comments, and `<!DOCTYPE ...>` prologs. Element
8//! and attribute names are case-sensitive, and namespace prefixes are kept as
9//! literal name characters (`svg:path`), matching XML semantics rather than
10//! the HTML parser's case-insensitive handling.
11//!
12//! The parser is a thin tree-builder over [`super::tokenizer::Tokenizer`],
13//! reusing the shared HTML tokenizer for low-level lexing so that attribute
14//! value quoting, comment syntax, entity decoding, and whitespace handling
15//! all follow a single well-tested implementation.
16
17use super::tokenizer::{Attribute, Token, Tokenizer};
18use crate::engine::tree::{NodeRef, Tree, TreeNode};
19use std::rc::Rc;
20
21/// A parsed XML element: its (case-sensitive, possibly namespace-qualified)
22/// tag name and its attribute list.
23#[derive(Debug, Clone, PartialEq)]
24pub struct XmlElement {
25    pub name: String,
26    pub attributes: Vec<Attribute>,
27}
28
29impl XmlElement {
30    /// Returns the value of the attribute named `name`, or `None`.
31    pub fn attr(&self, name: &str) -> Option<&str> {
32        self.attributes
33            .iter()
34            .find(|attribute| attribute.name == name)
35            .map(|attribute| attribute.value.as_str())
36    }
37}
38
39/// Parses XML markup into an element tree.
40///
41/// The root of the returned tree is the document element (the first element of
42/// the document). Everything before it — the XML declaration `<?xml ...?>`,
43/// comments, and the `<!DOCTYPE ...>` prolog — is skipped. Returns an error
44/// when the markup is not well-formed.
45pub fn parse(markup: &str) -> Result<Tree<XmlElement>, String> {
46    let mut tokenizer = Tokenizer::new(markup);
47    let mut root: Option<NodeRef<XmlElement>> = None;
48    let mut stack: Vec<NodeRef<XmlElement>> = Vec::new();
49
50    while let Some(token) = tokenizer.next_token() {
51        match token {
52            Token::StartTag {
53                name,
54                attributes,
55                self_closing,
56            } => {
57                let node = TreeNode::new(XmlElement { name, attributes });
58                match stack.last() {
59                    Some(parent) => {
60                        if self_closing {
61                            TreeNode::add_child(parent, node);
62                        } else {
63                            TreeNode::add_child(parent, Rc::clone(&node));
64                            stack.push(Rc::clone(&node));
65                        }
66                    }
67                    None => {
68                        if self_closing {
69                            root = Some(node);
70                        } else {
71                            root = Some(Rc::clone(&node));
72                            stack.push(node);
73                        }
74                    }
75                }
76            }
77            Token::EndTag { name } => {
78                if let Some(top) = stack.last() {
79                    if top.borrow().value.name == name {
80                        stack.pop();
81                    } else {
82                        return Err(format!(
83                            "XML: mismatched closing tag </{name}> for <{}>",
84                            top.borrow().value.name
85                        ));
86                    }
87                }
88            }
89            _ => {}
90        }
91    }
92
93    match root {
94        Some(root) if stack.is_empty() => Ok(Tree::from_root(root)),
95        Some(_) => Err(format!(
96            "XML: unclosed element <{}>",
97            stack.last().unwrap().borrow().value.name
98        )),
99        None => Err("XML: no document element".to_string()),
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn parses_svg_document_with_pi_and_comment() {
109        let tree = parse(
110            r##"<?xml version="1.0" encoding="UTF-8"?>
111               <!-- a comment -->
112               <svg width="12" height="8" xmlns="http://www.w3.org/2000/svg">
113                 <g id="layer">
114                   <rect width="12" height="8" fill="#ff0000"/>
115                   <path d="M 0 0 L 1 1"/>
116                 </g>
117               </svg>"##,
118        )
119        .expect("parses");
120        let root = tree.root.borrow();
121        assert_eq!(root.value.name, "svg");
122        assert_eq!(root.value.attr("width"), Some("12"));
123        let children = root.children().to_vec();
124        assert_eq!(children.len(), 1);
125        assert_eq!(children[0].borrow().value.name, "g");
126        let g_children = children[0].borrow().children().to_vec();
127        assert_eq!(g_children.len(), 2);
128        assert_eq!(g_children[0].borrow().value.name, "rect");
129        assert_eq!(g_children[0].borrow().value.attr("fill"), Some("#ff0000"));
130        assert_eq!(g_children[1].borrow().value.name, "path");
131    }
132
133    #[test]
134    fn keeps_namespace_qualified_names() {
135        let tree = parse(
136            r#"<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
137                 <inkscape:label foo:bar="1"/>
138               </svg>"#,
139        )
140        .expect("parses");
141        let root = tree.root.borrow();
142        assert_eq!(root.value.name, "svg");
143        let ns = &root.children()[0].borrow().value;
144        assert_eq!(ns.name, "inkscape:label");
145        assert_eq!(ns.attr("foo:bar"), Some("1"));
146    }
147
148    #[test]
149    fn element_names_are_case_sensitive() {
150        let tree = parse(r#"<svg><path d="M0 0"/><PATH d="x"/></svg>"#).expect("parses");
151        let root = tree.root.borrow();
152        let children = root.children().to_vec();
153        assert_eq!(children.len(), 2);
154        assert_eq!(children[0].borrow().value.name, "path");
155        assert_eq!(children[1].borrow().value.name, "PATH");
156    }
157
158    #[test]
159    fn accepts_well_formed_and_rejects_malformed() {
160        assert!(parse("<svg><rect/></svg>").is_ok());
161        assert!(parse("<svg/>").is_ok());
162        assert!(parse("<svg><rect/></svg>trailing").is_ok());
163        assert!(parse("<svg>").is_err());
164        assert!(parse("<svg><rect/></g>").is_err());
165        assert!(parse("plain text").is_err());
166    }
167}