htmx 0.1.0

Some server side utilities for htmx
Documentation
#![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 {
    /// # Errors
    /// Errors when the [`Write`] errors.
    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 {
            // https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0
            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 {
            // https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0
            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(())
    }
}