Skip to main content

easyofd_reader/tools/
namespace_cleaner.rs

1//! 命名空间清理工具。
2//!
3//! 对应 Java: org.ofdrw.reader.tools.NameSpaceCleaner
4//!
5//! 从 OFD XML 中移除命名空间前缀,使其可以被不支持命名空间的解析器处理。
6
7/// OFD 命名空间清理器。
8///
9/// 对应 Java: `org.ofdrw.reader.tools.NameSpaceCleaner`
10///
11/// 将 XML 中的命名空间前缀(如 `ofd:`)移除,使元素名变为
12/// 无前缀的本地名称。这在需要与不支持 XML 命名空间的工具交互时有用。
13#[derive(Debug, Clone, Copy)]
14pub struct NamespaceCleaner;
15
16impl NamespaceCleaner {
17    /// 从 XML 字符串中移除指定的命名空间前缀。
18    ///
19    /// 将所有 `ofd:ElementName` 替换为 `ElementName`。
20    #[must_use]
21    pub fn remove_prefix(xml: &str, prefix: &str) -> String {
22        let with_colon = format!("{prefix}:");
23        xml.replace(&with_colon, "")
24    }
25
26    /// 从 XML 字符串中移除 OFD 命名空间前缀。
27    #[must_use]
28    pub fn remove_ofd_prefix(xml: &str) -> String {
29        Self::remove_prefix(xml, "ofd")
30    }
31
32    /// 从 XML 字符串中移除命名空间声明属性。
33    ///
34    /// 移除 `xmlns:ofd="..."` 形式的属性。
35    #[must_use]
36    pub fn remove_namespace_declarations(xml: &str) -> String {
37        let mut result = String::with_capacity(xml.len());
38        let mut remaining = xml;
39
40        while let Some(start) = remaining.find("xmlns:") {
41            result.push_str(&remaining[..start]);
42            // 找到属性值的结束引号
43            let after_attr = &remaining[start..];
44            if let Some(eq_pos) = after_attr.find('=').map(|p| p + 1) {
45                let after_eq = &after_attr[eq_pos..];
46                // 跳过引号内的内容
47                if let Some(quote_end) = find_closing_quote(after_eq) {
48                    remaining = &after_eq[quote_end + 1..];
49                } else {
50                    // 没有找到关闭引号,保留剩余内容
51                    result.push_str(after_attr);
52                    break;
53                }
54            } else {
55                result.push_str(after_attr);
56                break;
57            }
58        }
59        result.push_str(remaining);
60        result
61    }
62}
63
64/// 查找引号字符串的关闭位置。
65fn find_closing_quote(s: &str) -> Option<usize> {
66    let s = s.trim_start();
67    let quote_char = s.as_bytes().first()?;
68    if *quote_char != b'"' && *quote_char != b'\'' {
69        return None;
70    }
71    let content = &s[1..];
72    content
73        .find(*quote_char as char)
74        .map(|pos| pos + 1 + (s.len() - content.len()))
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_remove_ofd_prefix() {
83        let xml = "<ofd:OFD><ofd:DocBody><ofd:DocRoot/></ofd:DocBody></ofd:OFD>";
84        let result = NamespaceCleaner::remove_ofd_prefix(xml);
85        assert!(!result.contains("ofd:"));
86        assert!(result.contains("<OFD>"));
87        assert!(result.contains("<DocBody>"));
88        assert!(result.contains("<DocRoot/>"));
89        assert!(result.contains("</OFD>"));
90    }
91
92    #[test]
93    fn test_remove_custom_prefix() {
94        let xml = "<ns:Root><ns:Child/></ns:Root>";
95        let result = NamespaceCleaner::remove_prefix(xml, "ns");
96        assert_eq!(result, "<Root><Child/></Root>");
97    }
98
99    #[test]
100    fn test_remove_namespace_declarations() {
101        let xml =
102            r#"<ofd:OFD xmlns:ofd="http://www.ofdspec.org/2016" xmlns:custom="http://custom">"#;
103        let result = NamespaceCleaner::remove_namespace_declarations(xml);
104        assert!(!result.contains("xmlns:ofd"));
105        assert!(!result.contains("xmlns:custom"));
106        assert!(result.contains("<ofd:OFD"));
107    }
108
109    #[test]
110    fn test_no_prefix() {
111        let xml = "<Root><Child/></Root>";
112        let result = NamespaceCleaner::remove_ofd_prefix(xml);
113        assert_eq!(result, xml);
114    }
115
116    #[test]
117    fn test_empty_xml() {
118        let result = NamespaceCleaner::remove_ofd_prefix("");
119        assert!(result.is_empty());
120    }
121}