inline-html 0.1.0

Write html! {} in Rust
Documentation
use std::fmt::{self, Display, Formatter};

#[derive(Debug, Clone)]
pub struct Tag {
    pub name: &'static str,
    pub args: Vec<(&'static str, String)>,
    pub children: Vec<Element>,
    pub self_closing: bool,
}

#[derive(Debug, Clone)]
pub enum Element {
    Tag(Tag),
    String(String),
}

#[derive(Clone, Copy)]
pub struct Indent(u32);

impl Tag {
    pub fn new(name: &'static str) -> Self {
        Self {
            name,
            args: Vec::new(),
            children: Vec::new(),
            self_closing: false,
        }
    }
}

impl Element {
    pub fn render(&self, f: &mut Formatter<'_>, ind: Indent) -> fmt::Result {
        match self {
            Self::Tag(Tag {
                name,
                args,
                children,
                self_closing,
            }) => {
                write!(f, "{ind}<{name}")?;
                for (name, value) in args {
                    write!(f, " {name}={value:?}")?;
                }
                write!(f, ">")?;
                match &children[..] {
                    [] if *self_closing => writeln!(f),
                    [] => writeln!(f, "</{name}>"),
                    [Element::String(s)] => writeln!(f, "{s}</{name}>"),
                    _ => {
                        writeln!(f)?;

                        for child in children {
                            child.render(f, ind.next())?;
                        }

                        writeln!(f, "{ind}</{name}>")?;
                        Ok(())
                    }
                }
            }
            Self::String(s) if s.is_empty() => Ok(()),
            Self::String(s) => writeln!(f, "{ind}{s}"),
        }
    }
}

impl From<&str> for Element {
    fn from(value: &str) -> Self {
        Self::String(value.into())
    }
}

impl From<String> for Element {
    fn from(value: String) -> Self {
        Self::String(value)
    }
}

impl Display for Element {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        self.render(f, Indent(0))
    }
}

impl Display for Indent {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        for _ in 0..self.0 {
            write!(f, "\t")?;
        }
        Ok(())
    }
}

impl Indent {
    fn next(self) -> Self {
        Self(self.0 + 1)
    }
}

#[macro_export]
macro_rules! html {
    ($name:ident $([ $($an:tt = $av:expr),* $(,)? ])? { $($tk:tt)* }) => {
	{
	    #[allow(unused_mut)]
	    let mut tag = $crate::Tag {
		name: stringify!($name),
		args: vec! [
		    $(
			$(
			    (html!(@ $an), $av.to_string())
			),*
		    )?
		],
		children: Vec::new(),
		self_closing: false,
	    };

	    html! {
		! tag $($tk)*
	    }

	    $crate::Element::Tag(tag)
	}
    };
    (! $parent:ident $name:ident $([$($args:tt)*])? { $($tk:tt)* } $($rest:tt)*) => {
	$parent.children.push(html! { $name $([$($args)*])? { $($tk)* } });

	html! {
	    ! $parent $($rest)*
	}
    };
    (! $parent:ident $name:ident $([$($an:ident = $av:expr),*])? ; $($rest:tt)*) => {
	$parent.children.push(
	    $crate::Tag($crate::Tag {
		name: stringify!($name),
		args: vec! [
		    $(
			$(
			    (html! { @$an}, $av.to_string())
			),*
		    )?
		],
		children: Vec::new(),
		self_closing: true,
	    })
	);

	html! {
	    ! $parent $($rest)*
	}
    };
    (! $parent:ident $name:ident = $val:expr ; $($rest:tt)*) => {
	$parent.children.push($crate::Element::Tag($crate::Tag {
	    name: stringify!($name),
	    args: Vec::new(),
	    children: vec![
            $crate::Element::String($val.into())
	    ],
	    self_closing: false,
	}));

	html! {
	    ! $parent $($rest)*
	}
    };
    (! $parent:ident $s:literal ; $($rest:tt)*) => {
	$parent.children.push($crate::Element::String(format!($s)));

	html! {
	    ! $parent $($rest)*
	}
    };
    (! $parent:ident { $e:expr } $($rest:tt)*) => {
	$parent.children.push($e.into());

	html! {
	    ! $parent $($rest)*
	}
    };
    (! $parent:ident ? { $e:expr } $($rest:tt)*) => {
	if let Some(e) = $e {
	    $parent.children.push(e.into());
	}

	html! {
	    ! $parent $($rest)*
	}
    };
    (! $parent:ident [ $e:expr ] $($rest:tt)*) => {
	for e in $e {
	    $parent.children.push(e.into());
	}

	html! {
	    ! $parent $($rest)*
	}
    };
    (! $parent:ident) => {};
    (@ $i:ident) => { stringify!($i) };
    (@ $s:literal) => { $s };
}