terrio 0.2.0

Pure-Rust geospatial raster, vector and point cloud data I/O: read, write, convert and reproject GIS formats without C dependencies
Documentation
//! A small XML element tree, for the XML vector formats (GML, GPX, KML).
//!
//! Names are local names: namespace prefixes are dropped, which is how the formats' readers look
//! elements up.

use crate::{Error, Result};
use quick_xml::events::{BytesStart, Event};
use quick_xml::{Reader, XmlVersion};

/// An XML element.
#[derive(Debug, Clone, Default)]
pub struct Element {
    /// The local name (without namespace prefix).
    pub name: String,
    /// The namespace prefix, empty when there is none.
    pub prefix: String,
    /// The attributes, by local name, unescaped.
    pub attrs: Vec<(String, String)>,
    /// The child elements, in document order.
    pub children: Vec<Element>,
    /// The text content directly inside the element (not its children's), unescaped.
    pub text: String,
}

impl Element {
    /// The value of an attribute, by local name.
    pub fn attr(&self, name: &str) -> Option<&str> {
        self.attrs.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
    }

    /// The first child with a local name.
    pub fn child(&self, name: &str) -> Option<&Element> {
        self.children.iter().find(|c| c.name == name)
    }

    /// The children with a local name.
    pub fn children_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Element> + 'a {
        self.children.iter().filter(move |c| c.name == name)
    }

    /// The trimmed text of the first child with a local name, when it is not empty.
    pub fn child_text(&self, name: &str) -> Option<&str> {
        self.child(name).map(|c| c.text.trim()).filter(|t| !t.is_empty())
    }

    /// The first descendant (depth first, this element included) with a local name.
    pub fn find(&self, name: &str) -> Option<&Element> {
        if self.name == name {
            return Some(self);
        }
        self.children.iter().find_map(|c| c.find(name))
    }

    /// Every descendant with a local name, in document order, not looking inside matches.
    pub fn find_all<'a>(&'a self, name: &str, out: &mut Vec<&'a Element>) {
        for c in &self.children {
            if c.name == name {
                out.push(c);
            } else {
                c.find_all(name, out);
            }
        }
    }
}

fn local(name: &str) -> String {
    name.rsplit_once(':').map_or(name, |(_, l)| l).to_string()
}

fn element(e: &BytesStart) -> Element {
    let attrs = e
        .attributes()
        .flatten()
        .map(|a| (local(a.key.as_ref()), a.normalized_value(XmlVersion::Implicit1_0).map(|v| v.into_owned()).unwrap_or_default()))
        .collect();
    let name = e.name();
    let qname: &str = name.as_ref();
    let prefix = qname.rsplit_once(':').map(|(p, _)| p.to_string()).unwrap_or_default();
    Element { name: local(qname), prefix, attrs, ..Default::default() }
}

/// Decodes a document's bytes: UTF-8 (with or without a byte order mark), or Latin-1 when the
/// bytes are not UTF-8.
pub fn decode(bytes: &[u8]) -> String {
    let bytes = bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes);
    match std::str::from_utf8(bytes) {
        Ok(s) => s.to_string(),
        Err(_) => bytes.iter().map(|&b| b as char).collect(),
    }
}

/// Parses a document and returns its root element.
pub fn parse(text: &str) -> Result<Element> {
    let err = |e: &dyn std::fmt::Display| Error::InvalidData(format!("XML: {e}"));
    let mut reader = Reader::from_str(text);
    reader.config_mut().check_end_names = false;
    let mut stack: Vec<Element> = vec![Element::default()];
    loop {
        match reader.read_event().map_err(|e| err(&e))? {
            Event::Start(e) => stack.push(element(&e)),
            Event::Empty(e) => {
                let el = element(&e);
                if let Some(parent) = stack.last_mut() {
                    parent.children.push(el);
                }
            }
            Event::End(_) => {
                if stack.len() > 1 {
                    let el = stack.pop().unwrap_or_default();
                    if let Some(parent) = stack.last_mut() {
                        parent.children.push(el);
                    }
                }
            }
            Event::Text(t) => {
                if let Some(top) = stack.last_mut() {
                    top.text.push_str(&t.xml10_content());
                }
            }
            Event::CData(t) => {
                if let Some(top) = stack.last_mut() {
                    top.text.push_str(&t.xml10_content());
                }
            }
            Event::GeneralRef(r) => {
                let c = match r.resolve_char_ref().ok().flatten() {
                    Some(c) => Some(c),
                    None => match &*r {
                        "amp" => Some('&'),
                        "lt" => Some('<'),
                        "gt" => Some('>'),
                        "quot" => Some('"'),
                        "apos" => Some('\''),
                        _ => None,
                    },
                };
                if let (Some(c), Some(top)) = (c, stack.last_mut()) {
                    top.text.push(c);
                }
            }
            Event::Eof => break,
            _ => {}
        }
    }
    // Close elements left open by a truncated document.
    while stack.len() > 1 {
        let el = stack.pop().unwrap_or_default();
        if let Some(parent) = stack.last_mut() {
            parent.children.push(el);
        }
    }
    let doc = stack.pop().unwrap_or_default();
    doc.children.into_iter().next().ok_or_else(|| Error::InvalidData("XML: no root element".into()))
}

/// Escapes text for XML content and attribute values.
pub fn escape(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&apos;"),
            c => out.push(c),
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_elements_text_and_entities() {
        let root = parse(r#"<?xml version="1.0"?><a:root xmlns:a="x" a:id="1"><b>T &amp; &#233;<![CDATA[<c>]]></b><b/></a:root>"#).unwrap();
        assert_eq!((root.name.as_str(), root.prefix.as_str()), ("root", "a"));
        assert_eq!(root.attr("id"), Some("1"));
        assert_eq!(root.children_named("b").count(), 2);
        assert_eq!(root.child_text("b"), Some("T & é<c>"));
        assert_eq!(escape("a<b & \"c\""), "a&lt;b &amp; &quot;c&quot;");
    }
}