use std::borrow::Cow;
use std::default::Default;
use std::io::{Cursor, Write};
use std::path::Path;
use crate::error::OtherError;
use crate::xdoc::error::Result;
use crate::{Element, Index, Misc, Pi, WriteOpts};
#[derive(Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash)]
pub enum Version {
V10,
V11,
}
impl Default for Version {
fn default() -> Self {
Version::V10
}
}
#[derive(Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash)]
pub enum Encoding {
Utf8,
}
impl Default for Encoding {
fn default() -> Self {
Encoding::Utf8
}
}
#[derive(Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash, Default)]
pub struct Declaration {
pub version: Option<Version>,
pub encoding: Option<Encoding>,
}
#[derive(Debug, Clone, Eq, PartialOrd, PartialEq, Hash, Default)]
pub struct Document {
prolog: Prolog,
root: Element,
epilog_misc: Vec<Misc>,
}
impl Unpin for Document {}
#[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Hash, Default)]
struct Prolog {
xml_decl: Declaration,
misc_before_doctype: Vec<Misc>,
doctypedecl: Option<String>,
misc_after_doctype: Vec<Misc>,
}
impl<'a> From<&'a Element> for Cow<'a, Element> {
fn from(x: &'a Element) -> Cow<'a, Element> {
Cow::Borrowed(x)
}
}
impl<'a> From<Element> for Cow<'a, Element> {
fn from(x: Element) -> Cow<'a, Element> {
Cow::Owned(x)
}
}
impl<'a> Document {
pub fn new() -> Document {
Document::default()
}
}
impl Document {
pub fn from_root(root: Element) -> Self {
Document {
prolog: Default::default(),
root,
epilog_misc: Vec::new(),
}
}
pub fn root(&self) -> &Element {
&self.root
}
pub fn set_root(&mut self, element_data: Element) {
self.root = element_data;
}
pub fn root_mut(&mut self) -> &mut Element {
&mut self.root
}
pub fn declaration(&self) -> &Declaration {
&self.prolog.xml_decl
}
pub fn set_declaration(&mut self, declaration: Declaration) {
self.prolog.xml_decl = declaration;
}
#[cfg(feature = "doctype_wip")]
pub fn set_doctype<S: Into<String>>(&mut self, doctype: S) -> Result<()> {
self.prolog.doctypedecl = Some(doctype.into());
Ok(())
}
#[cfg(not(feature = "doctype_wip"))]
pub fn set_doctype<S: Into<String>>(&mut self, _: S) -> Result<()> {
Ok(())
}
pub fn add_prolog_comment<S: Into<String>>(&mut self, comment: S) -> Result<()> {
if self.prolog.doctypedecl.is_none() {
self.prolog
.misc_before_doctype
.push(Misc::Comment(comment.into()));
} else {
self.prolog
.misc_after_doctype
.push(Misc::Comment(comment.into()));
}
Ok(())
}
pub fn add_prolog_pi(&mut self, pi: Pi) {
if self.prolog.doctypedecl.is_none() {
self.prolog.misc_before_doctype.push(Misc::Pi(pi));
} else {
self.prolog.misc_after_doctype.push(Misc::Pi(pi));
}
}
pub fn clear_prolog_misc(&mut self) {
self.prolog.misc_before_doctype.clear();
self.prolog.misc_after_doctype.clear();
}
pub fn prolog_misc(&self) -> impl Iterator<Item = &Misc> + '_ {
self.prolog
.misc_before_doctype
.iter()
.chain(self.prolog.misc_after_doctype.iter())
}
pub fn add_epilog_comment<S: Into<String>>(&mut self, comment: S) -> Result<()> {
self.epilog_misc.push(Misc::Comment(comment.into()));
Ok(())
}
pub fn add_epilog_pi(&mut self, pi: Pi) {
self.epilog_misc.push(Misc::Pi(pi));
}
pub fn clear_epilog_misc(&mut self) {
self.epilog_misc.clear()
}
pub fn epilog_misc(&self) -> std::slice::Iter<'_, Misc> {
self.epilog_misc.iter()
}
pub fn write<W>(&self, writer: &mut W) -> crate::error::Result<()>
where
W: Write,
{
self.write_opts(writer, &WriteOpts::default())
.map_err(crate::error::Error::XdocErr)
}
pub fn write_opts<W>(&self, writer: &mut W, opts: &WriteOpts) -> Result<()>
where
W: Write,
{
if self.declaration().encoding.is_some() || self.declaration().version.is_some() {
xwrite!(writer, "<?xml ")?;
let need_space = true;
if let Some(version) = &self.declaration().version {
match version {
Version::V10 => {
xwrite!(writer, "version=\"1.0\"")?;
}
Version::V11 => {
xwrite!(writer, "version=\"1.1\"")?;
}
}
}
if let Some(encoding) = &self.declaration().encoding {
match encoding {
Encoding::Utf8 => {
if need_space {
xwrite!(writer, " ")?;
}
xwrite!(writer, "encoding=\"UTF-8\"")?
}
}
}
xwrite!(writer, "?>")?;
if let Err(e) = opts.newline(writer) {
return wrap_err!(e);
}
}
for misc in &self.prolog.misc_before_doctype {
misc.write(writer, opts, 0)?;
opts.newline(writer)?;
}
if let Some(doctype) = &self.prolog.doctypedecl {
xwrite!(writer, "{}", doctype)?;
opts.newline(writer)?;
}
for misc in &self.prolog.misc_after_doctype {
misc.write(writer, opts, 0)?;
opts.newline(writer)?;
}
self.root().write(writer, opts, 0)?;
for misc in self.epilog_misc() {
opts.newline(writer)?;
misc.write(writer, opts, 0)?;
}
opts.newline(writer)?;
Ok(())
}
pub fn to_string_opts(&self, opts: &WriteOpts) -> Result<String> {
let mut c = Cursor::new(Vec::new());
self.write_opts(&mut c, &opts)?;
let data = c.into_inner();
match std::str::from_utf8(data.as_slice()) {
Ok(s) => Ok(s.to_owned()),
Err(e) => wrap_err!(e),
}
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> crate::error::Result<()> {
std::fs::write(path.as_ref(), self.to_string().as_bytes()).map_err(|e| {
crate::error::Error::Other(OtherError {
throw_site: throw_site!(),
message: Some(format!("Unable to save file '{}'", path.as_ref().display())),
source: Some(Box::new(e)),
})
})
}
pub fn index(self) -> Index {
Index::build(self)
}
}
impl ToString for Document {
fn to_string(&self) -> String {
let opts = WriteOpts::default();
match self.to_string_opts(&opts) {
Ok(s) => s,
Err(_) => "<error/>".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use crate::Node;
use super::*;
fn assert_ezfile(doc: &Document) {
let root = doc.root();
let root_data = root;
assert_eq!("cats", root_data.name());
assert_eq!(None, root_data.prefix());
assert_eq!(0, root_data.attributes_len());
assert_eq!(2, root_data.nodes_len());
let bones_element = root_data.node(0).unwrap();
if let Node::Element(bones) = bones_element {
assert_eq!("cat", bones.name());
assert_eq!(None, bones.prefix());
assert_eq!(1, bones.attributes_len());
assert_eq!(0, bones.nodes_len());
let name_attribute_value = bones.attribute("name").unwrap();
assert_eq!("bones", name_attribute_value);
} else {
panic!("bones was supposed to be an element but was not");
}
let bishop_element = root_data.node(1).unwrap();
if let Node::Element(bishop) = bishop_element {
assert_eq!("cat", bishop.name());
assert_eq!(None, bishop.prefix());
assert_eq!(1, bishop.attributes_len());
let name_attribute_value = bishop.attribute("name").unwrap();
assert_eq!("bishop", name_attribute_value);
assert_eq!(1, bishop.nodes_len());
if let Node::Text(text) = bishop.node(0).unwrap() {
assert_eq!("punks", text);
} else {
panic!("Expected to find a text node but it was not there.");
}
} else {
panic!("bones was supposed to be an element but was not");
}
}
const EZFILE_STR: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<cats>
<cat name="bones"/>
<cat name="bishop">punks</cat>
</cats>
"#;
fn create_ezfile() -> Document {
let mut bones = Element::from_name("cat");
bones.add_attribute("name", "bones");
let mut bishop = Element::from_name("cat");
bishop.add_attribute("name", "bishop");
bishop.add_text("punks");
let mut cats = Element::from_name("cats");
cats.add_child(bones);
cats.add_child(bishop);
let mut doc = Document::from_root(cats);
doc.set_declaration(Declaration {
version: Some(Version::V10),
encoding: Some(Encoding::Utf8),
});
doc
}
#[test]
fn test_ezfile_create() {
let ezfile = create_ezfile();
assert_ezfile(&ezfile);
}
#[test]
fn test_ezfile_to_string() {
let doc = create_ezfile();
let mut c = Cursor::new(Vec::new());
let result = doc.write(&mut c);
assert!(result.is_ok());
let data = c.into_inner();
let data_str = std::str::from_utf8(data.as_slice()).unwrap();
assert_eq!(EZFILE_STR, data_str);
}
#[test]
fn test_escapes() {
let mut expected = r#"<root attr="<&>"🍔"''">&&&<<<'"🍔"'>>>&&&</root>"#.to_owned();
expected.push('\n');
let mut root = Element::default();
root.set_name("root");
root.add_attribute("attr", "<&>\"🍔\"\'\'");
root.add_text("&&&<<<'\"🍔\"'>>>&&&");
let doc = Document::from_root(root);
let mut c = Cursor::new(Vec::new());
let result = doc.write(&mut c);
assert!(result.is_ok());
let data = c.into_inner();
let data_str = std::str::from_utf8(data.as_slice()).unwrap();
assert_eq!(expected, data_str);
}
}