yo-html 0.1.1

JSX-like macro similar to what you can find in React or Yew but without framework nor trait.
Documentation
use implicit_clone::unsync::*;
use indexmap::IndexMap;
use std::fmt;
use std::rc::Rc;

pub enum VNode {
    Tagged {
        tag: &'static str,
        children: Rc<[VNode]>,
        attrs: IMap<IString, IString>,
        classes: IArray<IString>,
    },
    Text(IString),
    Fragment(Rc<[VNode]>),
    Component(Rc<dyn Component>),
}

impl From<String> for VNode {
    fn from(s: String) -> VNode {
        VNode::Text(s.into())
    }
}

impl From<&'static str> for VNode {
    fn from(s: &'static str) -> VNode {
        VNode::Text(s.into())
    }
}

impl From<std::fmt::Arguments<'_>> for VNode {
    fn from(args: std::fmt::Arguments) -> VNode {
        VNode::Text(args.into())
    }
}

impl<const N: usize> From<[VNode; N]> for VNode {
    fn from(elements: [VNode; N]) -> VNode {
        VNode::Fragment(elements.into())
    }
}

impl VNode {
    pub fn builder(tag: &'static str) -> VNodeBuilder {
        VNodeBuilder::new(tag)
    }
}

pub struct VNodeBuilder {
    tag: &'static str,
    class: Vec<IString>,
    dyn_attrs: IndexMap<IString, IString>,
    children: Vec<VNode>,
}

impl VNodeBuilder {
    pub fn new(tag: &'static str) -> Self {
        Self {
            tag,
            class: Default::default(),
            dyn_attrs: Default::default(),
            children: Default::default(),
        }
    }

    pub fn set_attr_class(&mut self, class: impl Into<IString>) -> &mut Self {
        self.add_attr_class(class, 1)
    }

    pub fn add_attr_class(&mut self, class: impl Into<IString>, additional: usize) -> &mut Self {
        //dbg!(additional);
        self.class.reserve_exact(additional);
        self.class.push(class.into());
        self
    }

    pub fn set_attr_style(&mut self, style: impl Into<IString>) -> &mut Self {
        self.add_attr("style", style, 1)
    }

    pub fn add_child(&mut self, element: impl Into<VNode>, additional: usize) -> &mut Self {
        //dbg!(additional);
        self.children.reserve_exact(additional);
        self.children.push(element.into());
        self
    }

    pub fn add_attr(
        &mut self,
        name: impl Into<IString>,
        value: impl Into<IString>,
        additional: usize,
    ) -> &mut Self {
        //dbg!(additional);
        // NOTE: it's actually better to use reserve() here instead of reserve_exact() because
        //       there can be multiple different keys that will have their own number of items.
        self.dyn_attrs.reserve(additional);
        self.dyn_attrs.insert(name.into(), value.into());
        self
    }

    pub fn finish(&mut self) -> VNode {
        let children = Rc::from(std::mem::take(&mut self.children));
        let dyn_attrs = Rc::from(std::mem::take(&mut self.dyn_attrs));
        let class = Rc::from(std::mem::take(&mut self.class));
        if self.tag == "" {
            VNode::Fragment(children)
        } else {
            VNode::Tagged {
                tag: self.tag,
                children,
                attrs: IMap::from(dyn_attrs),
                classes: IArray::from(class),
            }
        }
    }
}

impl fmt::Display for VNode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Text(s) => {
                write!(f, "{s}")?;
            }
            Self::Tagged {
                tag,
                children,
                attrs,
                classes,
            } => {
                write!(f, "<{tag}")?;
                if !classes.is_empty() {
                    write!(f, " class=\"")?;
                    let mut it = classes.iter();
                    escape_attr_quoted_fmt(f, &it.next().unwrap())?;
                    for class in it {
                        write!(f, " ")?;
                        escape_attr_quoted_fmt(f, &class)?;
                    }
                    write!(f, "\"",)?;
                }
                for (attr_name, attr_value) in attrs.iter() {
                    write!(f, " {attr_name}=\"")?;
                    escape_attr_quoted_fmt(f, &attr_value)?;
                    write!(f, "\"")?;
                }
                write!(f, ">")?;
                for child in children.iter() {
                    write!(f, "{child}")?;
                }
                write!(f, "</{tag}>")?;
            }
            Self::Fragment(children) => {
                for child in children.iter() {
                    write!(f, "{child}")?;
                }
            }
            Self::Component(comp) => {
                write!(f, "{comp}")?;
            }
        }
        Ok(())
    }
}

pub trait Component: fmt::Display {}

pub struct MyComponent<T = ()> {
    phantom: std::marker::PhantomData<T>,
}

impl<T> MyComponent<T> {
    pub fn builder(_tag: &'static str) -> MyComponentBuilder<T> {
        MyComponentBuilder {
            phantom: std::marker::PhantomData,
        }
    }
}

pub struct MyComponentBuilder<T> {
    phantom: std::marker::PhantomData<T>,
}

impl<T: 'static> MyComponentBuilder<T> {
    pub fn finish(&mut self) -> VNode {
        VNode::Component(Rc::new(MyComponentProps {
            phantom: std::marker::PhantomData::<T>,
        }))
    }
}

pub struct MyComponentProps<T> {
    phantom: std::marker::PhantomData<T>,
}

impl<T> fmt::Display for MyComponentProps<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "My custom component")
    }
}

impl<T> Component for MyComponentProps<T> {}

impl<T: 'static> From<MyComponentProps<T>> for VNode {
    fn from(component: MyComponentProps<T>) -> VNode {
        VNode::Component(Rc::new(component))
    }
}

fn escape_attr_quoted_fmt<W: fmt::Write>(w: &mut W, s: &str) -> fmt::Result {
    for ch in s.chars() {
        match ch {
            '&' => w.write_str("&amp;")?,
            '"' => w.write_str("&quot;")?,
            '<' => w.write_str("&lt;")?,
            '>' => w.write_str("&gt;")?,
            _ => w.write_char(ch)?,
        }
    }
    Ok(())
}

#[doc(hidden)]
#[allow(non_camel_case_types)]
pub mod html_context {
    pub type h1 = super::VNode;
    pub type div = super::VNode;
    pub type span = super::VNode;
    pub type br = super::VNode;
    pub type Text = super::VNode;
    pub type Fragment = super::VNode;
    pub use super::MyComponent;
}

pub mod prelude {
    pub use super::{html_context, VNode};
    pub use yo_html::html;
}