alpine-markup 0.1.0

Core markup types for Alpine, a simple HTML template engine
Documentation
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt::{self, Display, Formatter};

pub enum Node {
    Text(String),
    Element(Element),
}

impl Display for Node {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::Text(text) => write!(f, "{text}"),
            Self::Element(element) => write!(f, "{element}"),
        }
    }
}

impl From<String> for Node {
    fn from(text: String) -> Self {
        Self::Text(text)
    }
}

impl From<&str> for Node {
    fn from(text: &str) -> Self {
        text.to_owned().into()
    }
}

impl From<Element> for Node {
    fn from(element: Element) -> Self {
        Self::Element(element)
    }
}

pub struct Element {
    name: &'static str,
    classes: Vec<Cow<'static, str>>,
    attributes: Option<BTreeMap<Cow<'static, str>, String>>,
    children: Vec<Node>,
}

impl Display for Element {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "<{}", self.name)?;
        if !self.classes.is_empty() {
            write!(f, " class=\"{}\"", self.classes.join(" "))?;
        }
        if let Some(attributes) = &self.attributes {
            for (key, value) in attributes {
                write!(f, " {key}=\"{value}\"")?;
            }
        }
        if self.children.is_empty() {
            write!(f, " />")
        } else {
            write!(f, ">")?;
            for child in &self.children {
                write!(f, "{child}",)?;
            }
            write!(f, "</{}>", self.name)
        }
    }
}

macro_rules! attributes {
    ($($name:ident)+) => {$(
        #[must_use]
        pub fn $name(self, value: impl ToString) -> Self {
            self.attr(stringify!($name), value.to_string())
        }
    )+};
}

impl Element {
    #[must_use]
    pub const fn new(name: &'static str) -> Self {
        Self {
            name,
            attributes: None,
            classes: Vec::new(),
            children: Vec::new(),
        }
    }

    #[must_use]
    pub fn attr(mut self, key: impl Into<Cow<'static, str>>, value: impl ToString) -> Self {
        let key = key.into();
        let value = value.to_string();
        if key == "class" {
            for class in value.split(' ') {
                self.classes.push(class.to_string().into());
            }
        } else {
            self.attributes
                .get_or_insert_with(BTreeMap::new)
                .insert(key, value.to_string());
        }
        self
    }

    #[must_use]
    pub fn data(self, key: &'static str, value: impl ToString) -> Self {
        self.attr(format!("data-{key}"), value)
    }

    #[must_use]
    pub fn classes(self, classes: impl IntoIterator<Item = impl ToString>) -> Self {
        classes.into_iter().fold(self, Self::class)
    }

    #[must_use]
    pub fn classes_if(
        self,
        condition: bool,
        if_true: impl IntoIterator<Item = impl ToString>,
    ) -> Self {
        if condition {
            self.classes(if_true)
        } else {
            self
        }
    }

    #[must_use]
    pub fn classes_if_else(
        self,
        condition: bool,
        if_true: impl IntoIterator<Item = impl ToString>,
        if_false: impl IntoIterator<Item = impl ToString>,
    ) -> Self {
        if condition {
            self.classes(if_true)
        } else {
            self.classes(if_false)
        }
    }

    #[must_use]
    pub fn add_class(self, class: impl ToString) -> Self {
        self.attr("class", class)
    }

    attributes! {
        charset
        class
        height
        href
        id
        src
        width
        name
        value
        content
    }

    #[must_use]
    pub fn push(mut self, child: impl Markup) -> Self {
        self.children.extend(child.iter_nodes().map(Into::into));
        self
    }

    #[must_use]
    pub fn push_map<I, T, F, M>(mut self, items: I, func: F) -> Self
    where
        I: IntoIterator<Item = T>,
        F: Fn(T) -> M,
        M: Markup,
    {
        self.children.extend(
            items
                .into_iter()
                .map(func)
                .flat_map(Markup::iter_nodes)
                .map(Into::into),
        );
        self
    }

    #[must_use]
    pub fn push_if(mut self, condition: bool, child: impl Markup) -> Self {
        if condition {
            self.children.extend(child.iter_nodes().map(Into::into));
        }
        self
    }

    #[must_use]
    pub fn push_if_else(
        mut self,
        condition: bool,
        if_true: impl Markup,
        if_false: impl Markup,
    ) -> Self {
        if condition {
            self.children.extend(if_true.iter_nodes().map(Into::into));
        } else {
            self.children.extend(if_false.iter_nodes().map(Into::into));
        }
        self
    }

    #[must_use]
    pub fn push_if_some<T, F, M>(self, maybe_t: Option<T>, func: F) -> Self
    where
        F: Fn(T) -> M,
        M: Markup,
    {
        if let Some(t) = maybe_t {
            self.push(func(t))
        } else {
            self
        }
    }

    #[must_use]
    pub fn tag(&self) -> &'static str {
        self.name
    }
}

pub trait Markup {
    type Iter: Iterator<Item = Self::Item>;
    type Item: Into<Node>;
    fn iter_nodes(self) -> Self::Iter;
}

impl<T> Markup for T
where
    T: Into<Node>,
{
    type Iter = std::iter::Once<Self::Item>;
    type Item = Self;
    fn iter_nodes(self) -> Self::Iter {
        std::iter::once(self)
    }
}

impl<const N: usize> Markup for [Element; N] {
    type Iter = std::array::IntoIter<Self::Item, N>;
    type Item = Element;
    fn iter_nodes(self) -> Self::Iter {
        self.into_iter()
    }
}

impl Markup for Vec<Element> {
    type Iter = std::vec::IntoIter<Element>;
    type Item = Element;
    fn iter_nodes(self) -> Self::Iter {
        self.into_iter()
    }
}

#[macro_export]
macro_rules! raw_tags {
    ($($upper_name:ident, $lower_name:literal)+) => {$(
        pub const $upper_name: $crate::Element = $crate::Element::new($lower_name);
    )+};
}