minidom: breaking: Allow getting Element attributes by namespace

Maxime “pep” Buquet created

- Move Element.attributes to `AttrMap`, slowly using rxml's features and
  unrolling our own.
- Add Element::attr_ns that requires the attribute namespace.
  Element:attr defaults to rxml::Namespace::none() but the interface
  changes nonetheless for a &NcNameStr. Similar changes on
  `ElementBuilder` methods.
- Remove iterator structs for attributes, return a ref on the AttrMap
  directly as we don't need to keep attributes' internals hidden
  anymore.
- Enable rxml's `macros` feature within tests to access the `xml_ncname`
  macro.

Signed-off-by: pep <pep@bouah.net>

Change summary

jid/src/lib.rs                  |  45 ++++++++--
minidom/CHANGELOG.md            |   6 +
minidom/Cargo.toml              |   3 
minidom/src/element.rs          | 138 ++++++++++++++++------------------
minidom/src/tests.rs            |  37 ++++++---
minidom/src/tree_builder.rs     |  56 ++++++++++---
parsers/src/extdisco.rs         |   2 
parsers/src/jingle_s5b.rs       |  11 +-
parsers/src/presence.rs         |  17 +++-
parsers/src/receipts.rs         |   5 
parsers/src/util/macro_tests.rs |   5 
parsers/src/util/macros.rs      |  12 +-
parsers/src/xhtml.rs            |  89 ++++++++++++++-------
xso/src/minidom_compat.rs       |  82 ++------------------
14 files changed, 272 insertions(+), 236 deletions(-)

Detailed changes

jid/src/lib.rs 🔗

