Skip to main content

easyofd_core/
xml_parse.rs

1//! quick-xml 解析桥接:XML 字符串 → [`XmlNode`] 节点树。
2//!
3//! 对应 Java: ofdrw 解析侧(Element 代理)。
4
5use quick_xml::Reader;
6use quick_xml::events::Event;
7
8use crate::xml_element::XmlNode;
9
10/// 将 XML 字符串解析为 `XmlNode` 节点树(首个根元素)。
11///
12/// # 错误
13///
14/// XML 语法错误时返回错误。
15pub fn parse_xml_to_nodes(xml: &str) -> Result<XmlNode, String> {
16    let mut reader = Reader::from_str(xml);
17    reader.config_mut().trim_text(false);
18    let mut buf = Vec::new();
19    let mut stack: Vec<XmlNode> = Vec::new();
20    let mut root: Option<XmlNode> = None;
21
22    loop {
23        match reader.read_event_into(&mut buf) {
24            Ok(Event::Start(e)) => {
25                let name = local_name(e.name().as_ref());
26                let mut node = XmlNode::element(name);
27                for attr in e.attributes().flatten() {
28                    let key = local_name(attr.key.as_ref());
29                    let value = attr
30                        .normalized_value(quick_xml::XmlVersion::Explicit1_0)
31                        .unwrap_or_default()
32                        .to_string();
33                    node.attrs.push((key, value));
34                }
35                stack.push(node);
36            }
37            Ok(Event::Empty(e)) => {
38                let name = local_name(e.name().as_ref());
39                let mut node = XmlNode::element(name);
40                for attr in e.attributes().flatten() {
41                    let key = local_name(attr.key.as_ref());
42                    let value = attr
43                        .normalized_value(quick_xml::XmlVersion::Explicit1_0)
44                        .unwrap_or_default()
45                        .to_string();
46                    node.attrs.push((key, value));
47                }
48                attach_node(&mut stack, &mut root, node);
49            }
50            Ok(Event::Text(e)) => {
51                let text = e.xml10_content().into_owned();
52                // Append (not assign): quick-xml may split a text node into
53                // multiple Text events around entity references.  We keep ALL
54                // text including whitespace-only fragments so that the tree
55                // matches the flat event-stream behaviour (callers trim at the
56                // call site via `find_text_deep` / `find_optional_text_deep`).
57                if let Some(top) = stack.last_mut() {
58                    match &mut top.text {
59                        Some(existing) => existing.push_str(&text),
60                        None => top.text = Some(text),
61                    }
62                }
63            }
64            Ok(Event::GeneralRef(e)) => {
65                let name = e.xml10_content().into_owned();
66                if let Some(ch) = resolve_xml_entity_ref(&name) {
67                    if let Some(top) = stack.last_mut() {
68                        match &mut top.text {
69                            Some(existing) => existing.push(ch),
70                            None => top.text = Some(ch.to_string()),
71                        }
72                    }
73                }
74            }
75            Ok(Event::End(_)) => {
76                if let Some(node) = stack.pop() {
77                    attach_node(&mut stack, &mut root, node);
78                }
79            }
80            Ok(Event::Eof) => break,
81            Err(e) => return Err(format!("XML 解析失败: {e}")),
82            _ => {}
83        }
84        buf.clear();
85    }
86
87    root.ok_or_else(|| "XML 无根元素".to_string())
88}
89
90/// 去掉命名空间前缀("ofd:Page" → "Page")。
91fn local_name(name: &str) -> String {
92    match name.rsplit_once(':') {
93        Some((_, local)) => local.to_string(),
94        None => name.to_string(),
95    }
96}
97
98/// Resolve an XML entity reference name (e.g. "amp", "apos", "#x41") to a char.
99///
100/// Handles the 5 predefined XML entities and numeric character references.
101fn resolve_xml_entity_ref(name: &str) -> Option<char> {
102    if let Some(entity) = quick_xml::escape::resolve_xml_entity(name) {
103        return entity.chars().next();
104    }
105    let number = if let Some(hex) = name.strip_prefix("#x") {
106        u32::from_str_radix(hex, 16).ok()
107    } else if let Some(decimal) = name.strip_prefix('#') {
108        decimal.parse().ok()
109    } else {
110        None
111    }?;
112    char::from_u32(number)
113}
114
115/// 将节点挂到栈顶父节点或作为根。
116fn attach_node(stack: &mut [XmlNode], root: &mut Option<XmlNode>, node: XmlNode) {
117    match stack.last_mut() {
118        Some(parent) => parent.children.push(node),
119        None => *root = Some(node),
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_parse_simple() {
129        let xml = r#"<ofd:Page ID="1" BaseLoc="Pages/Page_0/Content.xml"/>"#;
130        let node = parse_xml_to_nodes(xml).unwrap();
131        assert_eq!(node.name, "Page");
132        assert_eq!(node.get_attr("ID"), Some("1"));
133        assert_eq!(node.get_attr("BaseLoc"), Some("Pages/Page_0/Content.xml"));
134    }
135
136    #[test]
137    fn test_parse_nested_with_text() {
138        let xml = r"<Document><CommonData><MaxUnitID>88</MaxUnitID></CommonData></Document>";
139        let node = parse_xml_to_nodes(xml).unwrap();
140        let common = node.child("CommonData").unwrap();
141        let max = common.child("MaxUnitID").unwrap();
142        assert_eq!(max.text.as_deref(), Some("88"));
143    }
144
145    #[test]
146    fn test_parse_multiple_children() {
147        let xml = r#"<Pages><Page ID="1"/><Page ID="2"/></Pages>"#;
148        let node = parse_xml_to_nodes(xml).unwrap();
149        let pages: Vec<_> = node.children_named("Page").collect();
150        assert_eq!(pages.len(), 2);
151        assert_eq!(pages[0].get_attr("ID"), Some("1"));
152    }
153}