use alloc::string::String;
use core::fmt::Write;
use crate::tree::{Document, NodeId, NodeKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SerialiseOptions {
pub indent: Option<Indent>,
pub empty_elements: EmptyElement,
pub newline: Newline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Indent {
Spaces(u8),
Tab,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EmptyElement {
#[default]
SelfClosing,
SelfClosingSpaced,
Expanded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Newline {
#[default]
Lf,
CrLf,
}
impl Newline {
const fn as_str(self) -> &'static str {
match self {
Self::Lf => "\n",
Self::CrLf => "\r\n",
}
}
}
impl Document {
#[must_use]
pub fn to_xml(&self) -> String {
let mut out = String::new();
let _ = self.write_xml(&mut out);
out
}
pub fn write_xml<W: Write>(&self, out: &mut W) -> core::fmt::Result {
if self.needs_xml_11() {
out.write_str("<?xml version=\"1.1\"?>")?;
}
for &child in self.children(self.root()) {
self.write_node(child, out)?;
}
Ok(())
}
fn needs_xml_11(&self) -> bool {
self.descendants().any(|id| match self.kind(id) {
Some(NodeKind::Text(t)) => t.chars().any(needs_reference),
Some(NodeKind::Attr(a)) => a.value.chars().any(needs_reference),
Some(NodeKind::Namespace { prefix, uri }) => {
!prefix.is_empty() && uri.is_empty()
}
_ => false,
})
}
#[must_use]
pub fn to_xml_with(&self, options: SerialiseOptions) -> String {
let mut out = String::new();
let _ = self.write_xml_with(&mut out, options);
out
}
pub fn write_xml_with<W: Write>(
&self,
out: &mut W,
options: SerialiseOptions,
) -> core::fmt::Result {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("oxml.serialise").entered();
if self.needs_xml_11() {
out.write_str("<?xml version=\"1.1\"?>")?;
if options.indent.is_some() {
out.write_str(options.newline.as_str())?;
}
}
let top = self.children(self.root());
for (i, &child) in top.iter().enumerate() {
self.write_node_with(child, out, options, 0)?;
if options.indent.is_some() && i + 1 < top.len() {
out.write_str(options.newline.as_str())?;
}
}
Ok(())
}
fn write_indent<W: Write>(
out: &mut W,
options: SerialiseOptions,
depth: usize,
) -> core::fmt::Result {
out.write_str(options.newline.as_str())?;
match options.indent {
Some(Indent::Spaces(n)) => {
for _ in 0..(depth * n as usize) {
out.write_char(' ')?;
}
}
Some(Indent::Tab) => {
for _ in 0..depth {
out.write_char('\t')?;
}
}
None => {}
}
Ok(())
}
fn write_node_with<W: Write>(
&self,
id: NodeId,
out: &mut W,
options: SerialiseOptions,
depth: usize,
) -> core::fmt::Result {
match self.kind(id) {
Some(NodeKind::Element { .. }) => {
self.write_element_with(id, out, options, depth)
}
_ => self.write_node(id, out),
}
}
fn write_element_with<W: Write>(
&self,
id: NodeId,
out: &mut W,
options: SerialiseOptions,
depth: usize,
) -> core::fmt::Result {
let name = self.qualified_name(id);
write!(out, "<{name}")?;
self.write_tag_attributes(id, out)?;
let children = self.children(id);
if children.is_empty() {
return match options.empty_elements {
EmptyElement::SelfClosing => out.write_str("/>"),
EmptyElement::SelfClosingSpaced => out.write_str(" />"),
EmptyElement::Expanded => write!(out, "></{name}>"),
};
}
out.write_char('>')?;
let element_only = options.indent.is_some()
&& children.iter().all(|&c| {
matches!(self.kind(c), Some(NodeKind::Element { .. }))
});
if element_only {
for &child in children {
Self::write_indent(out, options, depth + 1)?;
self.write_node_with(child, out, options, depth + 1)?;
}
Self::write_indent(out, options, depth)?;
} else {
let compact = SerialiseOptions {
indent: None,
..options
};
for &child in children {
self.write_node_with(child, out, compact, depth)?;
}
}
write!(out, "</{name}>")
}
fn write_tag_attributes<W: Write>(
&self,
id: NodeId,
out: &mut W,
) -> core::fmt::Result {
for &ns in self.namespace_nodes(id) {
if let Some(NodeKind::Namespace { prefix, uri }) = self.kind(ns) {
if prefix == "xml" {
continue;
}
if prefix.is_empty() {
write!(out, " xmlns=\"")?;
} else {
write!(out, " xmlns:{prefix}=\"")?;
}
write_attribute_value(uri, out)?;
out.write_char('"')?;
}
}
for &attr in self.attribute_nodes(id) {
if let Some(NodeKind::Attr(a)) = self.kind(attr) {
let attr_name = self.qualified_attribute_name(attr);
write!(out, " {attr_name}=\"")?;
write_attribute_value(a.value, out)?;
out.write_char('"')?;
}
}
Ok(())
}
fn write_node<W: Write>(
&self,
id: NodeId,
out: &mut W,
) -> core::fmt::Result {
match self.kind(id) {
Some(NodeKind::Element { .. }) => self.write_element(id, out),
Some(NodeKind::Text(t)) => write_text(t, out),
Some(NodeKind::Comment(c)) => write!(out, "<!--{c}-->"),
Some(NodeKind::ProcessingInstruction { target, data }) => {
if data.is_empty() {
write!(out, "<?{target}?>")
} else {
write!(out, "<?{target} {data}?>")
}
}
_ => Ok(()),
}
}
fn write_element<W: Write>(
&self,
id: NodeId,
out: &mut W,
) -> core::fmt::Result {
let name = self.qualified_name(id);
write!(out, "<{name}")?;
self.write_tag_attributes(id, out)?;
let children = self.children(id);
if children.is_empty() {
return out.write_str("/>");
}
out.write_char('>')?;
for &child in children {
self.write_node(child, out)?;
}
write!(out, "</{name}>")
}
fn qualified_name(&self, id: NodeId) -> String {
let mut s = String::new();
if let Some(NodeKind::Element { name, .. }) = self.kind(id) {
if let Some(prefix) = self.prefix(name) {
if !prefix.is_empty() {
s.push_str(prefix);
s.push(':');
}
}
if let Some(expanded) = self.name(name) {
s.push_str(&expanded.local);
}
}
s
}
fn qualified_attribute_name(&self, id: NodeId) -> String {
let mut s = String::new();
if let Some(NodeKind::Attr(a)) = self.kind(id) {
if let Some(prefix) = self.prefix(a.name) {
if !prefix.is_empty() {
s.push_str(prefix);
s.push(':');
}
}
if let Some(expanded) = self.name(a.name) {
s.push_str(&expanded.local);
}
}
s
}
}
fn write_text<W: Write>(text: &str, out: &mut W) -> core::fmt::Result {
for c in text.chars() {
match c {
'&' => out.write_str("&")?,
'<' => out.write_str("<")?,
'>' => out.write_str(">")?,
'\r' => out.write_str(" ")?,
c if needs_reference(c) => write_reference(c, out)?,
other => out.write_char(other)?,
}
}
Ok(())
}
const fn needs_reference(c: char) -> bool {
matches!(c, '\u{1}'..='\u{8}' | '\u{b}' | '\u{c}' | '\u{e}'..='\u{1f}'
| '\u{7f}'..='\u{9f}')
}
fn write_reference<W: Write>(c: char, out: &mut W) -> core::fmt::Result {
write!(out, "&#{};", c as u32)
}
fn write_attribute_value<W: Write>(
value: &str,
out: &mut W,
) -> core::fmt::Result {
for c in value.chars() {
match c {
'&' => out.write_str("&")?,
'<' => out.write_str("<")?,
'>' => out.write_str(">")?,
'"' => out.write_str(""")?,
'\t' => out.write_str("	")?,
'\n' => out.write_str(" ")?,
'\r' => out.write_str(" ")?,
c if needs_reference(c) => write_reference(c, out)?,
other => out.write_char(other)?,
}
}
Ok(())
}