@@ -1126,19 +1126,35 @@ mod tests {
     #[test]
     fn minidom() {
         let elem: minidom::Element = "<message xmlns='ns1' from='a@b/c'/>".parse().unwrap();
-        let to: Jid = elem.attr("from").unwrap().parse().unwrap();
+        let to: Jid = elem
+            .attr("from".try_into().unwrap())
+            .unwrap()
+            .parse()
+            .unwrap();
         assert_eq!(to, Jid::from(FullJid::new("a@b/c").unwrap()));
 
         let elem: minidom::Element = "<message xmlns='ns1' from='a@b'/>".parse().unwrap();
-        let to: Jid = elem.attr("from").unwrap().parse().unwrap();
+        let to: Jid = elem
+            .attr("from".try_into().unwrap())
+            .unwrap()
+            .parse()
+            .unwrap();
         assert_eq!(to, Jid::from(BareJid::new("a@b").unwrap()));
 
         let elem: minidom::Element = "<message xmlns='ns1' from='a@b/c'/>".parse().unwrap();
-        let to: FullJid = elem.attr("from").unwrap().parse().unwrap();
+        let to: FullJid = elem
+            .attr("from".try_into().unwrap())
+            .unwrap()
+            .parse()
+            .unwrap();
         assert_eq!(to, FullJid::new("a@b/c").unwrap());
 
         let elem: minidom::Element = "<message xmlns='ns1' from='a@b'/>".parse().unwrap();
-        let to: BareJid = elem.attr("from").unwrap().parse().unwrap();
+        let to: BareJid = elem
+            .attr("from".try_into().unwrap())
+            .unwrap()
+            .parse()
+            .unwrap();
         assert_eq!(to, BareJid::new("a@b").unwrap());
     }
 
@@ -1147,21 +1163,30 @@ mod tests {
     fn minidom_into_attr() {
         let full = FullJid::new("a@b/c").unwrap();
         let elem = minidom::Element::builder("message", "jabber:client")
-            .attr("from", full.clone())
+            .attr("from".try_into().unwrap(), full.clone())
             .build();
-        assert_eq!(elem.attr("from"), Some(full.to_string().as_str()));
+        assert_eq!(
+            elem.attr("from".try_into().unwrap()),
+            Some(full.to_string().as_str())
+        );
 
         let bare = BareJid::new("a@b").unwrap();
         let elem = minidom::Element::builder("message", "jabber:client")
-            .attr("from", bare.clone())
+            .attr("from".try_into().unwrap(), bare.clone())
             .build();
-        assert_eq!(elem.attr("from"), Some(bare.to_string().as_str()));
+        assert_eq!(
+            elem.attr("from".try_into().unwrap()),
+            Some(bare.to_string().as_str())
+        );
 
         let jid = Jid::from(bare.clone());
         let _elem = minidom::Element::builder("message", "jabber:client")
-            .attr("from", jid)
+            .attr("from".try_into().unwrap(), jid)
             .build();
-        assert_eq!(elem.attr("from"), Some(bare.to_string().as_str()));
+        assert_eq!(
+            elem.attr("from".try_into().unwrap()),
+            Some(bare.to_string().as_str())
+        );
     }
 
     #[test]

minidom/CHANGELOG.md 🔗

@@ -2,6 +2,12 @@ Version NEXT:
   * Breaking:
     * Removed 'std' feature for now because it makes build fail on
       no-default-features.
+    * Add `Element::attr_ns` that requires the attribute namespace in addition
+      to `Element::attr` (which defaults to `rxml::Namespace::none()`. Similar
+      changes on `ElementBuilder` methods.
+      `Element` now uses `AttrMap` to store attributes, and this also implies
+      some more changes in the Element interface such as iterators on
+      attributes.
   * Changes
     * Use thiserror for the error type. (!616)
 

minidom/Cargo.toml 🔗

@@ -23,3 +23,6 @@ gitlab = { repository = "xmpp-rs/xmpp-rs" }
 [dependencies]
 rxml = { version = "0.13.1", default-features = false, features = [ "std", "compact_str" ] }
 thiserror = "2.0"
+
+[dev-dependencies]
+rxml = { version = "0.13.1", default-features = false, features = [ "std", "compact_str", "macros" ] }

minidom/src/element.rs 🔗

@@ -20,7 +20,6 @@ use crate::prefixes::{Namespace, Prefix, Prefixes};
 use crate::tree_builder::TreeBuilder;
 
 use alloc::borrow::Cow;
-use alloc::collections::btree_map::{self, BTreeMap};
 use alloc::string::String;
 use alloc::vec::Vec;
 
@@ -30,7 +29,7 @@ use core::str::FromStr;
 use std::io;
 
 use rxml::writer::{Encoder, Item, TrackNamespace};
-use rxml::{Namespace as RxmlNamespace, RawReader, XmlVersion};
+use rxml::{AttrMap, Namespace as RxmlNamespace, NcName, NcNameStr, RawReader, XmlVersion};
 
 fn encode_and_write<W: io::Write, T: rxml::writer::TrackNamespace>(
     item: Item<'_>,
@@ -121,7 +120,7 @@ pub struct Element {
     namespace: String,
     /// Namespace declarations
     pub prefixes: Prefixes,
-    attributes: BTreeMap<String, String>,
+    attributes: AttrMap,
     children: Vec<Node>,
 }
 
@@ -162,7 +161,7 @@ impl Element {
         name: String,
         namespace: String,
         prefixes: P,
-        attributes: BTreeMap<String, String>,
+        attributes: AttrMap,
         children: Vec<Node>,
     ) -> Element {
         Element {
@@ -180,16 +179,17 @@ impl Element {
     ///
     /// ```rust
     /// use minidom::Element;
+    /// use rxml::{Namespace, xml_ncname};
     ///
     /// let elem = Element::builder("name", "namespace")
-    ///                    .attr("name", "value")
+    ///                    .attr(xml_ncname!("name").to_owned(), "value")
     ///                    .append("inner")
     ///                    .build();
     ///
     /// assert_eq!(elem.name(), "name");
     /// assert_eq!(elem.ns(), "namespace".to_owned());
-    /// assert_eq!(elem.attr("name"), Some("value"));
-    /// assert_eq!(elem.attr("inexistent"), None);
+    /// assert_eq!(elem.attr(xml_ncname!("name")), Some("value"));
+    /// assert_eq!(elem.attr(xml_ncname!("inexistent")), None);
     /// assert_eq!(elem.text(), "inner");
     /// ```
     pub fn builder<S: AsRef<str>, NS: Into<String>>(name: S, namespace: NS) -> ElementBuilder {
@@ -198,7 +198,7 @@ impl Element {
                 name.as_ref().to_string(),
                 namespace.into(),
                 None,
-                BTreeMap::new(),
+                AttrMap::new(),
                 Vec::new(),
             ),
         }
@@ -210,12 +210,13 @@ impl Element {
     ///
     /// ```rust
     /// use minidom::Element;
+    /// use rxml::{Namespace, xml_ncname};
     ///
     /// let bare = Element::bare("name", "namespace");
     ///
     /// assert_eq!(bare.name(), "name");
     /// assert_eq!(bare.ns(), "namespace");
-    /// assert_eq!(bare.attr("name"), None);
+    /// assert_eq!(bare.attr(xml_ncname!("name")), None);
     /// assert_eq!(bare.text(), "");
     /// ```
     pub fn bare<S: Into<String>, NS: Into<String>>(name: S, namespace: NS) -> Element {
@@ -223,7 +224,7 @@ impl Element {
             name.into(),
             namespace.into(),
             None,
-            BTreeMap::new(),
+            AttrMap::new(),
             Vec::new(),
         )
     }
@@ -242,8 +243,17 @@ impl Element {
 
     /// Returns a reference to the value of the given attribute, if it exists, else `None`.
     #[must_use]
-    pub fn attr(&self, name: &str) -> Option<&str> {
-        if let Some(value) = self.attributes.get(name) {
+    pub fn attr<'a>(&'a self, name: &'a NcNameStr) -> Option<&'a str> {
+        if let Some(value) = self.attributes.get(&RxmlNamespace::NONE, name) {
+            return Some(value);
+        }
+        None
+    }
+
+    /// Returns a reference to the value of the given namespaced attribute, if it exists, else `None`.
+    #[must_use]
+    pub fn attr_ns<'a>(&'a self, ns: &'a RxmlNamespace, name: &'a NcNameStr) -> Option<&'a str> {
+        if let Some(value) = self.attributes.get(ns, name) {
             return Some(value);
         }
         None
@@ -255,43 +265,39 @@ impl Element {
     ///
     /// ```rust
     /// use minidom::Element;
+    /// use rxml::{Namespace, xml_ncname};
     ///
     /// let elm: Element = "<elem xmlns=\"ns1\" a=\"b\" />".parse().unwrap();
     ///
-    /// let mut iter = elm.attrs();
+    /// let mut iter = elm.attrs().iter();
     ///
-    /// assert_eq!(iter.next().unwrap(), ("a", "b"));
+    /// assert_eq!(iter.next().unwrap(), ((&Namespace::NONE, &xml_ncname!("a").to_owned()), &String::from("b")));
     /// assert_eq!(iter.next(), None);
     /// ```
     #[must_use]
-    pub fn attrs(&self) -> Attrs<'_> {
-        Attrs {
-            iter: self.attributes.iter(),
-        }
+    pub fn attrs(&self) -> &AttrMap {
+        &self.attributes
     }
 
     /// Returns an iterator over the attributes of this element, with the value being a mutable
     /// reference.
     #[must_use]
-    pub fn attrs_mut(&mut self) -> AttrsMut<'_> {
-        AttrsMut {
-            iter: self.attributes.iter_mut(),
-        }
+    pub fn attrs_mut(&mut self) -> &mut AttrMap {
+        &mut self.attributes
     }
 
     /// Modifies the value of an attribute.
-    pub fn set_attr<S: Into<String>, V: IntoAttributeValue>(&mut self, name: S, val: V) {
-        let name = name.into();
+    pub fn set_attr<V: IntoAttributeValue>(&mut self, ns: RxmlNamespace, name: NcName, val: V) {
         let val = val.into_attribute_value();
 
-        if let Some(value) = self.attributes.get_mut(&name) {
+        if let Some(value) = self.attributes.get_mut(&ns, &name) {
             *value = val
                 .expect("removing existing value via set_attr, this is not yet supported (TODO)"); // TODO
             return;
         }
 
         if let Some(val) = val {
-            self.attributes.insert(name, val);
+            self.attributes.insert(ns.clone(), name, val);
         }
     }
 
@@ -405,19 +411,8 @@ impl Element {
         let namespace: RxmlNamespace = self.namespace.clone().into();
         writer.write(Item::ElementHeadStart(&namespace, (*self.name).try_into()?))?;
 
-        for (key, value) in &self.attributes {
-            let (prefix, name) = <&rxml::NameStr>::try_from(&**key)
-                .unwrap()
-                .split_name()
-                .unwrap();
-            let namespace = match prefix {
-                Some(prefix) => match writer.encoder.ns_tracker().lookup_prefix(Some(prefix)) {
-                    Ok(v) => v,
-                    Err(rxml::writer::PrefixError::Undeclared) => return Err(Error::InvalidPrefix),
-                },
-                None => RxmlNamespace::NONE,
-            };
-            writer.write(Item::Attribute(&namespace, name, value))?;
+        for ((ns, key), value) in &self.attributes {
+            writer.write(Item::Attribute(&ns, key, value))?;
         }
 
         if !self.children.is_empty() {
@@ -870,32 +865,6 @@ pub type Nodes<'a> = slice::Iter<'a, Node>;
 /// An iterator over mutable references to all child nodes of an `Element`.
 pub type NodesMut<'a> = slice::IterMut<'a, Node>;
 
-/// An iterator over the attributes of an `Element`.
-pub struct Attrs<'a> {
-    iter: btree_map::Iter<'a, String, String>,
-}
-
-impl<'a> Iterator for Attrs<'a> {
-    type Item = (&'a str, &'a str);
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.iter.next().map(|(x, y)| (x.as_ref(), y.as_ref()))
-    }
-}
-
-/// An iterator over the attributes of an `Element`, with the values mutable.
-pub struct AttrsMut<'a> {
-    iter: btree_map::IterMut<'a, String, String>,
-}
-
-impl<'a> Iterator for AttrsMut<'a> {
-    type Item = (&'a str, &'a mut String);
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.iter.next().map(|(x, y)| (x.as_ref(), y))
-    }
-}
-
 /// A builder for `Element`s.
 pub struct ElementBuilder {
     root: Element,
@@ -917,12 +886,20 @@ impl ElementBuilder {
 
     /// Sets an attribute.
     #[must_use]
-    pub fn attr<S: Into<String>, V: IntoAttributeValue>(
+    pub fn attr<V: IntoAttributeValue>(mut self, name: NcName, value: V) -> ElementBuilder {
+        self.root.set_attr(RxmlNamespace::NONE, name, value);
+        self
+    }
+
+    /// Sets an attribute.
+    #[must_use]
+    pub fn attr_ns<V: IntoAttributeValue>(
         mut self,
-        name: S,
+        ns: RxmlNamespace,
+        name: NcName,
         value: V,
     ) -> ElementBuilder {
-        self.root.set_attr(name, value);
+        self.root.set_attr(ns, name, value);
         self
     }
 
@@ -955,21 +932,32 @@ impl ElementBuilder {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use rxml::xml_ncname;
+    use std::collections::BTreeMap;
 
     #[test]
     fn test_element_new() {
+        let mut attrs = AttrMap::new();
+        attrs.insert(
+            String::from("namespace").into(),
+            xml_ncname!("name").to_owned(),
+            "value".to_string(),
+        );
         let elem = Element::new(
             "name".to_owned(),
             "namespace".to_owned(),
             (None, "namespace".to_owned()),
-            BTreeMap::from_iter([("name".to_string(), "value".to_string())].into_iter()),
+            attrs,
             Vec::new(),
         );
 
         assert_eq!(elem.name(), "name");
         assert_eq!(elem.ns(), "namespace".to_owned());
-        assert_eq!(elem.attr("name"), Some("value"));
-        assert_eq!(elem.attr("inexistent"), None);
+        assert_eq!(
+            elem.attr_ns(&String::from("namespace").into(), xml_ncname!("name")),
+            Some("value")
+        );
+        assert_eq!(elem.attr(xml_ncname!("inexistent")), None);
     }
 
     #[test]
@@ -987,7 +975,9 @@ mod tests {
         let xml = b"<foo xmlns='ns1'><bar xmlns='ns1' baz='qxx' /></foo>";
         let elem = Element::from_reader(&xml[..]);
 
-        let nested = Element::builder("bar", "ns1").attr("baz", "qxx").build();
+        let nested = Element::builder("bar", "ns1")
+            .attr(xml_ncname!("baz").to_owned(), "qxx")
+            .build();
         let elem2 = Element::builder("foo", "ns1").append(nested).build();
 
         assert_eq!(elem.unwrap(), elem2);
@@ -998,7 +988,9 @@ mod tests {
         let xml = b"<foo xmlns='ns1'><prefix:bar xmlns:prefix='ns1' baz='qxx' /></foo>";
         let elem = Element::from_reader(&xml[..]);
 
-        let nested = Element::builder("bar", "ns1").attr("baz", "qxx").build();
+        let nested = Element::builder("bar", "ns1")
+            .attr(xml_ncname!("baz").to_owned(), "qxx")
+            .build();
         let elem2 = Element::builder("foo", "ns1").append(nested).build();
 
         assert_eq!(elem.unwrap(), elem2);

minidom/src/tests.rs 🔗

@@ -16,19 +16,23 @@ use alloc::format;
 use alloc::string::String;
 use alloc::vec::Vec;
 
+use rxml::{xml_ncname, Namespace as RxmlNamespace};
+
 const TEST_STRING: &'static [u8] = br#"<root xmlns='root_ns' a='b' xml:lang='en'>meow<child c='d'/><child xmlns='child_ns' d='e' xml:lang='fr'/>nya</root>"#;
 
 fn build_test_tree() -> Element {
     let mut root = Element::builder("root", "root_ns")
-        .attr("xml:lang", "en")
-        .attr("a", "b")
+        .attr_ns(RxmlNamespace::XML, xml_ncname!("lang").to_owned(), "en")
+        .attr(xml_ncname!("a").to_owned(), "b")
         .build();
     root.append_text_node("meow");
-    let child = Element::builder("child", "root_ns").attr("c", "d").build();
+    let child = Element::builder("child", "root_ns")
+        .attr(xml_ncname!("c").to_owned(), "d")
+        .build();
     root.append_child(child);
     let other_child = Element::builder("child", "child_ns")
-        .attr("d", "e")
-        .attr("xml:lang", "fr")
+        .attr(xml_ncname!("d").to_owned(), "e")
+        .attr_ns(RxmlNamespace::XML, xml_ncname!("lang").to_owned(), "fr")
         .build();
     root.append_child(other_child);
     root.append_text_node("nya");
@@ -243,7 +247,7 @@ fn writer_with_prefix_deduplicate() {
 #[test]
 fn writer_escapes_attributes() {
     let root = Element::builder("root", "ns1")
-        .attr("a", "\"Air\" quotes")
+        .attr(xml_ncname!("a").to_owned(), "\"Air\" quotes")
         .build();
     let mut writer = Vec::new();
     {
@@ -271,14 +275,14 @@ fn writer_escapes_text() {
 #[test]
 fn builder_works() {
     let elem = Element::builder("a", "b")
-        .attr("c", "d")
+        .attr(xml_ncname!("c").to_owned(), "d")
         .append(Element::builder("child", "b"))
         .append("e")
         .build();
     assert_eq!(elem.name(), "a");
     assert_eq!(elem.ns(), "b".to_owned());
-    assert_eq!(elem.attr("c"), Some("d"));
-    assert_eq!(elem.attr("x"), None);
+    assert_eq!(elem.attr(xml_ncname!("c")), Some("d"));
+    assert_eq!(elem.attr(xml_ncname!("x")), None);
     assert_eq!(elem.text(), "e");
     assert!(elem.has_child("child", "b"));
     assert!(elem.is("a", "b"));
@@ -307,11 +311,15 @@ fn get_child_works() {
         .unwrap()
         .is("child", "child_ns"));
     assert_eq!(
-        root.get_child("child", "root_ns").unwrap().attr("c"),
+        root.get_child("child", "root_ns")
+            .unwrap()
+            .attr(xml_ncname!("c")),
         Some("d")
     );
     assert_eq!(
-        root.get_child("child", "child_ns").unwrap().attr("d"),
+        root.get_child("child", "child_ns")
+            .unwrap()
+            .attr(xml_ncname!("d")),
         Some("e")
     );
 }
@@ -349,12 +357,15 @@ fn two_elements_with_same_arguments_different_order_are_equal() {
 #[test]
 fn namespace_attributes_works() {
     let root = Element::from_reader(TEST_STRING).unwrap();
-    assert_eq!("en", root.attr("xml:lang").unwrap());
+    assert_eq!(
+        Some("en"),
+        root.attr_ns(RxmlNamespace::xml(), xml_ncname!("lang"))
+    );
     assert_eq!(
         "fr",
         root.get_child("child", "child_ns")
             .unwrap()
-            .attr("xml:lang")
+            .attr_ns(RxmlNamespace::xml(), xml_ncname!("lang"))
             .unwrap()
     );
 }

minidom/src/tree_builder.rs 🔗

@@ -1,4 +1,5 @@
 // Copyright (c) 2022 Astro <astro@spaceboyz.net>
+// Copyright (c) 2025 pep <pep@bouah.net>
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -8,19 +9,18 @@
 
 use crate::prefixes::{Prefix, Prefixes};
 use crate::{Element, Error};
-use alloc::collections::BTreeMap;
-use alloc::format;
 use alloc::string::String;
 use alloc::vec::Vec;
-use rxml::RawEvent;
+use rxml::{AttrMap, Namespace, RawEvent};
 
 /// Tree-building parser state
 pub struct TreeBuilder {
-    next_tag: Option<(Prefix, String, Prefixes, BTreeMap<String, String>)>,
+    next_tag: Option<(Prefix, String, Prefixes, AttrMap)>,
     /// Parsing stack
     stack: Vec<Element>,
     /// Namespace set stack by prefix
     prefixes_stack: Vec<Prefixes>,
+    attrs_stack: Vec<(String, String, String)>,
     /// Document root element if finished
     pub root: Option<Element>,
 }
@@ -39,6 +39,7 @@ impl TreeBuilder {
             next_tag: None,
             stack: Vec::new(),
             prefixes_stack: Vec::new(),
+            attrs_stack: Vec::new(),
             root: None,
         }
     }
@@ -83,8 +84,11 @@ impl TreeBuilder {
 
     /// Lookup XML namespace declaration for given prefix (or no prefix)
     #[must_use]
-    fn lookup_prefix(&self, prefix: &Option<String>) -> Option<&str> {
-        for nss in self.prefixes_stack.iter().rev() {
+    fn lookup_prefix<'a>(
+        prefixes_stack: &'a Vec<Prefixes>,
+        prefix: &'a Option<String>,
+    ) -> Option<&'a str> {
+        for nss in prefixes_stack.iter().rev() {
             if let Some(ns) = nss.get(prefix) {
                 return Some(ns);
             }
@@ -129,7 +133,7 @@ impl TreeBuilder {
                     prefix.map(|prefix| prefix.as_str().to_owned()),
                     name.as_str().to_owned(),
                     prefixes,
-                    BTreeMap::new(),
+                    AttrMap::new(),
                 ));
             }
 
@@ -141,23 +145,47 @@ impl TreeBuilder {
                             prefixes.insert(Some(prefix.as_str().to_owned()), value);
                         }
                         (Some(prefix), name) => {
-                            attrs.insert(format!("{prefix}:{name}"), value.as_str().to_owned());
+                            self.attrs_stack
+                                .push((prefix.to_string(), name.to_string(), value));
                         }
                         (None, name) => {
-                            attrs.insert(name.as_str().to_owned(), value.as_str().to_owned());
+                            attrs.insert(
+                                Namespace::NONE,
+                                name.try_into().unwrap(),
+                                value.as_str().to_owned(),
+                            );
                         }
                     }
                 }
             }
 
             RawEvent::ElementHeadClose(_) => {
-                if let Some((prefix, name, prefixes, attrs)) = self.next_tag.take() {
+                if let Some((prefix, name, prefixes, mut attrs)) = self.next_tag.take() {
                     self.prefixes_stack.push(prefixes.clone());
 
-                    let namespace = self
-                        .lookup_prefix(&prefix.map(|prefix| prefix.as_str().to_owned()))
-                        .ok_or(Error::MissingNamespace)?
-                        .to_owned();
+                    let namespace = TreeBuilder::lookup_prefix(
+                        &self.prefixes_stack,
+                        &prefix.map(|prefix| prefix.as_str().to_owned()),
+                    )
+                    .ok_or(Error::MissingNamespace)?
+                    .to_owned();
+
+                    for (prefix, attr, value) in self.attrs_stack.drain(..) {
+                        let ns = if prefix == "xml" {
+                            rxml::Namespace::xml()
+                        } else {
+                            &TreeBuilder::lookup_prefix(
+                                &self.prefixes_stack,
+                                &Some(prefix.to_string()),
+                            )
+                            .map(String::from)
+                            .map(|s| TryInto::<Namespace>::try_into(s).unwrap())
+                            .ok_or(Error::MissingNamespace)?
+                            .to_owned()
+                        };
+                        attrs.insert(ns.clone(), attr.try_into().unwrap(), value.to_string());
+                    }
+
                     let el = Element::new(name.to_owned(), namespace, prefixes, attrs, Vec::new());
                     self.stack.push(el);
                 }

parsers/src/extdisco.rs 🔗

@@ -193,7 +193,7 @@ mod tests {
         let query = ServicesQuery { type_: None };
         let elem = Element::from(query);
         assert!(elem.is("services", ns::EXT_DISCO));
-        assert_eq!(elem.attrs().next(), None);
+        assert_eq!(elem.attrs().into_iter().next(), None);
         assert_eq!(elem.nodes().next(), None);
     }
 

parsers/src/jingle_s5b.rs 🔗

@@ -4,6 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+use xso::exports::rxml;
 use xso::{
     error::{Error, FromElementError},
     AsXml, FromXml,
@@ -254,9 +255,9 @@ impl TryFrom<Element> for Transport {
 impl From<Transport> for Element {
     fn from(transport: Transport) -> Element {
         Element::builder("transport", ns::JINGLE_S5B)
-            .attr("sid", transport.sid)
-            .attr("dstaddr", transport.dstaddr)
-            .attr("mode", transport.mode)
+            .attr(rxml::xml_ncname!("sid").into(), transport.sid)
+            .attr(rxml::xml_ncname!("dstaddr").into(), transport.dstaddr)
+            .attr(rxml::xml_ncname!("mode").into(), transport.mode)
             .append_all(match transport.payload {
                 TransportPayload::Candidates(candidates) => candidates
                     .into_iter()
@@ -264,7 +265,7 @@ impl From<Transport> for Element {
                     .collect::<Vec<_>>(),
                 TransportPayload::Activated(cid) => {
                     vec![Element::builder("activated", ns::JINGLE_S5B)
-                        .attr("cid", cid)
+                        .attr(rxml::xml_ncname!("cid").into(), cid)
                         .build()]
                 }
                 TransportPayload::CandidateError => {
@@ -272,7 +273,7 @@ impl From<Transport> for Element {
                 }
                 TransportPayload::CandidateUsed(cid) => {
                     vec![Element::builder("candidate-used", ns::JINGLE_S5B)
-                        .attr("cid", cid)
+                        .attr(rxml::xml_ncname!("cid").into(), cid)
                         .build()]
                 }
                 TransportPayload::ProxyError => {

parsers/src/presence.rs 🔗

@@ -293,6 +293,7 @@ mod tests {
     use super::*;
     use jid::{BareJid, FullJid};
     use xso::error::FromElementError;
+    use xso::exports::rxml;
 
     #[cfg(target_pointer_width = "32")]
     #[test]
@@ -602,25 +603,31 @@ mod tests {
     fn presence_with_to() {
         let presence = Presence::new(Type::None);
         let elem: Element = presence.into();
-        assert_eq!(elem.attr("to"), None);
+        assert_eq!(elem.attr(rxml::xml_ncname!("to")), None);
 
         let presence = Presence::new(Type::None).with_to(Jid::new("localhost").unwrap());
         let elem: Element = presence.into();
-        assert_eq!(elem.attr("to"), Some("localhost"));
+        assert_eq!(elem.attr(rxml::xml_ncname!("to")), Some("localhost"));
 
         let presence = Presence::new(Type::None).with_to(BareJid::new("localhost").unwrap());
         let elem: Element = presence.into();
-        assert_eq!(elem.attr("to"), Some("localhost"));
+        assert_eq!(elem.attr(rxml::xml_ncname!("to")), Some("localhost"));
 
         let presence =
             Presence::new(Type::None).with_to(Jid::new("test@localhost/coucou").unwrap());
         let elem: Element = presence.into();
-        assert_eq!(elem.attr("to"), Some("test@localhost/coucou"));
+        assert_eq!(
+            elem.attr(rxml::xml_ncname!("to")),
+            Some("test@localhost/coucou")
+        );
 
         let presence =
             Presence::new(Type::None).with_to(FullJid::new("test@localhost/coucou").unwrap());
         let elem: Element = presence.into();
-        assert_eq!(elem.attr("to"), Some("test@localhost/coucou"));
+        assert_eq!(
+            elem.attr(rxml::xml_ncname!("to")),
+            Some("test@localhost/coucou")
+        );
     }
 
     #[test]

parsers/src/receipts.rs 🔗

@@ -35,6 +35,7 @@ mod tests {
     use crate::ns;
     use minidom::Element;
     use xso::error::{Error, FromElementError};
+    use xso::exports::rxml;
 
     #[cfg(target_pointer_width = "32")]
     #[test]
@@ -80,13 +81,13 @@ mod tests {
         let receipt = Request;
         let elem: Element = receipt.into();
         assert!(elem.is("request", ns::RECEIPTS));
-        assert_eq!(elem.attrs().count(), 0);
+        assert_eq!(elem.attrs().into_iter().count(), 0);
 
         let receipt = Received {
             id: String::from("coucou"),
         };
         let elem: Element = receipt.into();
         assert!(elem.is("received", ns::RECEIPTS));
-        assert_eq!(elem.attr("id"), Some("coucou"));
+        assert_eq!(elem.attr(rxml::xml_ncname!("id")), Some("coucou"));
     }
 }

parsers/src/util/macro_tests.rs 🔗

@@ -46,6 +46,7 @@ mod helpers {
 
 use self::helpers::{parse_str, roundtrip_full};
 
+use xso::exports::rxml;
 use xso::{AsXml, FromXml, PrintRawXml};
 
 // these are adverserial local names in order to trigger any issues with
@@ -1790,9 +1791,9 @@ fn element_catch_one_and_many_parse_in_order() {
         "<parent xmlns='urn:example:ns1'><child num='0'/><child num='1'/></parent>",
     ) {
         Ok(ElementCatchOneAndMany { child, children }) => {
-            assert_eq!(child.attr("num"), Some("0"));
+            assert_eq!(child.attr(rxml::xml_ncname!("num")), Some("0"));
             assert_eq!(children.len(), 1);
-            assert_eq!(children[0].attr("num"), Some("1"));
+            assert_eq!(children[0].attr(rxml::xml_ncname!("num")), Some("1"));
         }
         other => panic!("unexpected result: {:?}", other),
     }

parsers/src/util/macros.rs 🔗

@@ -15,13 +15,13 @@ macro_rules! get_attr {
         )
     };
     ($elem:ident, $attr:tt, Option, $value:ident, $func:expr) => {
-        match $elem.attr($attr) {
+        match $elem.attr(::xso::exports::rxml::xml_ncname!($attr).into()) {
             Some($value) => Some($func),
             None => None,
         }
     };
     ($elem:ident, $attr:tt, Required, $value:ident, $func:expr) => {
-        match $elem.attr($attr) {
+        match $elem.attr(::xso::exports::rxml::xml_ncname!($attr).into()) {
             Some($value) => $func,
             None => {
                 return Err(xso::error::Error::Other(
@@ -32,7 +32,7 @@ macro_rules! get_attr {
         }
     };
     ($elem:ident, $attr:tt, Default, $value:ident, $func:expr) => {
-        match $elem.attr($attr) {
+        match $elem.attr(::xso::exports::rxml::xml_ncname!($attr).into()) {
             Some($value) => $func,
             None => ::core::default::Default::default(),
         }
@@ -277,7 +277,7 @@ macro_rules! generate_attribute_enum {
         impl From<$elem> for minidom::Element {
             fn from(elem: $elem) -> minidom::Element {
                 minidom::Element::builder($name, crate::ns::$ns)
-                    .attr($attr, match elem {
+                    .attr(::xso::exports::rxml::xml_ncname!($attr).into(), match elem {
                          $($elem::$enum => $enum_name,)+
                      })
                      .build()
@@ -343,9 +343,9 @@ macro_rules! check_no_attributes {
 macro_rules! check_no_unknown_attributes {
     ($elem:ident, $name:tt, [$($attr:tt),*]) => (
         #[cfg(not(feature = "disable-validation"))]
-        for (_attr, _) in $elem.attrs() {
+        for ((ns, attr), _) in $elem.attrs() {
             $(
-                if _attr == $attr {
+                if *ns == ::xso::exports::rxml::Namespace::NONE && attr == $attr {
                     continue;
                 }
             )*

parsers/src/xhtml.rs 🔗

@@ -8,7 +8,11 @@ use crate::message::MessagePayload;
 use crate::ns;
 use alloc::collections::BTreeMap;
 use minidom::{Element, Node};
-use xso::error::{Error, FromElementError};
+use xso::exports::rxml;
+use xso::{
+    error::{Error, FromElementError},
+    exports::rxml::Namespace,
+};
 
 // TODO: Use a proper lang type.
 type Lang = String;
@@ -72,7 +76,10 @@ impl TryFrom<Element> for XhtmlIm {
         for child in elem.children() {
             if child.is("body", ns::XHTML) {
                 let child = child.clone();
-                let lang = child.attr("xml:lang").unwrap_or("").to_string();
+                let lang = child
+                    .attr_ns(rxml::Namespace::xml(), rxml::xml_ncname!("lang").into())
+                    .unwrap_or("")
+                    .to_string();
                 let body = Body::try_from(child)?;
                 match bodies.insert(lang, body) {
                     None => (),
@@ -161,8 +168,13 @@ impl TryFrom<Element> for Body {
         }
 
         Ok(Body {
-            style: parse_css(elem.attr("style")),
-            xml_lang: elem.attr("xml:lang").map(|xml_lang| xml_lang.to_string()),
+            style: parse_css(elem.attr(rxml::xml_ncname!("style"))),
+            xml_lang: elem
+                .attr_ns(
+                    &Into::<Namespace>::into(String::from("xml")),
+                    rxml::xml_ncname!("lang").into(),
+                )
+                .map(|xml_lang| xml_lang.to_string()),
             children,
         })
     }
@@ -171,8 +183,15 @@ impl TryFrom<Element> for Body {
 impl From<Body> for Element {
     fn from(body: Body) -> Element {
         Element::builder("body", ns::XHTML)
-            .attr("style", get_style_string(body.style))
-            .attr("xml:lang", body.xml_lang)
+            .attr(
+                rxml::xml_ncname!("style").into(),
+                get_style_string(body.style),
+            )
+            .attr_ns(
+                Into::<Namespace>::into(String::from("xml")),
+                rxml::xml_ncname!("lang").into(),
+                body.xml_lang,
+            )
             .append_all(children_to_nodes(body.children))
             .build()
     }
@@ -309,44 +328,52 @@ impl TryFrom<Element> for Tag {
 
         Ok(match elem.name() {
             "a" => Tag::A {
-                href: elem.attr("href").map(|href| href.to_string()),
-                style: parse_css(elem.attr("style")),
-                type_: elem.attr("type").map(|type_| type_.to_string()),
+                href: elem
+                    .attr(rxml::xml_ncname!("href"))
+                    .map(|href| href.to_string()),
+                style: parse_css(elem.attr(rxml::xml_ncname!("style"))),
+                type_: elem
+                    .attr(rxml::xml_ncname!("type"))
+                    .map(|type_| type_.to_string()),
                 children,
             },
             "blockquote" => Tag::Blockquote {
-                style: parse_css(elem.attr("style")),
+                style: parse_css(elem.attr(rxml::xml_ncname!("style"))),
                 children,
             },
             "br" => Tag::Br,
             "cite" => Tag::Cite {
-                style: parse_css(elem.attr("style")),
+                style: parse_css(elem.attr(rxml::xml_ncname!("style"))),
                 children,
             },
             "em" => Tag::Em { children },
             "img" => Tag::Img {
-                src: elem.attr("src").map(|src| src.to_string()),
-                alt: elem.attr("alt").map(|alt| alt.to_string()),
+                src: elem
+                    .attr(rxml::xml_ncname!("src"))
+                    .map(|src| src.to_string()),
+                alt: elem
+                    .attr(rxml::xml_ncname!("alt"))
+                    .map(|alt| alt.to_string()),
             },
             "li" => Tag::Li {
-                style: parse_css(elem.attr("style")),
+                style: parse_css(elem.attr(rxml::xml_ncname!("style"))),
                 children,
             },
             "ol" => Tag::Ol {
-                style: parse_css(elem.attr("style")),
+                style: parse_css(elem.attr(rxml::xml_ncname!("style"))),
                 children,
             },
             "p" => Tag::P {
-                style: parse_css(elem.attr("style")),
+                style: parse_css(elem.attr(rxml::xml_ncname!("style"))),
                 children,
             },
             "span" => Tag::Span {
-                style: parse_css(elem.attr("style")),
+                style: parse_css(elem.attr(rxml::xml_ncname!("style"))),
                 children,
             },
             "strong" => Tag::Strong { children },
             "ul" => Tag::Ul {
-                style: parse_css(elem.attr("style")),
+                style: parse_css(elem.attr(rxml::xml_ncname!("style"))),
                 children,
             },
             _ => Tag::Unknown(children),
@@ -367,13 +394,13 @@ impl From<Tag> for Element {
                 {
                     let mut attrs = vec![];
                     if let Some(href) = href {
-                        attrs.push(("href", href));
+                        attrs.push((rxml::xml_ncname!("href"), href));
                     }
                     if let Some(style) = get_style_string(style) {
-                        attrs.push(("style", style));
+                        attrs.push((rxml::xml_ncname!("style"), style));
                     }
                     if let Some(type_) = type_ {
-                        attrs.push(("type", type_));
+                        attrs.push((rxml::xml_ncname!("type"), type_));
                     }
                     attrs
                 },
@@ -382,7 +409,7 @@ impl From<Tag> for Element {
             Tag::Blockquote { style, children } => (
                 "blockquote",
                 match get_style_string(style) {
-                    Some(style) => vec![("style", style)],
+                    Some(style) => vec![(rxml::xml_ncname!("style"), style)],
                     None => vec![],
                 },
                 children,
@@ -391,7 +418,7 @@ impl From<Tag> for Element {
             Tag::Cite { style, children } => (
                 "cite",
                 match get_style_string(style) {
-                    Some(style) => vec![("style", style)],
+                    Some(style) => vec![(rxml::xml_ncname!("style"), style)],
                     None => vec![],
                 },
                 children,
@@ -400,17 +427,17 @@ impl From<Tag> for Element {
             Tag::Img { src, alt } => {
                 let mut attrs = vec![];
                 if let Some(src) = src {
-                    attrs.push(("src", src));
+                    attrs.push((rxml::xml_ncname!("src"), src));
                 }
                 if let Some(alt) = alt {
-                    attrs.push(("alt", alt));
+                    attrs.push((rxml::xml_ncname!("alt"), alt));
                 }
                 ("img", attrs, vec![])
             }
             Tag::Li { style, children } => (
                 "li",
                 match get_style_string(style) {
-                    Some(style) => vec![("style", style)],
+                    Some(style) => vec![(rxml::xml_ncname!("style"), style)],
                     None => vec![],
                 },
                 children,
@@ -418,7 +445,7 @@ impl From<Tag> for Element {
             Tag::Ol { style, children } => (
                 "ol",
                 match get_style_string(style) {
-                    Some(style) => vec![("style", style)],
+                    Some(style) => vec![(rxml::xml_ncname!("style"), style)],
                     None => vec![],
                 },
                 children,
@@ -426,7 +453,7 @@ impl From<Tag> for Element {
             Tag::P { style, children } => (
                 "p",
                 match get_style_string(style) {
-                    Some(style) => vec![("style", style)],
+                    Some(style) => vec![(rxml::xml_ncname!("style"), style)],
                     None => vec![],
                 },
                 children,
@@ -434,7 +461,7 @@ impl From<Tag> for Element {
             Tag::Span { style, children } => (
                 "span",
                 match get_style_string(style) {
-                    Some(style) => vec![("style", style)],
+                    Some(style) => vec![(rxml::xml_ncname!("style"), style)],
                     None => vec![],
                 },
                 children,
@@ -443,7 +470,7 @@ impl From<Tag> for Element {
             Tag::Ul { style, children } => (
                 "ul",
                 match get_style_string(style) {
-                    Some(style) => vec![("style", style)],
+                    Some(style) => vec![(rxml::xml_ncname!("style"), style)],
                     None => vec![],
                 },
                 children,
@@ -454,7 +481,7 @@ impl From<Tag> for Element {
         };
         let mut builder = Element::builder(name, ns::XHTML).append_all(children_to_nodes(children));
         for (key, value) in attrs {
-            builder = builder.attr(key, value);
+            builder = builder.attr(key.into(), value);
         }
         builder.build()
     }

xso/src/minidom_compat.rs 🔗

@@ -15,11 +15,7 @@ use core::marker::PhantomData;
 
 use minidom::{Element, Node};
 
-use rxml::{
-    parser::EventMetrics,
-    writer::{SimpleNamespaces, TrackNamespace},
-    AttrMap, Event, Name, NameStr, Namespace, NcName, NcNameStr,
-};
+use rxml::{parser::EventMetrics, AttrMap, Event, Namespace, NcName, NcNameStr};
 
 use crate::{
     error::{Error, FromEventsError},
@@ -74,26 +70,8 @@ pub fn make_start_ev_parts(el: &Element) -> Result<(rxml::QName, AttrMap), Error
     let namespace = Namespace::from(el.ns());
 
     let mut attrs = AttrMap::new();
-    for (name, value) in el.attrs() {
-        let name = Name::try_from(name)?;
-        let (prefix, name) = name.split_name()?;
-        let namespace = if let Some(prefix) = prefix {
-            if prefix == "xml" {
-                Namespace::XML
-            } else {
-                let ns = match el.prefixes.get(&Some(prefix.into())) {
-                    Some(v) => v,
-                    None => {
-                        panic!("undeclared xml namespace prefix in minidom::Element")
-                    }
-                };
-                Namespace::from(ns.to_owned())
-            }
-        } else {
-            Namespace::NONE
-        };
-
-        attrs.insert(namespace, name, value.to_owned());
+    for ((namespace, name), value) in el.attrs() {
+        attrs.insert(namespace.clone(), name.clone(), value.to_owned());
     }
 
     Ok(((namespace, name), attrs))
@@ -178,7 +156,7 @@ enum AsXmlState<'a> {
         element: &'a Element,
 
         /// Attribute iterator.
-        attributes: minidom::element::Attrs<'a>,
+        attributes: rxml::xml_map::Iter<'a, std::string::String>,
     },
 
     /// Content: The contents of the element are streamed as events.
@@ -218,7 +196,7 @@ impl<'a> Iterator for ElementAsXml<'a> {
                 );
                 self.0 = Some(AsXmlState::Attributes {
                     element,
-                    attributes: element.attrs(),
+                    attributes: element.attrs().iter(),
                 });
                 Some(Ok(item))
             }
@@ -226,38 +204,9 @@ impl<'a> Iterator for ElementAsXml<'a> {
                 ref mut attributes,
                 element,
             }) => {
-                if let Some((name, value)) = attributes.next() {
-                    let name = match <&NameStr>::try_from(name) {
-                        Ok(v) => v,
-                        Err(e) => {
-                            self.0 = None;
-                            return Some(Err(e.into()));
-                        }
-                    };
-                    let (prefix, name) = match name.split_name() {
-                        Ok(v) => v,
-                        Err(e) => {
-                            self.0 = None;
-                            return Some(Err(e.into()));
-                        }
-                    };
-                    let namespace = if let Some(prefix) = prefix {
-                        if prefix == "xml" {
-                            Namespace::XML
-                        } else {
-                            let ns = match element.prefixes.get(&Some(prefix.as_str().to_owned())) {
-                                Some(v) => v,
-                                None => {
-                                    panic!("undeclared xml namespace prefix in minidom::Element")
-                                }
-                            };
-                            Namespace::from(ns.to_owned())
-                        }
-                    } else {
-                        Namespace::NONE
-                    };
+                if let Some(((namespace, name), value)) = attributes.next() {
                     Some(Ok(Item::Attribute(
-                        namespace,
+                        namespace.clone(),
                         Cow::Borrowed(name),
                         Cow::Borrowed(value),
                     )))
@@ -330,24 +279,9 @@ impl ElementFromEvents {
     /// [`minidom::Element`], this is contractually infallible. Using this may
     /// thus save you an `unwrap()` call.
     pub fn new(qname: rxml::QName, attrs: rxml::AttrMap) -> Self {
-        let mut prefixes = SimpleNamespaces::new();
         let mut builder = Element::builder(qname.1, qname.0);
         for ((namespace, name), value) in attrs.into_iter() {
-            if namespace.is_none() {
-                builder = builder.attr(name, value);
-            } else {
-                let (is_new, prefix) = prefixes.declare_with_auto_prefix(namespace.clone());
-                let name = prefix.with_suffix(&name);
-                if is_new {
-                    builder = builder
-                        .prefix(
-                            Some(prefix.as_str().to_owned()),
-                            namespace.as_str().to_owned(),
-                        )
-                        .unwrap();
-                }
-                builder = builder.attr(name, value);
-            }
+            builder = builder.attr_ns(namespace, name, value);
         }
 
         let element = builder.build();