Skip to main content

easyofd_core/
xml_element.rs

1//! XML 元素树抽象。
2//!
3//! 对应 Java: org.ofdrw.core.OFDElement
4//!
5//! ofdrw 中每个 OFD 类型都是 XML 元素(可 `toXML()` 自序列化、从 XML 解析)。
6//! 本模块用 Rust trait 复刻该范式:类型实现 [`XmlElement`],提供元素名、
7//! 属性、子元素与文本;[`to_xml`](XmlElement::to_xml) 手工拼接 XML(对应
8//! ofdrw writer 侧),[`from_xml`](XmlElement::from_xml) 从
9//! [`XmlNode`] 解析(quick-xml 解析产物)。
10
11use std::fmt::Write as _;
12
13/// XML 节点(解析中间表示)。
14///
15/// quick-xml 解析结果归一化为统一的节点树,`XmlElement::from_xml` 从节点
16/// 恢复类型。
17#[derive(Debug, Clone, PartialEq)]
18pub struct XmlNode {
19    /// 元素名(不含命名空间前缀,如 "TextObject")。
20    pub name: String,
21    /// 属性列表(保序)。
22    pub attrs: Vec<(String, String)>,
23    /// 子元素。
24    pub children: Vec<XmlNode>,
25    /// 文本内容(若为文本节点)。
26    pub text: Option<String>,
27}
28
29impl XmlNode {
30    /// 创建元素节点。
31    #[must_use]
32    pub fn element(name: impl Into<String>) -> Self {
33        Self {
34            name: name.into(),
35            attrs: Vec::new(),
36            children: Vec::new(),
37            text: None,
38        }
39    }
40
41    /// 创建文本节点。
42    #[must_use]
43    pub fn text_node(text: impl Into<String>) -> Self {
44        Self {
45            name: String::new(),
46            attrs: Vec::new(),
47            children: Vec::new(),
48            text: Some(text.into()),
49        }
50    }
51
52    /// 添加属性。
53    #[must_use]
54    pub fn attr(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
55        self.attrs.push((key.into(), value.into()));
56        self
57    }
58
59    /// 添加子元素。
60    pub fn push_child(&mut self, child: XmlNode) {
61        self.children.push(child);
62    }
63
64    /// 读取属性值。
65    #[must_use]
66    pub fn get_attr(&self, key: &str) -> Option<&str> {
67        self.attrs
68            .iter()
69            .find(|(k, _)| k == key)
70            .map(|(_, v)| v.as_str())
71    }
72
73    /// 首个匹配名字的子元素。
74    #[must_use]
75    pub fn child(&self, name: &str) -> Option<&XmlNode> {
76        self.children.iter().find(|c| c.name == name)
77    }
78
79    /// 所有匹配名字的子元素。
80    pub fn children_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a XmlNode> {
81        self.children.iter().filter(move |c| c.name == name)
82    }
83
84    /// 将节点序列化为 XML 片段(不含 XML 声明)。
85    ///
86    /// 对应 Java: OFDElement.toString()
87    #[must_use]
88    pub fn to_xml_string(&self) -> String {
89        let mut out = String::new();
90        self.write_self_xml(&mut out);
91        out
92    }
93}
94
95/// XML 元素 trait(对应 Java: ofdrw OFDElement)。
96///
97/// 实现者描述一个 OFD XML 元素:元素名、属性、子元素、文本。
98/// [`to_xml`](XmlElement::to_xml) 手工拼 XML(writer 侧),
99/// [`from_xml`](XmlElement::from_xml) 从节点树解析(quick-xml 解析侧)。
100pub trait XmlElement {
101    /// 元素名(如 "TextObject"、"ofd:Page")。
102    fn element_name(&self) -> &'static str;
103
104    /// XML 属性列表。
105    fn attributes(&self) -> Vec<(String, String)>;
106
107    /// 子元素(作为 `XmlNode` 提供,便于嵌套序列化)。
108    fn child_nodes(&self) -> Vec<XmlNode> {
109        Vec::new()
110    }
111
112    /// 文本内容(简单文本元素)。
113    fn text_content(&self) -> Option<&str> {
114        None
115    }
116
117    /// 手工拼接 XML(对应 ofdrw writer 侧;子元素由 `child_nodes` 递归)。
118    fn to_xml(&self) -> String {
119        let mut out = String::new();
120        self.write_xml(&mut out);
121        out
122    }
123
124    /// 将节点树写为 XML 片段(无 XML 声明)。
125    fn write_xml(&self, out: &mut String) {
126        let name = self.element_name();
127        out.push('<');
128        out.push_str(name);
129        for (k, v) in self.attributes() {
130            out.push(' ');
131            out.push_str(&k);
132            out.push_str("=\"");
133            out.push_str(&xml_escape(&v));
134            out.push('"');
135        }
136        let children = self.child_nodes();
137        let text = self.text_content();
138        if children.is_empty() && text.is_none_or(|s| s.is_empty()) {
139            out.push_str("/>");
140            return;
141        }
142        out.push('>');
143        if let Some(t) = text {
144            out.push_str(&xml_escape(t));
145        }
146        for child in children {
147            child.write_self_xml(out);
148        }
149        out.push_str("</");
150        out.push_str(name);
151        out.push('>');
152    }
153
154    /// 从节点树解析(对应 ofdrw 解析侧;quick-xml 产物)。
155    ///
156    /// # 错误
157    ///
158    /// 节点与元素结构不匹配时返回错误。
159    fn from_xml(node: &XmlNode) -> Result<Self, XmlElementError>
160    where
161        Self: Sized;
162}
163
164/// 解析错误。
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct XmlElementError(pub String);
167
168impl std::fmt::Display for XmlElementError {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        f.write_str(&self.0)
171    }
172}
173
174impl std::error::Error for XmlElementError {}
175
176impl XmlNode {
177    /// 递归写 XML 片段。
178    ///
179    /// 空元素自闭合:无子节点或仅有空文本子节点时输出 `<Tag/>`,
180    /// 匹配 ofdrw/dom4j 默认行为(如 `<ofd:Keywords/>`)。
181    fn write_self_xml(&self, out: &mut String) {
182        if let Some(text) = &self.text {
183            out.push_str(&xml_escape(text));
184            return;
185        }
186        let _ = write!(out, "<{}", self.name);
187        for (k, v) in &self.attrs {
188            let _ = write!(out, " {k}=\"{}\"", xml_escape(v));
189        }
190        // 判断是否为空内容:无子节点,或唯一子节点是空文本
191        let is_empty = self.children.is_empty()
192            || (self.children.len() == 1
193                && self.children[0].name.is_empty()
194                && self.children[0].text.as_deref().is_some_and(str::is_empty));
195        if is_empty {
196            out.push_str("/>");
197            return;
198        }
199        out.push('>');
200        for child in &self.children {
201            child.write_self_xml(out);
202        }
203        let _ = write!(out, "</{}>", self.name);
204    }
205}
206
207/// XML 特殊字符转义。
208#[must_use]
209pub fn xml_escape(s: &str) -> String {
210    let mut out = String::with_capacity(s.len());
211    for c in s.chars() {
212        match c {
213            '&' => out.push_str("&amp;"),
214            '<' => out.push_str("&lt;"),
215            '>' => out.push_str("&gt;"),
216            '"' => out.push_str("&quot;"),
217            '\'' => out.push_str("&apos;"),
218            _ => out.push(c),
219        }
220    }
221    out
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    struct SampleElement {
229        id: String,
230        name: String,
231    }
232
233    impl XmlElement for SampleElement {
234        fn element_name(&self) -> &'static str {
235            "Sample"
236        }
237
238        fn attributes(&self) -> Vec<(String, String)> {
239            vec![("ID".to_string(), self.id.clone())]
240        }
241
242        fn text_content(&self) -> Option<&str> {
243            Some(&self.name)
244        }
245
246        fn from_xml(node: &XmlNode) -> Result<Self, XmlElementError> {
247            Ok(Self {
248                id: node.get_attr("ID").unwrap_or_default().to_string(),
249                name: node.text.clone().unwrap_or_default(),
250            })
251        }
252    }
253
254    #[test]
255    fn test_to_xml_simple() {
256        let el = SampleElement {
257            id: "1".to_string(),
258            name: "测试 & 数据".to_string(),
259        };
260        let xml = el.to_xml();
261        assert_eq!(xml, r#"<Sample ID="1">测试 &amp; 数据</Sample>"#);
262    }
263
264    #[test]
265    fn test_node_roundtrip() {
266        let mut node = XmlNode::element("Page")
267            .attr("ID", "5")
268            .attr("BaseLoc", "Pages/Page_0/Content.xml");
269        node.push_child(XmlNode::element("Layer"));
270        assert_eq!(node.get_attr("ID"), Some("5"));
271        assert_eq!(node.child("Layer").unwrap().name, "Layer");
272
273        let mut out = String::new();
274        node.write_self_xml(&mut out);
275        let xml = out;
276        assert!(xml.starts_with(r#"<Page ID="5" BaseLoc="Pages/Page_0/Content.xml">"#));
277        assert!(xml.contains("<Layer/>"));
278    }
279
280    #[test]
281    fn test_xml_escape() {
282        assert_eq!(xml_escape("<a&b\"c'>"), "&lt;a&amp;b&quot;c&apos;&gt;");
283    }
284}