#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderOptions {
pub pretty: bool,
pub indent: String,
pub xhtml_self_closing: bool,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
pretty: false,
indent: " ".to_string(),
xhtml_self_closing: false,
}
}
}
impl RenderOptions {
#[must_use]
pub fn compact() -> Self {
Self::default()
}
#[must_use]
pub fn pretty() -> Self {
Self {
pretty: true,
..Self::default()
}
}
#[must_use]
pub fn with_indent(mut self, indent: impl Into<String>) -> Self {
self.indent = indent.into();
self
}
#[must_use]
pub fn with_xhtml_self_closing(mut self, yes: bool) -> Self {
self.xhtml_self_closing = yes;
self
}
pub(crate) fn write_indent(&self, out: &mut String, depth: usize) {
if depth == 0 || self.indent.is_empty() {
return;
}
out.reserve(self.indent.len() * depth);
for _ in 0..depth {
out.push_str(&self.indent);
}
}
}
pub trait Render {
fn write_into(&self, out: &mut String, options: &RenderOptions, depth: usize);
fn render(&self) -> String {
self.render_with(&RenderOptions::compact())
}
fn render_pretty(&self) -> String {
self.render_with(&RenderOptions::pretty())
}
fn render_with(&self, options: &RenderOptions) -> String {
let mut out = String::with_capacity(1024);
self.write_into(&mut out, options, 0);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::elements::{div, img, p};
#[test]
fn compact_is_the_default() {
let options = RenderOptions::default();
assert!(!options.pretty);
assert_eq!(options.indent, " ");
assert!(!options.xhtml_self_closing);
}
#[test]
fn the_indent_string_is_configurable() {
let options = RenderOptions::pretty().with_indent(" ");
let mut out = String::new();
options.write_indent(&mut out, 2);
assert_eq!(out, " ");
}
#[test]
fn options_have_value_semantics() {
let base = RenderOptions::compact();
let derived = base.clone().with_xhtml_self_closing(true);
assert!(!base.xhtml_self_closing);
assert!(derived.xhtml_self_closing);
}
#[test]
fn pretty_indents_children_by_two_spaces() {
let tree = div().child(p().text("Hi"));
assert_eq!(tree.render_pretty(), "<div>\n <p>Hi</p>\n</div>");
}
#[test]
fn xhtml_self_closing_is_per_call() {
let tag = img().attr("src", "a.png");
assert_eq!(tag.render(), r#"<img src="a.png">"#);
assert_eq!(
tag.render_with(&RenderOptions::compact().with_xhtml_self_closing(true)),
r#"<img src="a.png" />"#
);
assert_eq!(tag.render(), r#"<img src="a.png">"#);
}
#[test]
fn write_into_appends_rather_than_replacing() {
let mut buffer = String::from("<!-- header -->");
div()
.text("x")
.write_into(&mut buffer, &RenderOptions::compact(), 0);
assert_eq!(buffer, "<!-- header --><div>x</div>");
}
#[test]
fn concurrent_renders_do_not_share_state() {
let results: Vec<(usize, String)> = std::thread::scope(|scope| {
let handles: Vec<_> = (0..8)
.map(|index| {
scope.spawn(move || {
let tag = img().attr("src", format!("{index}.png"));
let options =
RenderOptions::compact().with_xhtml_self_closing(index % 2 == 0);
(index, tag.render_with(&options))
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("thread"))
.collect()
});
for (index, rendered) in results {
assert_eq!(
rendered.ends_with(" />"),
index % 2 == 0,
"{index} rendered {rendered:?}"
);
}
}
}