pub mod attribute;
pub mod content;
pub mod prelude;
use crate::prelude::{FmtWriter, IoWriter, WriterExt};
#[derive(Debug)]
pub enum Body<'a> {
Root,
Element {
name: &'a str,
parent: Box<Body<'a>>,
},
}
impl Body<'_> {
pub fn path(&self) -> String {
match self {
Self::Root => String::from("$"),
Self::Element { name, parent } => {
let mut parent_path = parent.path();
parent_path.push_str(" > ");
parent_path.push_str(name);
parent_path
}
}
}
}
#[derive(Debug)]
pub struct Element<'a> {
parent: Body<'a>,
name: &'a str,
}
#[derive(Clone, Debug)]
pub struct Buffer<W, C> {
inner: W,
current: C,
}
impl Default for Buffer<FmtWriter<String>, Body<'static>> {
fn default() -> Self {
Self::from(String::new())
}
}
impl<W: std::fmt::Write> From<W> for Buffer<FmtWriter<W>, Body<'static>> {
fn from(buffer: W) -> Self {
Self {
inner: FmtWriter(buffer),
current: Body::Root,
}
}
}
impl<W: std::io::Write> From<W> for Buffer<IoWriter<W>, Body<'static>> {
fn from(value: W) -> Self {
Self {
inner: IoWriter(value),
current: Body::Root,
}
}
}
impl<W> Buffer<FmtWriter<W>, Body<'_>> {
pub fn into_inner(self) -> W {
self.inner.0
}
}
impl<W> Buffer<IoWriter<W>, Body<'_>> {
pub fn into_inner(self) -> W {
self.inner.0
}
}
impl Buffer<FmtWriter<String>, Body<'_>> {
pub fn inner(&self) -> &str {
self.inner.0.as_str()
}
}
impl<W: WriterExt> Buffer<W, Body<'_>> {
pub fn doctype(mut self) -> Self {
self.inner.write_str("<!DOCTYPE html>").unwrap();
self
}
pub fn try_doctype(mut self) -> Result<Self, W::Error> {
self.inner.write_str("<!DOCTYPE html>")?;
Ok(self)
}
}
impl<'a, W: WriterExt> Buffer<W, Body<'a>> {
pub fn cond<F>(self, condition: bool, children: F) -> Buffer<W, Body<'a>>
where
F: FnOnce(Buffer<W, Body>) -> Buffer<W, Body>,
{
if condition {
children(self)
} else {
self
}
}
pub fn try_cond<F>(self, condition: bool, children: F) -> Result<Buffer<W, Body<'a>>, W::Error>
where
F: FnOnce(Buffer<W, Body>) -> Result<Buffer<W, Body>, W::Error>,
{
if condition {
children(self)
} else {
Ok(self)
}
}
pub fn optional<V, F>(self, value: Option<V>, children: F) -> Buffer<W, Body<'a>>
where
F: FnOnce(Buffer<W, Body>, V) -> Buffer<W, Body>,
{
if let Some(inner) = value {
children(self, inner)
} else {
self
}
}
pub fn try_optional<V, F>(
self,
value: Option<V>,
children: F,
) -> Result<Buffer<W, Body<'a>>, W::Error>
where
F: FnOnce(Buffer<W, Body>, V) -> Result<Buffer<W, Body>, W::Error>,
{
if let Some(inner) = value {
children(self, inner)
} else {
Ok(self)
}
}
pub fn node(mut self, tag: &'a str) -> Buffer<W, Element<'a>> {
self.inner.write_char('<').unwrap();
self.inner.write_str(tag).unwrap();
Buffer {
inner: self.inner,
current: Element {
name: tag,
parent: self.current,
},
}
}
pub fn try_node(mut self, tag: &'a str) -> Result<Buffer<W, Element<'a>>, W::Error> {
self.inner.write_char('<')?;
self.inner.write_str(tag)?;
Ok(Buffer {
inner: self.inner,
current: Element {
name: tag,
parent: self.current,
},
})
}
pub fn raw<V: std::fmt::Display>(mut self, value: V) -> Self {
self.inner.write(value).unwrap();
self
}
pub fn try_raw<V: std::fmt::Display>(mut self, value: V) -> Result<Self, W::Error> {
self.inner.write(value)?;
Ok(self)
}
pub fn text(mut self, input: &str) -> Self {
self.inner.write(content::EscapedContent(input)).unwrap();
self
}
pub fn try_text(mut self, input: &str) -> Result<Self, W::Error> {
self.inner.write(content::EscapedContent(input))?;
Ok(self)
}
}
impl<'a, W: WriterExt> Buffer<W, Element<'a>> {
pub fn attr<T>(mut self, attr: T) -> Self
where
attribute::Attribute<T>: std::fmt::Display,
{
self.inner.write(attribute::Attribute(attr)).unwrap();
self
}
#[inline]
pub fn try_attr<T>(mut self, attr: T) -> Result<Self, W::Error>
where
attribute::Attribute<T>: std::fmt::Display,
{
self.inner.write(attribute::Attribute(attr))?;
Ok(self)
}
#[inline]
pub fn cond_attr<T>(self, condition: bool, attr: T) -> Self
where
attribute::Attribute<T>: std::fmt::Display,
{
if condition {
self.attr(attr)
} else {
self
}
}
#[inline]
pub fn try_cond_attr<T>(self, condition: bool, attr: T) -> Result<Self, W::Error>
where
attribute::Attribute<T>: std::fmt::Display,
{
if condition {
self.try_attr(attr)
} else {
Ok(self)
}
}
pub fn close(mut self) -> Buffer<W, Body<'a>> {
self.inner.write_str(" />").unwrap();
Buffer {
inner: self.inner,
current: self.current.parent,
}
}
pub fn try_close(mut self) -> Result<Buffer<W, Body<'a>>, W::Error> {
self.inner.write_str(" />")?;
Ok(Buffer {
inner: self.inner,
current: self.current.parent,
})
}
pub fn content<F>(mut self, children: F) -> Buffer<W, Body<'a>>
where
F: FnOnce(Buffer<W, Body>) -> Buffer<W, Body>,
{
self.inner.write_char('>').unwrap();
let child_buffer = Buffer {
inner: self.inner,
current: Body::Element {
name: self.current.name,
parent: Box::new(self.current.parent),
},
};
let Buffer { mut inner, current } = children(child_buffer);
match current {
Body::Element { name, parent } => {
inner.write_str("</").unwrap();
inner.write_str(name).unwrap();
inner.write_char('>').unwrap();
Buffer {
inner,
current: *parent,
}
}
Body::Root => Buffer {
inner,
current: Body::Root,
},
}
}
pub fn try_content<F>(mut self, children: F) -> Result<Buffer<W, Body<'a>>, W::Error>
where
F: FnOnce(Buffer<W, Body>) -> Result<Buffer<W, Body>, W::Error>,
{
self.inner.write_char('>')?;
let child_buffer = Buffer {
inner: self.inner,
current: Body::Element {
name: self.current.name,
parent: Box::new(self.current.parent),
},
};
let Buffer { mut inner, current } = children(child_buffer)?;
match current {
Body::Element { name, parent } => {
inner.write_str("</")?;
inner.write_str(name)?;
inner.write_char('>')?;
Ok(Buffer {
inner,
current: *parent,
})
}
Body::Root => Ok(Buffer {
inner,
current: Body::Root,
}),
}
}
}
#[cfg(test)]
mod tests {
use std::io::{Cursor, Write};
use super::*;
#[test]
fn should_return_inner_value() {
let buf = Buffer::default().node("a").content(|buf| buf);
assert_eq!(buf.inner(), "<a></a>");
}
#[test]
fn should_give_node_path() {
let buf = Buffer::default();
assert_eq!(buf.current.path(), "$");
let _buf = buf.node("a").content(|buf| {
assert_eq!(buf.current.path(), "$ > a");
buf
});
}
#[test]
fn should_rollback_after_content() {
let buffer = Buffer::default().node("a").content(|buf| buf);
assert!(
matches!(buffer.current, Body::Root),
"found {:?}",
buffer.current
);
}
#[test]
fn simple_html() {
let html = Buffer::default()
.doctype()
.node("html")
.attr(("lang", "en"))
.content(|buf| {
buf.node("head")
.content(|buf| {
let buf = buf.node("meta").attr(("charset", "utf-8")).close();
buf.node("meta")
.attr(("name", "viewport"))
.attr(("content", "width=device-width, initial-scale=1"))
.close()
})
.node("body")
.close()
})
.into_inner();
assert_eq!(
html,
"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /></head><body /></html>"
);
}
#[test]
fn with_special_characters_in_attributes() {
let html = Buffer::default()
.node("a")
.attr(("title", "Let's add a quote \" like this"))
.attr(("href", "http://example.com?whatever=here"))
.content(|b| b.text("Click me!"))
.into_inner();
assert_eq!(
html,
"<a title=\"Let's add a quote \\\" like this\" href=\"http://example.com?whatever=here\">Click me!</a>"
);
}
#[test]
fn with_special_characters_in_content() {
let html = Buffer::default()
.node("p")
.content(|b| b.text("asd\"weiofew!/<>"))
.into_inner();
assert_eq!(html, "<p>asd"weiofew!/<></p>");
}
#[test]
fn with_optional_attributes() {
let html = Buffer::default()
.node("p")
.attr(Some(("foo", "bar")))
.attr(None::<(&str, &str)>)
.attr(Some("here"))
.attr(None::<&str>)
.close()
.into_inner();
assert_eq!(html, "<p foo=\"bar\" here />");
}
#[test]
fn with_attributes() {
let html = Buffer::default()
.node("p")
.attr(("foo", "bar"))
.attr(("bool", true))
.attr(("u8", 42u8))
.attr(("i8", -1i8))
.close()
.into_inner();
assert_eq!(html, "<p foo=\"bar\" bool=\"true\" u8=\"42\" i8=\"-1\" />");
}
#[test]
fn with_conditional_attributes() {
let html = Buffer::default()
.node("p")
.cond_attr(true, ("foo", "bar"))
.cond_attr(false, ("foo", "baz"))
.cond_attr(true, "here")
.cond_attr(false, "not-here")
.close()
.into_inner();
assert_eq!(html, "<p foo=\"bar\" here />");
}
#[test]
fn with_conditional_content() {
let notification = false;
let connected = true;
let html = Buffer::default()
.node("div")
.content(|buf| {
buf.cond(notification, |buf| {
buf.node("p")
.content(|buf| buf.text("You have a notification"))
})
.cond(connected, |buf| buf.text("Welcome!"))
})
.into_inner();
assert_eq!(html, "<div>Welcome!</div>");
}
#[test]
fn with_optional_content() {
let error = Some("This is an error");
let html = Buffer::default()
.node("div")
.content(|buf| buf.optional(error, |buf, msg| buf.text(msg)))
.into_inner();
assert_eq!(html, "<div>This is an error</div>");
}
#[test]
fn should_write_to_io_buffer() {
let buf = Buffer::from(Cursor::new(Vec::new()));
let buf = buf.node("div").content(|buf| buf.text("Hello World!"));
let mut writer = buf.into_inner();
writer.flush().unwrap();
let inner = writer.into_inner();
assert_eq!(&inner, "<div>Hello World!</div>".as_bytes());
}
}