Skip to main content

xml_3dm/node/
namespace.rs

1//! Namespace handling for XML elements.
2
3use std::collections::HashMap;
4use std::rc::Rc;
5
6/// Represents an expanded XML name (namespace URI + local name).
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct ExpandedName {
9    /// The namespace URI (empty string for no namespace).
10    pub namespace_uri: Rc<str>,
11    /// The local part of the name (without prefix).
12    pub local_name: String,
13}
14
15impl ExpandedName {
16    /// Creates a new expanded name with a namespace.
17    pub fn new(uri: impl Into<Rc<str>>, local: impl Into<String>) -> Self {
18        Self {
19            namespace_uri: uri.into(),
20            local_name: local.into(),
21        }
22    }
23
24    /// Creates an expanded name with no namespace.
25    pub fn no_namespace(local: impl Into<String>) -> Self {
26        Self {
27            namespace_uri: "".into(),
28            local_name: local.into(),
29        }
30    }
31}
32
33/// Tracks namespace bindings during parsing.
34pub struct NamespaceContext {
35    /// URI interning cache for memory efficiency.
36    uri_cache: HashMap<String, Rc<str>>,
37    /// Stack of scopes, each containing prefix -> URI bindings.
38    scopes: Vec<HashMap<String, Rc<str>>>,
39}
40
41impl Default for NamespaceContext {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl NamespaceContext {
48    /// Creates a new namespace context with XML namespace pre-bound.
49    pub fn new() -> Self {
50        let mut ctx = NamespaceContext {
51            uri_cache: HashMap::new(),
52            scopes: vec![HashMap::new()],
53        };
54        // xml prefix is always bound
55        ctx.bind("xml", "http://www.w3.org/XML/1998/namespace");
56        ctx
57    }
58
59    /// Pushes a new scope for entering an element.
60    pub fn push_scope(&mut self) {
61        self.scopes.push(HashMap::new());
62    }
63
64    /// Pops the current scope when leaving an element.
65    pub fn pop_scope(&mut self) {
66        if self.scopes.len() > 1 {
67            self.scopes.pop();
68        }
69    }
70
71    /// Binds a prefix to a URI in the current scope.
72    pub fn bind(&mut self, prefix: &str, uri: &str) {
73        let uri_rc = self.intern_uri(uri);
74        if let Some(scope) = self.scopes.last_mut() {
75            scope.insert(prefix.to_string(), uri_rc);
76        }
77    }
78
79    /// Resolves a prefix to its URI, searching from innermost scope.
80    pub fn resolve(&self, prefix: &str) -> Option<Rc<str>> {
81        for scope in self.scopes.iter().rev() {
82            if let Some(uri) = scope.get(prefix) {
83                return Some(uri.clone());
84            }
85        }
86        None
87    }
88
89    /// Returns the default namespace (empty prefix binding).
90    pub fn default_namespace(&self) -> Option<Rc<str>> {
91        self.resolve("")
92    }
93
94    /// Interns a URI string for memory efficiency.
95    pub fn intern_uri(&mut self, uri: &str) -> Rc<str> {
96        if let Some(cached) = self.uri_cache.get(uri) {
97            cached.clone()
98        } else {
99            let rc: Rc<str> = uri.into();
100            self.uri_cache.insert(uri.to_string(), rc.clone());
101            rc
102        }
103    }
104}
105
106/// Splits a qualified name into prefix and local name.
107///
108/// Returns (Some(prefix), local) for "prefix:local"
109/// Returns (None, name) for "name" without prefix
110pub fn split_qname(qname: &str) -> (Option<&str>, &str) {
111    if let Some(pos) = qname.find(':') {
112        (Some(&qname[..pos]), &qname[pos + 1..])
113    } else {
114        (None, qname)
115    }
116}
117
118/// Checks if an attribute name is a namespace declaration.
119pub fn is_xmlns_attr(name: &str) -> bool {
120    name == "xmlns" || name.starts_with("xmlns:")
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_split_qname() {
129        assert_eq!(split_qname("svg:rect"), (Some("svg"), "rect"));
130        assert_eq!(split_qname("rect"), (None, "rect"));
131        assert_eq!(split_qname("ns:foo:bar"), (Some("ns"), "foo:bar"));
132    }
133
134    #[test]
135    fn test_namespace_context() {
136        let mut ctx = NamespaceContext::new();
137        ctx.push_scope();
138        ctx.bind("svg", "http://www.w3.org/2000/svg");
139
140        assert!(ctx.resolve("svg").is_some());
141        assert_eq!(
142            ctx.resolve("svg").unwrap().as_ref(),
143            "http://www.w3.org/2000/svg"
144        );
145
146        ctx.pop_scope();
147        assert!(ctx.resolve("svg").is_none());
148    }
149
150    #[test]
151    fn test_is_xmlns() {
152        assert!(is_xmlns_attr("xmlns"));
153        assert!(is_xmlns_attr("xmlns:svg"));
154        assert!(!is_xmlns_attr("xml:space"));
155        assert!(!is_xmlns_attr("href"));
156    }
157
158    #[test]
159    fn test_default_namespace() {
160        let mut ctx = NamespaceContext::new();
161        assert!(ctx.default_namespace().is_none());
162
163        ctx.push_scope();
164        ctx.bind("", "http://www.w3.org/1999/xhtml");
165        assert_eq!(
166            ctx.default_namespace().unwrap().as_ref(),
167            "http://www.w3.org/1999/xhtml"
168        );
169
170        ctx.pop_scope();
171        assert!(ctx.default_namespace().is_none());
172    }
173
174    #[test]
175    fn test_xml_prefix_always_bound() {
176        let ctx = NamespaceContext::new();
177        assert_eq!(
178            ctx.resolve("xml").unwrap().as_ref(),
179            "http://www.w3.org/XML/1998/namespace"
180        );
181    }
182
183    #[test]
184    fn test_scope_inheritance() {
185        let mut ctx = NamespaceContext::new();
186        ctx.push_scope();
187        ctx.bind("a", "http://example.com/a");
188
189        ctx.push_scope();
190        ctx.bind("b", "http://example.com/b");
191
192        // Both should be visible
193        assert!(ctx.resolve("a").is_some());
194        assert!(ctx.resolve("b").is_some());
195
196        ctx.pop_scope();
197        // Only a should remain
198        assert!(ctx.resolve("a").is_some());
199        assert!(ctx.resolve("b").is_none());
200    }
201}