#![allow(non_camel_case_types)]
use std::collections::HashMap;
use std::fmt::{self, Write};
use std::iter::{self, Once};
use html_escape::encode_double_quoted_attribute;
pub trait HtmlElement {
fn write(&self, out: &mut dyn Write) -> fmt::Result;
}
pub trait Html: Sized {
type Element: HtmlElement;
type Elements: IntoIterator<Item = Self::Element>;
fn into_elements(self) -> Self::Elements;
fn into_string(self) -> String {
let mut out = String::new();
for element in self.into_elements() {
element
.write(&mut out)
.expect("writing to String should not fail");
}
out
}
}
impl<T: Html> Html for Vec<T> {
type Element = T::Element;
type Elements = Vec<Self::Element>;
fn into_elements(self) -> Self::Elements {
self.into_iter().flat_map(Html::into_elements).collect()
}
}
impl<T: HtmlElement> Html for T {
type Element = Self;
type Elements = Once<Self>;
fn into_elements(self) -> Self::Elements {
iter::once(self)
}
}
#[must_use]
#[derive(Default)]
pub struct div {
attributes: HashMap<&'static str, Option<String>>,
children: Vec<Box<dyn HtmlElement>>,
}
impl div {
pub fn builder() -> Self {
Self::default()
}
pub fn build(self) -> Self {
self
}
pub fn push(mut self, child: impl Html + 'static) -> Self {
self.children.extend(
child
.into_elements()
.into_iter()
.map(|elem| Box::new(elem) as Box<dyn HtmlElement>),
);
self
}
}
impl HtmlElement for div {
fn write(&self, out: &mut dyn Write) -> fmt::Result {
write!(out, "<div")?;
for (key, value) in &self.attributes {
debug_assert!(!key.chars().any(|c| c.is_whitespace()
|| c.is_control()
|| matches!(c, '\0' | '"' | '\'' | '>' | '/' | '=')));
write!(out, " {key}")?;
if let Some(value) = value {
write!(out, "=\"{}\"", encode_double_quoted_attribute(value))?;
}
}
write!(out, ">")?;
for child in &self.children {
child.write(out)?;
}
write!(out, "</div>")?;
Ok(())
}
}
#[must_use]
#[derive(Default)]
pub struct a {
attributes: HashMap<&'static str, Option<String>>,
children: Vec<Box<dyn HtmlElement>>,
}
impl a {
pub fn href(mut self, value: impl Into<String>) -> Self {
self.attributes.insert("href", Some(value.into()));
self
}
}
impl a {
pub fn builder() -> Self {
Self::default()
}
pub fn build(self) -> Self {
self
}
pub fn push(mut self, child: impl HtmlElement + 'static) -> Self {
self.children.push(Box::new(child));
self
}
}
impl HtmlElement for a {
fn write(&self, out: &mut dyn Write) -> fmt::Result {
write!(out, "<a")?;
for (key, value) in &self.attributes {
debug_assert!(!key.chars().any(|c| c.is_whitespace()
|| c.is_control()
|| matches!(c, '\0' | '"' | '\'' | '>' | '/' | '=')));
write!(out, " {key}")?;
if let Some(value) = value {
write!(out, "=\"{}\"", encode_double_quoted_attribute(value))?;
}
}
write!(out, ">")?;
for child in &self.children {
child.write(out)?;
}
write!(out, "</a>")?;
Ok(())
}
}