diff --git a/minidom-rs/.gitignore b/minidom-rs/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a9d37c560c6ab8d4afbf47eda643e8c42e857716 --- /dev/null +++ b/minidom-rs/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/minidom-rs/.gitlab-ci.yml b/minidom-rs/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..efd57f1e923b2926fbf8cf39bbd5ef0727d7a1e9 --- /dev/null +++ b/minidom-rs/.gitlab-ci.yml @@ -0,0 +1,62 @@ +stages: + - build + - test + +variables: + FEATURES: "" + RUST_BACKTRACE: "full" + +.stable: + image: rust:latest + cache: + key: stable + paths: + - target/ + +.nightly: + image: rustlang/rust:nightly + cache: + key: nightly + paths: + - target/ + +.build: + stage: build + script: + - cargo build --verbose --no-default-features --features=$FEATURES + +.test: + stage: test + script: + - cargo test --verbose --no-default-features --features=$FEATURES + +rust-latest-build: + extends: + - .build + - .stable + +rust-nightly-build: + extends: + - .build + - .nightly + + +rust-latest-test: + extends: + - .test + - .stable + +rust-nightly-test: + extends: + - .test + - .nightly + +rust-latest-build with features=comments: + extends: rust-latest-build + variables: + FEATURES: "comments" + +rust-latest-test with features=comments: + extends: rust-latest-test + variables: + FEATURES: "comments" diff --git a/minidom-rs/CHANGELOG.md b/minidom-rs/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..275c0e69884a0b3638462e6353d571fe7390aad0 --- /dev/null +++ b/minidom-rs/CHANGELOG.md @@ -0,0 +1,51 @@ +Version XXX, released YYY: + * Changes + * Update edition to 2018 + * Fixes + * Update old CI configuration with newer Rust images +Version 0.11.1, released 2019-09-06: + * Changes + * Update to quick-xml 0.16 + * Add a default "comments" feature to transform comments into errors when unset. +Version 0.11.0, released 2019-06-14: + * Breaking + * Get rid of IntoElements, replace with `Into` and ` IntoIterator>` + * Fixes + * Remote unused `mut` attribute on variable + * Changes + * Update quick-xml to 0.14 + * Split Node into its own module + * Nicer Debug implementation for NamespaceSet +Version 0.10.0, released 2018-10-21: + * Changes + * Update quick-xml to 0.13 + * Update doc to reflect switch from xml-rs to quick-xml. +Version 0.9.1, released 2018-05-29: + * Fixes + * Lumi fixed CDATA handling, minidom will not unescape CDATA bodies anymore. + * Small changes + - Link Mauve implemented IntoAttributeValue on std::net::IpAddr. +Version 0.9.0, released 2018-04-10: + * Small changes + - Upgrade quick_xml to 0.12.1 +Version 0.8.0, released 2018-02-18: + * Additions + - Link Mauve replaced error\_chain with failure ( https://gitlab.com/lumi/minidom-rs/merge_requests/27 ) + - Yue Liu added support for writing comments and made the writing methods use quick-xml's EventWriter ( https://gitlab.com/lumi/minidom-rs/merge_requests/26 ) +Version 0.6.2, released 2017-08-27: + * Additions + - Link Mauve added an implementation of IntoElements for all Into ( https://gitlab.com/lumi/minidom-rs/merge_requests/19 ) +Version 0.6.1, released 2017-08-20: + * Additions + - Astro added Element::has_ns, which checks whether an element's namespace matches the passed argument. ( https://gitlab.com/lumi/minidom-rs/merge_requests/16 ) + - Link Mauve updated the quick-xml dependency to the latest version. + * Fixes + - Because break value is now stable, Link Mauve rewrote some code marked FIXME to use it. +Version 0.6.0, released 2017-08-13: + * Big changes + - Astro added proper support for namespace prefixes. ( https://gitlab.com/lumi/minidom-rs/merge_requests/14 ) + * Fixes + - Astro fixed a regression that caused the writer not to escape its xml output properly. ( https://gitlab.com/lumi/minidom-rs/merge_requests/15 ) +Version 0.5.0, released 2017-06-10: + * Big changes + - Eijebong made parsing a lot faster by switching the crate from xml-rs to quick_xml. ( https://gitlab.com/lumi/minidom-rs/merge_requests/11 ) diff --git a/minidom-rs/Cargo.toml b/minidom-rs/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e46db5a346cca27dacd00c280ea00690bdc7016a --- /dev/null +++ b/minidom-rs/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "minidom" +version = "0.11.1" +authors = [ + "lumi ", + "Emmanuel Gil Peyrot ", + "Bastien Orivel ", + "Astro ", + "Maxime “pep” Buquet ", +] +description = "A small, simple DOM implementation on top of quick-xml" +homepage = "https://gitlab.com/lumi/minidom-rs" +repository = "https://gitlab.com/lumi/minidom-rs" +documentation = "https://docs.rs/minidom" +readme = "README.md" +keywords = ["xml"] +license = "MIT" +edition = "2018" + +[badges] +gitlab = { repository = "lumi/minidom-rs" } + +[dependencies] +quick-xml = "0.17" + +[features] +default = ["comments"] +comments = [] diff --git a/minidom-rs/LICENSE b/minidom-rs/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e3f453dd6acacbda82e62c5535f8055520dcad5e --- /dev/null +++ b/minidom-rs/LICENSE @@ -0,0 +1,15 @@ + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/minidom-rs/README.md b/minidom-rs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..93582491b6513fc889957fd7c8b2a7d331329e36 --- /dev/null +++ b/minidom-rs/README.md @@ -0,0 +1,32 @@ +minidom-rs +========== + +What's this? +------------ + +A minimal DOM library on top of quick-xml. + +What license is it under? +------------------------- + +MIT. See `LICENSE`. + +License yadda yadda. +-------------------- + +Copyright 2017 minidom-rs contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/minidom-rs/examples/articles.rs b/minidom-rs/examples/articles.rs new file mode 100644 index 0000000000000000000000000000000000000000..29b1e513464b36a0b094d0babd282c151ab24316 --- /dev/null +++ b/minidom-rs/examples/articles.rs @@ -0,0 +1,45 @@ +extern crate minidom; + +use minidom::Element; + +const DATA: &str = r#" +
+ 10 Terrible Bugs You Would NEVER Believe Happened + + Rust fixed them all. <3 + +
+
+ BREAKING NEWS: Physical Bug Jumps Out Of Programmer's Screen + + Just kidding! + +
+
"#; + +const ARTICLE_NS: &str = "article"; + +#[derive(Debug)] +pub struct Article { + title: String, + body: String, +} + +fn main() { + let root: Element = DATA.parse().unwrap(); + + let mut articles: Vec
= Vec::new(); + + for child in root.children() { + if child.is("article", ARTICLE_NS) { + let title = child.get_child("title", ARTICLE_NS).unwrap().text(); + let body = child.get_child("body", ARTICLE_NS).unwrap().text(); + articles.push(Article { + title: title, + body: body.trim().to_owned(), + }); + } + } + + println!("{:?}", articles); +} diff --git a/minidom-rs/src/convert.rs b/minidom-rs/src/convert.rs new file mode 100644 index 0000000000000000000000000000000000000000..7c6d314e56df62c13a3c6c9e62c6491859a14c54 --- /dev/null +++ b/minidom-rs/src/convert.rs @@ -0,0 +1,69 @@ +//! A module which exports a few traits for converting types to elements and attributes. + +/// A trait for types which can be converted to an attribute value. +pub trait IntoAttributeValue { + /// Turns this into an attribute string, or None if it shouldn't be added. + fn into_attribute_value(self) -> Option; +} + +macro_rules! impl_into_attribute_value { + ($t:ty) => { + impl IntoAttributeValue for $t { + fn into_attribute_value(self) -> Option { + Some(format!("{}", self)) + } + } + } +} + +macro_rules! impl_into_attribute_values { + ($($t:ty),*) => { + $(impl_into_attribute_value!($t);)* + } +} + +impl_into_attribute_values!(usize, u64, u32, u16, u8, isize, i64, i32, i16, i8, ::std::net::IpAddr); + +impl IntoAttributeValue for String { + fn into_attribute_value(self) -> Option { + Some(self) + } +} + +impl<'a> IntoAttributeValue for &'a String { + fn into_attribute_value(self) -> Option { + Some(self.to_owned()) + } +} + +impl<'a> IntoAttributeValue for &'a str { + fn into_attribute_value(self) -> Option { + Some(self.to_owned()) + } +} + +impl IntoAttributeValue for Option { + fn into_attribute_value(self) -> Option { + self.and_then(IntoAttributeValue::into_attribute_value) + } +} + +#[cfg(test)] +mod tests { + use super::IntoAttributeValue; + use std::net::IpAddr; + use std::str::FromStr; + + #[test] + fn test_into_attribute_value_on_ints() { + assert_eq!(16u8.into_attribute_value().unwrap() , "16"); + assert_eq!(17u16.into_attribute_value().unwrap() , "17"); + assert_eq!(18u32.into_attribute_value().unwrap() , "18"); + assert_eq!(19u64.into_attribute_value().unwrap() , "19"); + assert_eq!( 16i8.into_attribute_value().unwrap() , "16"); + assert_eq!((-17i16).into_attribute_value().unwrap(), "-17"); + assert_eq!( 18i32.into_attribute_value().unwrap(), "18"); + assert_eq!((-19i64).into_attribute_value().unwrap(), "-19"); + assert_eq!(IpAddr::from_str("127.000.0.1").unwrap().into_attribute_value().unwrap(), "127.0.0.1"); + } +} diff --git a/minidom-rs/src/element.rs b/minidom-rs/src/element.rs new file mode 100644 index 0000000000000000000000000000000000000000..08ccc709ecad30354909a673d5eefd9321c349a8 --- /dev/null +++ b/minidom-rs/src/element.rs @@ -0,0 +1,979 @@ +//! Provides an `Element` type, which represents DOM nodes, and a builder to create them with. + +use crate::convert::IntoAttributeValue; +use crate::error::{Error, Result}; +use crate::namespace_set::NamespaceSet; +use crate::node::Node; + +use std::io:: Write; +use std::collections::{btree_map, BTreeMap}; + +use std::str; +use std::rc::Rc; +use std::borrow::Cow; + +use quick_xml::Reader as EventReader; +use quick_xml::Writer as EventWriter; +use quick_xml::events::{Event, BytesStart, BytesEnd, BytesDecl}; + +use std::io::BufRead; + +use std::str::FromStr; + +use std::slice; + +/// helper function to escape a `&[u8]` and replace all +/// xml special characters (<, >, &, ', ") with their corresponding +/// xml escaped value. +pub fn escape(raw: &[u8]) -> Cow<[u8]> { + let mut escapes: Vec<(usize, &'static [u8])> = Vec::new(); + let mut bytes = raw.iter(); + fn to_escape(b: u8) -> bool { + match b { + b'<' | b'>' | b'\'' | b'&' | b'"' => true, + _ => false, + } + } + + let mut loc = 0; + while let Some(i) = bytes.position(|&b| to_escape(b)) { + loc += i; + match raw[loc] { + b'<' => escapes.push((loc, b"<")), + b'>' => escapes.push((loc, b">")), + b'\'' => escapes.push((loc, b"'")), + b'&' => escapes.push((loc, b"&")), + b'"' => escapes.push((loc, b""")), + _ => unreachable!("Only '<', '>','\', '&' and '\"' are escaped"), + } + loc += 1; + } + + if escapes.is_empty() { + Cow::Borrowed(raw) + } else { + let len = raw.len(); + let mut v = Vec::with_capacity(len); + let mut start = 0; + for (i, r) in escapes { + v.extend_from_slice(&raw[start..i]); + v.extend_from_slice(r); + start = i + 1; + } + + if start < len { + v.extend_from_slice(&raw[start..]); + } + Cow::Owned(v) + } +} + + +#[derive(Clone, PartialEq, Eq, Debug)] +/// A struct representing a DOM Element. +pub struct Element { + prefix: Option, + name: String, + namespaces: Rc, + attributes: BTreeMap, + children: Vec, +} + +impl<'a> From<&'a Element> for String { + fn from(elem: &'a Element) -> String { + let mut writer = Vec::new(); + elem.write_to(&mut writer).unwrap(); + String::from_utf8(writer).unwrap() + } +} + +impl FromStr for Element { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut reader = EventReader::from_str(s); + Element::from_reader(&mut reader) + } +} + +impl Element { + fn new>(name: String, prefix: Option, namespaces: NS, attributes: BTreeMap, children: Vec) -> Element { + Element { + prefix, name, + namespaces: Rc::new(namespaces.into()), + attributes, + children, + } + } + + /// Return a builder for an `Element` with the given `name`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem = Element::builder("name") + /// .ns("namespace") + /// .attr("name", "value") + /// .append("inner") + /// .build(); + /// + /// assert_eq!(elem.name(), "name"); + /// assert_eq!(elem.ns(), Some("namespace".to_owned())); + /// assert_eq!(elem.attr("name"), Some("value")); + /// assert_eq!(elem.attr("inexistent"), None); + /// assert_eq!(elem.text(), "inner"); + /// ``` + pub fn builder>(name: S) -> ElementBuilder { + let (prefix, name) = split_element_name(name).unwrap(); + ElementBuilder { + root: Element::new(name, prefix, None, BTreeMap::new(), Vec::new()), + namespaces: Default::default(), + } + } + + /// Returns a bare minimum `Element` with this name. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let bare = Element::bare("name"); + /// + /// assert_eq!(bare.name(), "name"); + /// assert_eq!(bare.ns(), None); + /// assert_eq!(bare.attr("name"), None); + /// assert_eq!(bare.text(), ""); + /// ``` + pub fn bare>(name: S) -> Element { + Element { + prefix: None, + name: name.into(), + namespaces: Rc::new(NamespaceSet::default()), + attributes: BTreeMap::new(), + children: Vec::new(), + } + } + + /// Returns a reference to the name of this element. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns a reference to the prefix of this element. + /// + /// # Examples + /// ```rust + /// use minidom::Element; + /// + /// let elem = Element::builder("prefix:name") + /// .build(); + /// + /// assert_eq!(elem.name(), "name"); + /// assert_eq!(elem.prefix(), Some("prefix")); + /// ``` + pub fn prefix(&self) -> Option<&str> { + self.prefix.as_ref().map(String::as_ref) + } + + /// Returns a reference to the namespace of this element, if it has one, else `None`. + pub fn ns(&self) -> Option { + self.namespaces.get(&self.prefix) + } + + /// Returns a reference to the value of the given attribute, if it exists, else `None`. + pub fn attr(&self, name: &str) -> Option<&str> { + if let Some(value) = self.attributes.get(name) { + return Some(value) + } + None + } + + /// Returns an iterator over the attributes of this element. + /// + /// # Example + /// + /// ```rust + /// use minidom::Element; + /// + /// let elm: Element = "".parse().unwrap(); + /// + /// let mut iter = elm.attrs(); + /// + /// assert_eq!(iter.next().unwrap(), ("a", "b")); + /// assert_eq!(iter.next(), None); + /// ``` + pub fn attrs(&self) -> Attrs { + Attrs { + iter: self.attributes.iter(), + } + } + + /// Returns an iterator over the attributes of this element, with the value being a mutable + /// reference. + pub fn attrs_mut(&mut self) -> AttrsMut { + AttrsMut { + iter: self.attributes.iter_mut(), + } + } + + /// Modifies the value of an attribute. + pub fn set_attr, V: IntoAttributeValue>(&mut self, name: S, val: V) { + let name = name.into(); + let val = val.into_attribute_value(); + + if let Some(value) = self.attributes.get_mut(&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); + } + } + + /// Returns whether the element has the given name and namespace. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem = Element::builder("name").ns("namespace").build(); + /// + /// assert_eq!(elem.is("name", "namespace"), true); + /// assert_eq!(elem.is("name", "wrong"), false); + /// assert_eq!(elem.is("wrong", "namespace"), false); + /// assert_eq!(elem.is("wrong", "wrong"), false); + /// ``` + pub fn is, NS: AsRef>(&self, name: N, namespace: NS) -> bool { + self.name == name.as_ref() && + self.has_ns(namespace) + } + + /// Returns whether the element has the given namespace. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem = Element::builder("name").ns("namespace").build(); + /// + /// assert_eq!(elem.has_ns("namespace"), true); + /// assert_eq!(elem.has_ns("wrong"), false); + /// ``` + pub fn has_ns>(&self, namespace: NS) -> bool { + self.namespaces.has(&self.prefix, namespace) + } + + /// Parse a document from an `EventReader`. + pub fn from_reader(reader: &mut EventReader) -> Result { + let mut buf = Vec::new(); + + let root: Element = loop { + let e = reader.read_event(&mut buf)?; + match e { + Event::Empty(ref e) | Event::Start(ref e) => { + break build_element(reader, e)?; + }, + Event::Eof => { + return Err(Error::EndOfDocument); + }, + #[cfg(not(feature = "comments"))] + Event::Comment { .. } => { + return Err(Error::CommentsDisabled); + } + #[cfg(feature = "comments")] + Event::Comment { .. } => (), + Event::Text { .. } | + Event::End { .. } | + Event::CData { .. } | + Event::Decl { .. } | + Event::PI { .. } | + Event::DocType { .. } => (), // TODO: may need more errors + } + }; + + let mut stack = vec![root]; + + loop { + match reader.read_event(&mut buf)? { + Event::Empty(ref e) => { + let elem = build_element(reader, e)?; + // Since there is no Event::End after, directly append it to the current node + stack.last_mut().unwrap().append_child(elem); + }, + Event::Start(ref e) => { + let elem = build_element(reader, e)?; + stack.push(elem); + }, + Event::End(ref e) => { + if stack.len() <= 1 { + break; + } + let elem = stack.pop().unwrap(); + if let Some(to) = stack.last_mut() { + // TODO: check whether this is correct, we are comparing &[u8]s, not &strs + let elem_name = e.name(); + let mut split_iter = elem_name.splitn(2, |u| *u == 0x3A); + let possible_prefix = split_iter.next().unwrap(); // Can't be empty. + match split_iter.next() { + Some(name) => { + match elem.prefix() { + Some(prefix) => { + if possible_prefix != prefix.as_bytes() { + return Err(Error::InvalidElementClosed); + } + }, + None => { + return Err(Error::InvalidElementClosed); + }, + } + if name != elem.name().as_bytes() { + return Err(Error::InvalidElementClosed); + } + }, + None => { + if elem.prefix().is_some() { + return Err(Error::InvalidElementClosed); + } + if possible_prefix != elem.name().as_bytes() { + return Err(Error::InvalidElementClosed); + } + }, + } + to.append_child(elem); + } + }, + Event::Text(s) => { + let text = s.unescape_and_decode(reader)?; + if text != "" { + let current_elem = stack.last_mut().unwrap(); + current_elem.append_text_node(text); + } + }, + Event::CData(s) => { + let text = reader.decode(&s)?.to_owned(); + if text != "" { + let current_elem = stack.last_mut().unwrap(); + current_elem.append_text_node(text); + } + }, + Event::Eof => { + break; + }, + #[cfg(not(feature = "comments"))] + Event::Comment(_) => return Err(Error::CommentsDisabled), + #[cfg(feature = "comments")] + Event::Comment(s) => { + let comment = reader.decode(&s)?.to_owned(); + if comment != "" { + let current_elem = stack.last_mut().unwrap(); + current_elem.append_comment_node(comment); + } + }, + Event::Decl { .. } | + Event::PI { .. } | + Event::DocType { .. } => (), + } + } + Ok(stack.pop().unwrap()) + } + + /// Output a document to a `Writer`. + pub fn write_to(&self, writer: &mut W) -> Result<()> { + self.to_writer(&mut EventWriter::new(writer)) + } + + /// Output the document to quick-xml `Writer` + pub fn to_writer(&self, writer: &mut EventWriter) -> Result<()> { + writer.write_event(Event::Decl(BytesDecl::new(b"1.0", Some(b"utf-8"), None)))?; + self.write_to_inner(writer) + } + + /// Like `write_to()` but without the `` prelude + pub fn write_to_inner(&self, writer: &mut EventWriter) -> Result<()> { + let name = match self.prefix { + None => Cow::Borrowed(&self.name), + Some(ref prefix) => Cow::Owned(format!("{}:{}", prefix, self.name)), + }; + + let mut start = BytesStart::borrowed(name.as_bytes(), name.len()); + for (prefix, ns) in self.namespaces.declared_ns() { + match *prefix { + None => start.push_attribute(("xmlns", ns.as_ref())), + Some(ref prefix) => { + let key = format!("xmlns:{}", prefix); + start.push_attribute((key.as_bytes(), ns.as_bytes())) + }, + } + } + for (key, value) in &self.attributes { + start.push_attribute((key.as_bytes(), escape(value.as_bytes()).as_ref())); + } + + if self.children.is_empty() { + writer.write_event(Event::Empty(start))?; + return Ok(()) + } + + writer.write_event(Event::Start(start))?; + + for child in &self.children { + child.write_to_inner(writer)?; + } + + writer.write_event(Event::End(BytesEnd::borrowed(name.as_bytes())))?; + Ok(()) + } + + /// Returns an iterator over references to every child node of this element. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = "abc".parse().unwrap(); + /// + /// let mut iter = elem.nodes(); + /// + /// assert_eq!(iter.next().unwrap().as_text().unwrap(), "a"); + /// assert_eq!(iter.next().unwrap().as_element().unwrap().name(), "c1"); + /// assert_eq!(iter.next().unwrap().as_text().unwrap(), "b"); + /// assert_eq!(iter.next().unwrap().as_element().unwrap().name(), "c2"); + /// assert_eq!(iter.next().unwrap().as_text().unwrap(), "c"); + /// assert_eq!(iter.next(), None); + /// ``` + #[inline] pub fn nodes(&self) -> Nodes { + self.children.iter() + } + + /// Returns an iterator over mutable references to every child node of this element. + #[inline] pub fn nodes_mut(&mut self) -> NodesMut { + self.children.iter_mut() + } + + /// Returns an iterator over references to every child element of this element. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = "hellothisisignored".parse().unwrap(); + /// + /// let mut iter = elem.children(); + /// assert_eq!(iter.next().unwrap().name(), "child1"); + /// assert_eq!(iter.next().unwrap().name(), "child2"); + /// assert_eq!(iter.next().unwrap().name(), "child3"); + /// assert_eq!(iter.next(), None); + /// ``` + #[inline] pub fn children(&self) -> Children { + Children { + iter: self.children.iter(), + } + } + + /// Returns an iterator over mutable references to every child element of this element. + #[inline] pub fn children_mut(&mut self) -> ChildrenMut { + ChildrenMut { + iter: self.children.iter_mut(), + } + } + + /// Returns an iterator over references to every text node of this element. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = "hello world!".parse().unwrap(); + /// + /// let mut iter = elem.texts(); + /// assert_eq!(iter.next().unwrap(), "hello"); + /// assert_eq!(iter.next().unwrap(), " world!"); + /// assert_eq!(iter.next(), None); + /// ``` + #[inline] pub fn texts(&self) -> Texts { + Texts { + iter: self.children.iter(), + } + } + + /// Returns an iterator over mutable references to every text node of this element. + #[inline] pub fn texts_mut(&mut self) -> TextsMut { + TextsMut { + iter: self.children.iter_mut(), + } + } + + /// Appends a child node to the `Element`, returning the appended node. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let mut elem = Element::bare("root"); + /// + /// assert_eq!(elem.children().count(), 0); + /// + /// elem.append_child(Element::bare("child")); + /// + /// { + /// let mut iter = elem.children(); + /// assert_eq!(iter.next().unwrap().name(), "child"); + /// assert_eq!(iter.next(), None); + /// } + /// + /// let child = elem.append_child(Element::bare("new")); + /// + /// assert_eq!(child.name(), "new"); + /// ``` + pub fn append_child(&mut self, child: Element) -> &mut Element { + child.namespaces.set_parent(Rc::clone(&self.namespaces)); + + self.children.push(Node::Element(child)); + if let Node::Element(ref mut cld) = *self.children.last_mut().unwrap() { + cld + } else { + unreachable!() + } + } + + /// Appends a text node to an `Element`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let mut elem = Element::bare("node"); + /// + /// assert_eq!(elem.text(), ""); + /// + /// elem.append_text_node("text"); + /// + /// assert_eq!(elem.text(), "text"); + /// ``` + pub fn append_text_node>(&mut self, child: S) { + self.children.push(Node::Text(child.into())); + } + + /// Appends a comment node to an `Element`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let mut elem = Element::bare("node"); + /// + /// elem.append_comment_node("comment"); + /// ``` + #[cfg(feature = "comments")] + pub fn append_comment_node>(&mut self, child: S) { + self.children.push(Node::Comment(child.into())); + } + + /// Appends a node to an `Element`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::{Element, Node}; + /// + /// let mut elem = Element::bare("node"); + /// + /// elem.append_node(Node::Text("hello".to_owned())); + /// + /// assert_eq!(elem.text(), "hello"); + /// ``` + pub fn append_node(&mut self, node: Node) { + self.children.push(node); + } + + /// Returns the concatenation of all text nodes in the `Element`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = "hello, world!".parse().unwrap(); + /// + /// assert_eq!(elem.text(), "hello, world!"); + /// ``` + pub fn text(&self) -> String { + self.texts().fold(String::new(), |ret, new| ret + new) + } + + /// Returns a reference to the first child element with the specific name and namespace, if it + /// exists in the direct descendants of this `Element`, else returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = r#""#.parse().unwrap(); + /// + /// assert!(elem.get_child("a", "ns").unwrap().is("a", "ns")); + /// assert!(elem.get_child("a", "other_ns").unwrap().is("a", "other_ns")); + /// assert!(elem.get_child("b", "ns").unwrap().is("b", "ns")); + /// assert_eq!(elem.get_child("c", "ns"), None); + /// assert_eq!(elem.get_child("b", "other_ns"), None); + /// assert_eq!(elem.get_child("a", "inexistent_ns"), None); + /// ``` + pub fn get_child, NS: AsRef>(&self, name: N, namespace: NS) -> Option<&Element> { + for fork in &self.children { + if let Node::Element(ref e) = *fork { + if e.is(name.as_ref(), namespace.as_ref()) { + return Some(e); + } + } + } + None + } + + /// Returns a mutable reference to the first child element with the specific name and namespace, + /// if it exists in the direct descendants of this `Element`, else returns `None`. + pub fn get_child_mut, NS: AsRef>(&mut self, name: N, namespace: NS) -> Option<&mut Element> { + for fork in &mut self.children { + if let Node::Element(ref mut e) = *fork { + if e.is(name.as_ref(), namespace.as_ref()) { + return Some(e); + } + } + } + None + } + + /// Returns whether a specific child with this name and namespace exists in the direct + /// descendants of the `Element`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = r#""#.parse().unwrap(); + /// + /// assert_eq!(elem.has_child("a", "other_ns"), true); + /// assert_eq!(elem.has_child("a", "ns"), true); + /// assert_eq!(elem.has_child("a", "inexistent_ns"), false); + /// assert_eq!(elem.has_child("b", "ns"), true); + /// assert_eq!(elem.has_child("b", "other_ns"), false); + /// assert_eq!(elem.has_child("b", "inexistent_ns"), false); + /// ``` + pub fn has_child, NS: AsRef>(&self, name: N, namespace: NS) -> bool { + self.get_child(name, namespace).is_some() + } + + /// Removes the first child with this name and namespace, if it exists, and returns an + /// `Option` containing this child if it succeeds. + /// Returns `None` if no child matches this name and namespace. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let mut elem: Element = r#""#.parse().unwrap(); + /// + /// assert!(elem.remove_child("a", "ns").unwrap().is("a", "ns")); + /// assert!(elem.remove_child("a", "ns").is_none()); + /// assert!(elem.remove_child("inexistent", "inexistent").is_none()); + /// ``` + pub fn remove_child, NS: AsRef>(&mut self, name: N, namespace: NS) -> Option { + let name = name.as_ref(); + let namespace = namespace.as_ref(); + let idx = self.children.iter().position(|x| { + if let Node::Element(ref elm) = x { + elm.is(name, namespace) + } else { + false + } + })?; + self.children.remove(idx).into_element() + } +} + +fn split_element_name>(s: S) -> Result<(Option, String)> { + let name_parts = s.as_ref().split(':').collect::>(); + match name_parts.len() { + 2 => Ok((Some(name_parts[0].to_owned()), name_parts[1].to_owned())), + 1 => Ok((None, name_parts[0].to_owned())), + _ => Err(Error::InvalidElement), + } +} + +fn build_element(reader: &EventReader, event: &BytesStart) -> Result { + let mut namespaces = BTreeMap::new(); + let attributes = event.attributes() + .map(|o| { + let o = o?; + let key = str::from_utf8(o.key)?.to_owned(); + let value = o.unescape_and_decode_value(reader)?; + Ok((key, value)) + }) + .filter(|o| { + match *o { + Ok((ref key, ref value)) if key == "xmlns" => { + namespaces.insert(None, value.to_owned()); + false + }, + Ok((ref key, ref value)) if key.starts_with("xmlns:") => { + namespaces.insert(Some(key[6..].to_owned()), value.to_owned()); + false + }, + _ => true, + } + }) + .collect::>>()?; + + let (prefix, name) = split_element_name(str::from_utf8(event.name())?)?; + let element = Element::new(name, prefix, namespaces, attributes, Vec::new()); + Ok(element) +} + +/// An iterator over references to child elements of an `Element`. +pub struct Children<'a> { + iter: slice::Iter<'a, Node>, +} + +impl<'a> Iterator for Children<'a> { + type Item = &'a Element; + + fn next(&mut self) -> Option<&'a Element> { + for item in &mut self.iter { + if let Node::Element(ref child) = *item { + return Some(child); + } + } + None + } +} + +/// An iterator over mutable references to child elements of an `Element`. +pub struct ChildrenMut<'a> { + iter: slice::IterMut<'a, Node>, +} + +impl<'a> Iterator for ChildrenMut<'a> { + type Item = &'a mut Element; + + fn next(&mut self) -> Option<&'a mut Element> { + for item in &mut self.iter { + if let Node::Element(ref mut child) = *item { + return Some(child); + } + } + None + } +} + +/// An iterator over references to child text nodes of an `Element`. +pub struct Texts<'a> { + iter: slice::Iter<'a, Node>, +} + +impl<'a> Iterator for Texts<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option<&'a str> { + for item in &mut self.iter { + if let Node::Text(ref child) = *item { + return Some(child); + } + } + None + } +} + +/// An iterator over mutable references to child text nodes of an `Element`. +pub struct TextsMut<'a> { + iter: slice::IterMut<'a, Node>, +} + +impl<'a> Iterator for TextsMut<'a> { + type Item = &'a mut String; + + fn next(&mut self) -> Option<&'a mut String> { + for item in &mut self.iter { + if let Node::Text(ref mut child) = *item { + return Some(child); + } + } + None + } +} + +/// An iterator over references to all child nodes of an `Element`. +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.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.iter.next().map(|(x, y)| (x.as_ref(), y)) + } +} + +/// A builder for `Element`s. +pub struct ElementBuilder { + root: Element, + namespaces: BTreeMap, String>, +} + +impl ElementBuilder { + /// Sets the namespace. + pub fn ns>(mut self, namespace: S) -> ElementBuilder { + self.namespaces + .insert(self.root.prefix.clone(), namespace.into()); + self + } + + /// Sets an attribute. + pub fn attr, V: IntoAttributeValue>(mut self, name: S, value: V) -> ElementBuilder { + self.root.set_attr(name, value); + self + } + + /// Appends anything implementing `Into` into the tree. + pub fn append>(mut self, node: T) -> ElementBuilder { + self.root.append_node(node.into()); + self + } + + /// Appends an iterator of things implementing `Into` into the tree. + pub fn append_all, I: IntoIterator>(mut self, iter: I) -> ElementBuilder { + for node in iter { + self.root.append_node(node.into()); + } + self + } + + /// Builds the `Element`. + pub fn build(self) -> Element { + let mut element = self.root; + // Set namespaces + element.namespaces = Rc::new(NamespaceSet::from(self.namespaces)); + // Propagate namespaces + for node in &element.children { + if let Node::Element(ref e) = *node { + e.namespaces.set_parent(Rc::clone(&element.namespaces)); + } + } + element + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_element_new() { + use std::iter::FromIterator; + + let elem = Element::new( "name".to_owned() + , None + , Some("namespace".to_owned()) + , BTreeMap::from_iter(vec![ ("name".to_string(), "value".to_string()) ].into_iter() ) + , Vec::new() ); + + assert_eq!(elem.name(), "name"); + assert_eq!(elem.ns(), Some("namespace".to_owned())); + assert_eq!(elem.attr("name"), Some("value")); + assert_eq!(elem.attr("inexistent"), None); + } + + #[test] + fn test_from_reader_simple() { + let xml = ""; + let mut reader = EventReader::from_str(xml); + let elem = Element::from_reader(&mut reader); + + let elem2 = Element::builder("foo").build(); + + assert_eq!(elem.unwrap(), elem2); + } + + #[test] + fn test_from_reader_nested() { + let xml = ""; + let mut reader = EventReader::from_str(xml); + let elem = Element::from_reader(&mut reader); + + let nested = Element::builder("bar") + .attr("baz", "qxx") + .build(); + let elem2 = Element::builder("foo") + .append(nested) + .build(); + + assert_eq!(elem.unwrap(), elem2); + } + + #[test] + fn test_from_reader_with_prefix() { + let xml = ""; + let mut reader = EventReader::from_str(xml); + let elem = Element::from_reader(&mut reader); + + let nested = Element::builder("prefix:bar") + .attr("baz", "qxx") + .build(); + let elem2 = Element::builder("foo") + .append(nested) + .build(); + + assert_eq!(elem.unwrap(), elem2); + } + + #[test] + fn parses_spectest_xml() { // From: https://gitlab.com/lumi/minidom-rs/issues/8 + let xml = r#" + + + + "#; + let mut reader = EventReader::from_str(xml); + let _ = Element::from_reader(&mut reader).unwrap(); + } + + #[test] + fn does_not_unescape_cdata() { + let xml = "]]>"; + let mut reader = EventReader::from_str(xml); + let elem = Element::from_reader(&mut reader).unwrap(); + assert_eq!(elem.text(), "'>blah"); + } +} diff --git a/minidom-rs/src/error.rs b/minidom-rs/src/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..579cee405aca8c68af1aa1f97dd0ff02329774df --- /dev/null +++ b/minidom-rs/src/error.rs @@ -0,0 +1,83 @@ +//! Provides an error type for this crate. + +use std::convert::From; +use std::error::Error as StdError; + +/// Our main error type. +#[derive(Debug)] +pub enum Error { + /// An error from quick_xml. + XmlError(::quick_xml::Error), + + /// An UTF-8 conversion error. + Utf8Error(::std::str::Utf8Error), + + /// An I/O error, from std::io. + IoError(::std::io::Error), + + /// An error which is returned when the end of the document was reached prematurely. + EndOfDocument, + + /// An error which is returned when an element is closed when it shouldn't be + InvalidElementClosed, + + /// An error which is returned when an elemet's name contains more than one colon + InvalidElement, + + /// An error which is returned when a comment is to be parsed by minidom + #[cfg(not(comments))] + CommentsDisabled, +} + +impl StdError for Error { + fn cause(&self) -> Option<&dyn StdError> { + match self { + // TODO: return Some(e) for this case after the merge of + // https://github.com/tafia/quick-xml/pull/170 + Error::XmlError(_e) => None, + Error::Utf8Error(e) => Some(e), + Error::IoError(e) => Some(e), + Error::EndOfDocument => None, + Error::InvalidElementClosed => None, + Error::InvalidElement => None, + #[cfg(not(comments))] + Error::CommentsDisabled => None, + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Error::XmlError(e) => write!(fmt, "XML error: {}", e), + Error::Utf8Error(e) => write!(fmt, "UTF-8 error: {}", e), + Error::IoError(e) => write!(fmt, "IO error: {}", e), + Error::EndOfDocument => write!(fmt, "the end of the document has been reached prematurely"), + Error::InvalidElementClosed => write!(fmt, "the XML is invalid, an element was wrongly closed"), + Error::InvalidElement => write!(fmt, "the XML element is invalid"), + #[cfg(not(comments))] + Error::CommentsDisabled => write!(fmt, "a comment has been found even though comments are disabled by feature"), + } + } +} + +impl From<::quick_xml::Error> for Error { + fn from(err: ::quick_xml::Error) -> Error { + Error::XmlError(err) + } +} + +impl From<::std::str::Utf8Error> for Error { + fn from(err: ::std::str::Utf8Error) -> Error { + Error::Utf8Error(err) + } +} + +impl From<::std::io::Error> for Error { + fn from(err: ::std::io::Error) -> Error { + Error::IoError(err) + } +} + +/// Our simplified Result type. +pub type Result = ::std::result::Result; diff --git a/minidom-rs/src/lib.rs b/minidom-rs/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..c7f43b23010d693ef3df8a92e2969cb8c608aa41 --- /dev/null +++ b/minidom-rs/src/lib.rs @@ -0,0 +1,80 @@ +#![deny(missing_docs)] + +//! A minimal DOM crate built on top of quick-xml. +//! +//! This library exports an `Element` struct which represents a DOM tree. +//! +//! # Example +//! +//! Run with `cargo run --example articles`. Located in `examples/articles.rs`. +//! +//! ```rust,ignore +//! extern crate minidom; +//! +//! use minidom::Element; +//! +//! const DATA: &'static str = r#" +//!
+//! 10 Terrible Bugs You Would NEVER Believe Happened +//! +//! Rust fixed them all. <3 +//! +//!
+//!
+//! BREAKING NEWS: Physical Bug Jumps Out Of Programmer's Screen +//! +//! Just kidding! +//! +//!
+//!
"#; +//! +//! const ARTICLE_NS: &'static str = "article"; +//! +//! #[derive(Debug)] +//! pub struct Article { +//! title: String, +//! body: String, +//! } +//! +//! fn main() { +//! let root: Element = DATA.parse().unwrap(); +//! +//! let mut articles: Vec
= Vec::new(); +//! +//! for child in root.children() { +//! if child.is("article", ARTICLE_NS) { +//! let title = child.get_child("title", ARTICLE_NS).unwrap().text(); +//! let body = child.get_child("body", ARTICLE_NS).unwrap().text(); +//! articles.push(Article { +//! title: title, +//! body: body.trim().to_owned(), +//! }); +//! } +//! } +//! +//! println!("{:?}", articles); +//! } +//! ``` +//! +//! # Usage +//! +//! To use `minidom`, add this to your `Cargo.toml` under `dependencies`: +//! +//! ```toml,ignore +//! minidom = "*" +//! ``` + +pub use quick_xml; + +pub mod error; +pub mod element; +pub mod convert; +pub mod node; +mod namespace_set; + +#[cfg(test)] mod tests; + +pub use error::{Error, Result}; +pub use element::{Element, Children, ChildrenMut, ElementBuilder}; +pub use node::Node; +pub use convert::IntoAttributeValue; diff --git a/minidom-rs/src/namespace_set.rs b/minidom-rs/src/namespace_set.rs new file mode 100644 index 0000000000000000000000000000000000000000..163a87304aadb1320e4ff4731f2a8f66614d97d6 --- /dev/null +++ b/minidom-rs/src/namespace_set.rs @@ -0,0 +1,170 @@ +use std::collections::BTreeMap; +use std::cell::RefCell; +use std::fmt; +use std::rc::Rc; + + +#[derive(Clone, PartialEq, Eq)] +pub struct NamespaceSet { + parent: RefCell>>, + namespaces: BTreeMap, String>, +} + +impl Default for NamespaceSet { + fn default() -> Self { + NamespaceSet { + parent: RefCell::new(None), + namespaces: BTreeMap::new(), + } + } +} + +impl fmt::Debug for NamespaceSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "NamespaceSet(")?; + for (prefix, namespace) in &self.namespaces { + write!(f, "xmlns{}={:?}, ", match prefix { + None => String::new(), + Some(prefix) => format!(":{}", prefix), + }, namespace)?; + } + write!(f, "parent: {:?})", *self.parent.borrow()) + } +} + +impl NamespaceSet { + pub fn declared_ns(&self) -> &BTreeMap, String> { + &self.namespaces + } + + pub fn get(&self, prefix: &Option) -> Option { + match self.namespaces.get(prefix) { + Some(ns) => Some(ns.clone()), + None => match *self.parent.borrow() { + None => None, + Some(ref parent) => parent.get(prefix) + }, + } + } + + pub fn has>(&self, prefix: &Option, wanted_ns: NS) -> bool { + match self.namespaces.get(prefix) { + Some(ns) => + ns == wanted_ns.as_ref(), + None => match *self.parent.borrow() { + None => + false, + Some(ref parent) => + parent.has(prefix, wanted_ns), + }, + } + } + + pub fn set_parent(&self, parent: Rc) { + let mut parent_ns = self.parent.borrow_mut(); + let new_set = parent; + *parent_ns = Some(new_set); + } + +} + +impl From, String>> for NamespaceSet { + fn from(namespaces: BTreeMap, String>) -> Self { + NamespaceSet { + parent: RefCell::new(None), + namespaces, + } + } +} + +impl From> for NamespaceSet { + fn from(namespace: Option) -> Self { + match namespace { + None => Self::default(), + Some(namespace) => Self::from(namespace), + } + } +} + +impl From for NamespaceSet { + fn from(namespace: String) -> Self { + let mut namespaces = BTreeMap::new(); + namespaces.insert(None, namespace); + + NamespaceSet { + parent: RefCell::new(None), + namespaces, + } + } +} + +impl From<(Option, String)> for NamespaceSet { + fn from(prefix_namespace: (Option, String)) -> Self { + let (prefix, namespace) = prefix_namespace; + let mut namespaces = BTreeMap::new(); + namespaces.insert(prefix, namespace); + + NamespaceSet { + parent: RefCell::new(None), + namespaces, + } + } +} + +impl From<(String, String)> for NamespaceSet { + fn from(prefix_namespace: (String, String)) -> Self { + let (prefix, namespace) = prefix_namespace; + Self::from((Some(prefix), namespace)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_has() { + let namespaces = NamespaceSet::from("foo".to_owned()); + assert_eq!(namespaces.get(&None), Some("foo".to_owned())); + assert!(namespaces.has(&None, "foo")); + } + + #[test] + fn get_has_prefixed() { + let namespaces = NamespaceSet::from(("x".to_owned(), "bar".to_owned())); + assert_eq!(namespaces.get(&Some("x".to_owned())), Some("bar".to_owned())); + assert!(namespaces.has(&Some("x".to_owned()), "bar")); + } + + #[test] + fn get_has_recursive() { + let mut parent = NamespaceSet::from("foo".to_owned()); + for _ in 0..1000 { + let namespaces = NamespaceSet::default(); + namespaces.set_parent(Rc::new(parent)); + assert_eq!(namespaces.get(&None), Some("foo".to_owned())); + assert!(namespaces.has(&None, "foo")); + parent = namespaces; + } + } + + #[test] + fn get_has_prefixed_recursive() { + let mut parent = NamespaceSet::from(("x".to_owned(), "bar".to_owned())); + for _ in 0..1000 { + let namespaces = NamespaceSet::default(); + namespaces.set_parent(Rc::new(parent)); + assert_eq!(namespaces.get(&Some("x".to_owned())), Some("bar".to_owned())); + assert!(namespaces.has(&Some("x".to_owned()), "bar")); + parent = namespaces; + } + } + + #[test] + fn debug_looks_correct() { + let parent = NamespaceSet::from("http://www.w3.org/2000/svg".to_owned()); + let namespaces = NamespaceSet::from(("xhtml".to_owned(), "http://www.w3.org/1999/xhtml".to_owned())); + namespaces.set_parent(Rc::new(parent)); + assert_eq!(format!("{:?}", namespaces), "NamespaceSet(xmlns:xhtml=\"http://www.w3.org/1999/xhtml\", parent: Some(NamespaceSet(xmlns=\"http://www.w3.org/2000/svg\", parent: None)))"); + } +} diff --git a/minidom-rs/src/node.rs b/minidom-rs/src/node.rs new file mode 100644 index 0000000000000000000000000000000000000000..e0d9a9ebbcf5dd3c2d68a879664491b042c07568 --- /dev/null +++ b/minidom-rs/src/node.rs @@ -0,0 +1,207 @@ +//! Provides the `Node` struct, which represents a node in the DOM. + +use crate::element::{Element, ElementBuilder}; +use crate::error::Result; + +use std::io::Write; + +use quick_xml::Writer as EventWriter; +use quick_xml::events::{Event, BytesText}; + +/// A node in an element tree. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Node { + /// An `Element`. + Element(Element), + /// A text node. + Text(String), + #[cfg(feature = "comments")] + /// A comment node. + Comment(String), +} + +impl Node { + /// Turns this into a reference to an `Element` if this is an element node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let elm = Node::Element("".parse().unwrap()); + /// let txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.as_element().unwrap().name(), "meow"); + /// assert_eq!(txt.as_element(), None); + /// ``` + pub fn as_element(&self) -> Option<&Element> { + match *self { + Node::Element(ref e) => Some(e), + Node::Text(_) => None, + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + /// Turns this into a mutable reference of an `Element` if this is an element node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let mut elm = Node::Element("".parse().unwrap()); + /// let mut txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.as_element_mut().unwrap().name(), "meow"); + /// assert_eq!(txt.as_element_mut(), None); + /// ``` + pub fn as_element_mut(&mut self) -> Option<&mut Element> { + match *self { + Node::Element(ref mut e) => Some(e), + Node::Text(_) => None, + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + /// Turns this into an `Element`, consuming self, if this is an element node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let elm = Node::Element("".parse().unwrap()); + /// let txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.into_element().unwrap().name(), "meow"); + /// assert_eq!(txt.into_element(), None); + /// ``` + pub fn into_element(self) -> Option { + match self { + Node::Element(e) => Some(e), + Node::Text(_) => None, + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + /// Turns this into an `&str` if this is a text node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let elm = Node::Element("".parse().unwrap()); + /// let txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.as_text(), None); + /// assert_eq!(txt.as_text().unwrap(), "meow"); + /// ``` + pub fn as_text(&self) -> Option<&str> { + match *self { + Node::Element(_) => None, + Node::Text(ref s) => Some(s), + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + /// Turns this into an `&mut String` if this is a text node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let mut elm = Node::Element("".parse().unwrap()); + /// let mut txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.as_text_mut(), None); + /// { + /// let text_mut = txt.as_text_mut().unwrap(); + /// assert_eq!(text_mut, "meow"); + /// text_mut.push_str("zies"); + /// assert_eq!(text_mut, "meowzies"); + /// } + /// assert_eq!(txt.as_text().unwrap(), "meowzies"); + /// ``` + pub fn as_text_mut(&mut self) -> Option<&mut String> { + match *self { + Node::Element(_) => None, + Node::Text(ref mut s) => Some(s), + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + /// Turns this into an `String`, consuming self, if this is a text node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let elm = Node::Element("".parse().unwrap()); + /// let txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.into_text(), None); + /// assert_eq!(txt.into_text().unwrap(), "meow"); + /// ``` + pub fn into_text(self) -> Option { + match self { + Node::Element(_) => None, + Node::Text(s) => Some(s), + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + #[doc(hidden)] + pub(crate) fn write_to_inner(&self, writer: &mut EventWriter) -> Result<()>{ + match *self { + Node::Element(ref elmt) => elmt.write_to_inner(writer)?, + Node::Text(ref s) => { + writer.write_event(Event::Text(BytesText::from_plain_str(s)))?; + }, + #[cfg(feature = "comments")] + Node::Comment(ref s) => { + writer.write_event(Event::Comment(BytesText::from_plain_str(s)))?; + }, + } + + Ok(()) + } +} + +impl From for Node { + fn from(elm: Element) -> Node { + Node::Element(elm) + } +} + +impl From for Node { + fn from(s: String) -> Node { + Node::Text(s) + } +} + +impl<'a> From<&'a str> for Node { + fn from(s: &'a str) -> Node { + Node::Text(s.to_owned()) + } +} + +impl From for Node { + fn from(builder: ElementBuilder) -> Node { + Node::Element(builder.build()) + } +} diff --git a/minidom-rs/src/tests.rs b/minidom-rs/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..046d8e5c817ac79febb208709c4f96b7671f8297 --- /dev/null +++ b/minidom-rs/src/tests.rs @@ -0,0 +1,251 @@ +use crate::element::Element; + +use quick_xml::Reader; + +const TEST_STRING: &'static str = r#"meownya"#; + +fn build_test_tree() -> Element { + let mut root = Element::builder("root") + .ns("root_ns") + .attr("xml:lang", "en") + .attr("a", "b") + .build(); + root.append_text_node("meow"); + let child = Element::builder("child") + .attr("c", "d") + .build(); + root.append_child(child); + let other_child = Element::builder("child") + .ns("child_ns") + .attr("d", "e") + .attr("xml:lang", "fr") + .build(); + root.append_child(other_child); + root.append_text_node("nya"); + root +} + +#[cfg(feature = "comments")] +const COMMENT_TEST_STRING: &'static str = r#""#; + +#[cfg(feature = "comments")] +fn build_comment_test_tree() -> Element { + let mut root = Element::builder("root").build(); + root.append_comment_node("This is a child."); + let mut child = Element::builder("child").attr("attr", "val").build(); + child.append_comment_node("This is a grandchild."); + let grand_child = Element::builder("grandchild").build(); + child.append_child(grand_child); + root.append_child(child); + root +} + +#[test] +fn reader_works() { + let mut reader = Reader::from_str(TEST_STRING); + assert_eq!(Element::from_reader(&mut reader).unwrap(), build_test_tree()); +} + +#[test] +fn writer_works() { + let root = build_test_tree(); + let mut writer = Vec::new(); + { + root.write_to(&mut writer).unwrap(); + } + assert_eq!(String::from_utf8(writer).unwrap(), TEST_STRING); +} + +#[test] +fn writer_escapes_attributes() { + let root = Element::builder("root") + .attr("a", "\"Air\" quotes") + .build(); + let mut writer = Vec::new(); + { + root.write_to(&mut writer).unwrap(); + } + assert_eq!(String::from_utf8(writer).unwrap(), + r#""# + ); +} + +#[test] +fn writer_escapes_text() { + let root = Element::builder("root") + .append("<3") + .build(); + let mut writer = Vec::new(); + { + root.write_to(&mut writer).unwrap(); + } + assert_eq!(String::from_utf8(writer).unwrap(), + r#"<3"# + ); +} + +#[test] +fn builder_works() { + let elem = Element::builder("a") + .ns("b") + .attr("c", "d") + .append(Element::builder("child")) + .append("e") + .build(); + assert_eq!(elem.name(), "a"); + assert_eq!(elem.ns(), Some("b".to_owned())); + assert_eq!(elem.attr("c"), Some("d")); + assert_eq!(elem.attr("x"), None); + assert_eq!(elem.text(), "e"); + assert!(elem.has_child("child", "b")); + assert!(elem.is("a", "b")); +} + +#[test] +fn children_iter_works() { + let root = build_test_tree(); + let mut iter = root.children(); + assert!(iter.next().unwrap().is("child", "root_ns")); + assert!(iter.next().unwrap().is("child", "child_ns")); + assert_eq!(iter.next(), None); +} + +#[test] +fn get_child_works() { + let root = build_test_tree(); + assert_eq!(root.get_child("child", "inexistent_ns"), None); + assert_eq!(root.get_child("not_a_child", "root_ns"), None); + assert!(root.get_child("child", "root_ns").unwrap().is("child", "root_ns")); + assert!(root.get_child("child", "child_ns").unwrap().is("child", "child_ns")); + assert_eq!(root.get_child("child", "root_ns").unwrap().attr("c"), Some("d")); + assert_eq!(root.get_child("child", "child_ns").unwrap().attr("d"), Some("e")); +} + +#[test] +fn namespace_propagation_works() { + let mut root = Element::builder("root").ns("root_ns").build(); + let mut child = Element::bare("child"); + let grandchild = Element::bare("grandchild"); + child.append_child(grandchild); + root.append_child(child); + + assert_eq!(root.get_child("child", "root_ns").unwrap().ns(), root.ns()); + assert_eq!(root.get_child("child", "root_ns").unwrap() + .get_child("grandchild", "root_ns").unwrap() + .ns(), root.ns()); +} + +#[test] +fn two_elements_with_same_arguments_different_order_are_equal() { + let elem1: Element = "".parse().unwrap(); + let elem2: Element = "".parse().unwrap(); + assert_eq!(elem1, elem2); + + let elem1: Element = "".parse().unwrap(); + let elem2: Element = "".parse().unwrap(); + assert_ne!(elem1, elem2); +} + +#[test] +fn namespace_attributes_works() { + let mut reader = Reader::from_str(TEST_STRING); + let root = Element::from_reader(&mut reader).unwrap(); + assert_eq!("en", root.attr("xml:lang").unwrap()); + assert_eq!("fr", root.get_child("child", "child_ns").unwrap().attr("xml:lang").unwrap()); +} + +#[test] +fn wrongly_closed_elements_error() { + let elem1 = "".parse::(); + assert!(elem1.is_err()); + let elem1 = "".parse::(); + assert!(elem1.is_err()); + let elem1 = "".parse::(); + assert!(elem1.is_ok()); +} + +#[test] +fn namespace_simple() { + let elem: Element = "".parse().unwrap(); + assert_eq!(elem.name(), "message"); + assert_eq!(elem.ns(), Some("jabber:client".to_owned())); +} + +#[test] +fn namespace_prefixed() { + let elem: Element = "" + .parse().unwrap(); + assert_eq!(elem.name(), "features"); + assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned())); +} + +#[test] +fn namespace_inherited_simple() { + let elem: Element = "".parse().unwrap(); + assert_eq!(elem.name(), "stream"); + assert_eq!(elem.ns(), Some("jabber:client".to_owned())); + let child = elem.children().next().unwrap(); + assert_eq!(child.name(), "message"); + assert_eq!(child.ns(), Some("jabber:client".to_owned())); +} + +#[test] +fn namespace_inherited_prefixed1() { + let elem: Element = "" + .parse().unwrap(); + assert_eq!(elem.name(), "features"); + assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned())); + let child = elem.children().next().unwrap(); + assert_eq!(child.name(), "message"); + assert_eq!(child.ns(), Some("jabber:client".to_owned())); +} + +#[test] +fn namespace_inherited_prefixed2() { + let elem: Element = "" + .parse().unwrap(); + assert_eq!(elem.name(), "stream"); + assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned())); + let child = elem.children().next().unwrap(); + assert_eq!(child.name(), "message"); + assert_eq!(child.ns(), Some("jabber:client".to_owned())); +} + +#[cfg(feature = "comments")] +#[test] +fn read_comments() { + let mut reader = Reader::from_str(COMMENT_TEST_STRING); + assert_eq!(Element::from_reader(&mut reader).unwrap(), build_comment_test_tree()); +} + +#[cfg(feature = "comments")] +#[test] +fn write_comments() { + let root = build_comment_test_tree(); + let mut writer = Vec::new(); + { + root.write_to(&mut writer).unwrap(); + } + assert_eq!(String::from_utf8(writer).unwrap(), COMMENT_TEST_STRING); +} + +#[test] +fn xml_error() { + match "".parse::() { + Err(crate::error::Error::XmlError(_)) => (), + err => panic!("No or wrong error: {:?}", err) + } + + match "() { + Err(crate::error::Error::XmlError(_)) => (), + err => panic!("No or wrong error: {:?}", err) + } +} + +#[test] +fn invalid_element_error() { + match "".parse::() { + Err(crate::error::Error::InvalidElement) => (), + err => panic!("No or wrong error: {:?}", err) + } +}