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);
)+};
}