use crate::html::Html;
use crate::value::Value;
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ElemType {
Html,
HtmlVoid,
Xml,
}
#[derive(Default)]
pub struct Tree {
doc: String,
stack: Vec<&'static str>,
tp: Option<ElemType>,
empty: bool,
}
#[deprecated]
pub type Page = Tree;
pub trait Element<'t> {
const TAG: &'static str;
const TP: ElemType;
fn new(tree: &'t mut Tree) -> Self;
}
impl fmt::Display for Tree {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut void = self.tp == Some(ElemType::HtmlVoid);
let mut self_closing = self.empty && self.tp == Some(ElemType::Xml);
if self_closing && let Some(tree) = self.doc.strip_suffix('>') {
write!(f, "{tree}")?;
} else {
write!(f, "{}", self.doc)?;
self_closing = false;
}
for tag in self.stack.iter().rev() {
if self_closing {
write!(f, " />")?;
} else if !void {
write!(f, "</{tag}>")?;
}
self_closing = false;
void = false;
}
Ok(())
}
}
impl From<Tree> for String {
fn from(mut tree: Tree) -> Self {
tree.close_to(1);
tree.doc
}
}
impl Tree {
pub fn new() -> Self {
Self::default()
}
#[deprecated]
pub fn with_doctype(self) -> Self {
self
}
pub fn html(&mut self) -> Html<'_> {
self.stack.clear();
self.doc.clear();
self.raw("<!DOCTYPE html>");
self.elem("html", ElemType::Html);
Html::new(self)
}
pub fn root<'t, E>(&'t mut self) -> E
where
E: Element<'t>,
{
self.elem(E::TAG, E::TP);
E::new(self)
}
#[deprecated]
pub fn frag<'t, E>(&'t mut self) -> E
where
E: Element<'t>,
{
self.root()
}
pub(crate) fn elem(&mut self, tag: &'static str, tp: ElemType) -> usize {
self.doc.push('<');
self.doc.push_str(tag);
self.doc.push('>');
self.empty = true;
self.tp = Some(tp);
self.stack.push(tag);
self.stack.len()
}
pub(crate) fn attr<'a, V>(&mut self, attr: &str, val: V)
where
V: Into<Value<'a>>,
{
match self.doc.pop() {
Some(gt) => assert_eq!(gt, '>'),
None => panic!("cannot add {attr} attribute after child content"),
}
self.doc.push(' ');
self.doc.push_str(attr);
self.doc.push_str("=\"");
val.into().encode_attr(&mut self.doc);
self.doc.push_str("\">");
}
pub(crate) fn attr_bool(&mut self, attr: &'static str) {
match self.doc.pop() {
Some(gt) => assert_eq!(gt, '>'),
None => panic!("cannot add {attr} attribute after child content"),
}
self.doc.push(' ');
self.doc.push_str(attr);
self.doc.push('>');
}
pub fn comment<'a, V>(&mut self, com: V) -> &mut Self
where
V: Into<Value<'a>>,
{
self.doc.push_str("<!--");
com.into().encode_comment(&mut self.doc);
self.doc.push_str("-->");
self.empty = false;
self
}
pub(crate) fn cdata<'a, V>(&mut self, text: V) -> &mut Self
where
V: Into<Value<'a>>,
{
text.into().encode_cdata(&mut self.doc);
self.empty = false;
self
}
pub(crate) fn cdata_len<'a, V>(&mut self, text: V, len: usize) -> &mut Self
where
V: Into<Value<'a>>,
{
text.into().encode_cdata_len(&mut self.doc, len);
self.empty = false;
self
}
pub fn raw(&mut self, trusted: impl AsRef<str>) -> &mut Self {
self.doc.push_str(trusted.as_ref());
self.empty = false;
self
}
pub(crate) fn close_to(&mut self, depth: usize) -> &mut Self {
while self.stack.len() >= depth {
self.close();
}
self
}
pub fn close(&mut self) -> &mut Self {
let tp = self.tp.take();
if let Some(tag) = self.stack.pop() {
let void = tp == Some(ElemType::HtmlVoid);
let self_closing = self.empty && tp == Some(ElemType::Xml);
if self_closing && self.doc.ends_with('>') {
self.doc.pop();
self.doc.push_str(" />");
} else if !void {
self.doc.push_str("</");
self.doc.push_str(tag);
self.doc.push('>');
}
}
self.empty = false;
self
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::html::*;
#[test]
fn div() {
let mut tree = Tree::new();
tree.root::<Div>();
assert_eq!(tree.to_string(), "<div></div>");
}
#[test]
fn boolean() {
let mut tree = Tree::new();
tree.root::<Div>().id("test").spellcheck(true);
assert_eq!(
tree.to_string(),
"<div id=\"test\" spellcheck=\"true\"></div>"
);
}
#[test]
fn paragraph() {
let mut tree = Tree::new();
tree.root::<P>().cdata("This is a paragraph");
assert_eq!(tree.to_string(), "<p>This is a paragraph</p>");
}
#[test]
fn escape_attr() {
let mut tree = Tree::new();
tree.root::<P>().id("quote\"ampersand&");
assert_eq!(
tree.to_string(),
"<p id=\"quote"ampersand&\"></p>"
);
}
#[test]
fn escape_cdata() {
let mut tree = Tree::new();
tree.root::<Em>().cdata("You <&> I");
assert_eq!(tree.to_string(), "<em>You <&> I</em>");
}
#[test]
fn raw_burger() {
let mut tree = Tree::new();
tree.root::<Span>().cdata("Raw").raw(" <em>Burger</em>!");
assert_eq!(tree.to_string(), "<span>Raw <em>Burger</em>!</span>");
}
#[test]
fn void() {
let mut tree = Tree::new();
tree.root::<Div>().input().r#type("text");
assert_eq!(tree.to_string(), "<div><input type=\"text\"></div>");
}
#[test]
fn html() {
let mut tree = Tree::new();
let mut ol = tree.root::<Ol>();
ol.li().class("cat").cdata("nori").close();
ol.li().class("cat").cdata("chashu");
assert_eq!(
tree.to_string(),
"<ol><li class=\"cat\">nori</li><li class=\"cat\">chashu</li></ol>"
);
}
#[test]
fn build_html() {
let mut tree = Tree::new();
let mut div = tree.root::<Div>();
div.p().cdata("Paragraph Text").close();
div.pre().cdata("Preformatted Text");
assert_eq!(
tree.to_string(),
"<div><p>Paragraph Text</p><pre>Preformatted Text</pre></div>"
);
}
#[test]
fn html_builder() {
let mut tree = Tree::new();
let mut html = tree.html();
let mut head = html.lang("en").head();
head.title_el().cdata("Title!");
head.close();
html.body().h1().cdata("Header!");
assert_eq!(
tree.to_string(),
"<!DOCTYPE html><html lang=\"en\"><head><title>Title!</title></head><body><h1>Header!</h1></body></html>"
);
}
#[test]
fn string_from() {
let mut tree = Tree::new();
let mut html = tree.html();
html.head().title_el().cdata("Head").close().close();
html.body().cdata("Body");
assert_eq!(
String::from(tree),
"<!DOCTYPE html><html><head><title>Head</title></head><body>Body</body></html>"
);
}
#[test]
fn comment() {
let mut tree = Tree::new();
tree.root::<I>().comment("comment");
assert_eq!(tree.to_string(), "<i><!--comment--></i>");
}
#[test]
fn escape_comment() {
let mut tree = Tree::new();
tree.comment("<-->");
assert_eq!(tree.to_string(), "<!--<‐‐>-->");
}
#[test]
fn xml() {
let mut tree = Tree::new();
tree.root::<Link>().rel("stylesheet").close();
assert_eq!(tree.to_string(), "<link rel=\"stylesheet\" />");
}
#[test]
fn close() {
let mut tree = Tree::new();
tree.root::<Span>().id("gle").close();
assert_eq!(tree.to_string(), "<span id=\"gle\"></span>");
}
#[test]
fn image() {
let mut tree = Tree::new();
tree.root::<Img>().width(100).height(50).close();
assert_eq!(tree.to_string(), "<img width=\"100\" height=\"50\">");
}
#[test]
fn data() {
let mut tree = Tree::new();
tree.root::<P>().data_("macro", "macrodata");
assert_eq!(tree.to_string(), "<p data-macro=\"macrodata\"></p>");
}
#[test]
#[should_panic]
fn attributes() {
let mut tree = Tree::new();
tree.root::<P>().cdata("character data").id("123");
}
#[test]
fn double_root() {
let mut tree = Tree::new();
tree.root::<Div>().close();
tree.root::<Div>().close();
assert_eq!(String::from(tree), "<div></div><div></div>");
}
}