Created
September 21, 2024 15:05
-
-
Save shritesh/346a87643280d1f7cf8397dff89fe8e6 to your computer and use it in GitHub Desktop.
HTML in Rust
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// TODO: escaping | |
enum Attribute { | |
Boolean(String), | |
Normal(String, String), | |
} | |
pub struct Element { | |
name: &'static str, | |
attributes: Vec<Attribute>, | |
child: Node, | |
} | |
impl Element { | |
pub fn on(mut self, event: &'static str, value: impl Into<String>) -> Self { | |
self.attributes | |
.push(Attribute::Normal(format!("x-on:{event}"), value.into())); | |
self | |
} | |
pub fn bind(mut self, attr: &'static str, value: impl Into<String>) -> Self { | |
self.attributes | |
.push(Attribute::Normal(format!("x-bind:{attr}"), value.into())); | |
self | |
} | |
} | |
macro_rules! impl_attrs { | |
($attr:ident) => { | |
impl Element { | |
pub fn $attr(mut self, value: impl Into<String>) -> Self { | |
self.attributes.push(Attribute::Normal(stringify!($attr).to_string(),value.into())); | |
self | |
} | |
} | |
}; | |
($attr:ident, $($rest:ident),+) => { | |
impl_attrs!($attr); | |
impl_attrs!($($rest),+); | |
}; | |
} | |
macro_rules! impl_bool_attrs { | |
($attr:ident) => { | |
impl Element { | |
pub fn $attr(mut self) -> Self { | |
self.attributes.push(Attribute::Boolean(stringify!($attr).to_string())); | |
self | |
} | |
} | |
}; | |
($attr:ident, $($rest:ident),+) => { | |
impl_bool_attrs!($attr); | |
impl_bool_attrs!($($rest),+); | |
}; | |
} | |
macro_rules! impl_alpine_attrs { | |
($attr:ident) => { | |
impl Element { | |
pub fn $attr(mut self, value: impl Into<String>) -> Self { | |
self.attributes.push(Attribute::Normal(format!("x-{}", stringify!($attr)),value.into())); | |
self | |
} | |
} | |
}; | |
($attr:ident, $($rest:ident),+) => { | |
impl_alpine_attrs!($attr); | |
impl_alpine_attrs!($($rest),+); | |
}; | |
} | |
impl Element { | |
pub fn render(&self, output: &mut String) { | |
if self.name == "html" { | |
output.push_str("<!DOCTYPE html>"); | |
} | |
output.push('<'); | |
output.push_str(&self.name); | |
for attribute in &self.attributes { | |
output.push(' '); | |
match attribute { | |
Attribute::Boolean(key) => output.push_str(key), | |
Attribute::Normal(key, value) => { | |
output.push_str(key); | |
output.push('='); | |
output.push('"'); | |
output.push_str(value); | |
output.push('"'); | |
} | |
} | |
} | |
if let Node::Void = self.child { | |
output.push_str("/>"); | |
return; | |
} | |
output.push('>'); | |
self.child.render(output); | |
output.push('<'); | |
output.push('/'); | |
output.push_str(&self.name); | |
output.push('>'); | |
} | |
} | |
pub enum Node { | |
Void, | |
Text(String), | |
Element(Box<Element>), | |
Fragment(Vec<Node>), | |
} | |
impl Node { | |
fn render(&self, output: &mut String) { | |
match self { | |
Node::Void => (), | |
Node::Text(t) => output.push_str(t), | |
Node::Element(el) => el.render(output), | |
Node::Fragment(fragments) => { | |
for f in fragments { | |
f.render(output); | |
} | |
} | |
} | |
} | |
} | |
impl From<()> for Node { | |
fn from(_: ()) -> Self { | |
Self::Text(String::new()) | |
} | |
} | |
impl From<String> for Node { | |
fn from(value: String) -> Self { | |
Self::Text(value) | |
} | |
} | |
impl From<&str> for Node { | |
fn from(value: &str) -> Self { | |
Self::Text(value.to_string()) | |
} | |
} | |
impl From<Element> for Node { | |
fn from(value: Element) -> Self { | |
Self::Element(Box::new(value)) | |
} | |
} | |
macro_rules! impl_from_tuple { | |
($($t:ident: $idx:tt),+) => { | |
impl<$($t: Into<Node>),+> From<($($t),+)> for Node { | |
fn from(value: ($($t),+)) -> Self { | |
let mut fragments = Vec::new(); | |
$( | |
fragments.push(value.$idx.into()); | |
)+ | |
Node::Fragment(fragments) | |
} | |
} | |
}; | |
} | |
macro_rules! impl_tags { | |
($tag:ident) => { | |
pub fn $tag(child: impl Into<Node>) -> Element { | |
Element { | |
name: stringify!($tag), | |
attributes: Vec::new(), | |
child: child.into(), | |
} | |
} | |
}; | |
($tag:ident, $($rest:ident),+) => { | |
impl_tags!($tag); | |
impl_tags!($($rest),+); | |
}; | |
} | |
macro_rules! impl_void_tags { | |
($tag:ident) => { | |
pub fn $tag() -> Element { | |
Element { | |
name: stringify!($tag), | |
attributes: Vec::new(), | |
child: Node::Void, | |
} | |
} | |
}; | |
($tag:ident, $($rest:ident),+) => { | |
impl_void_tags!($tag); | |
impl_void_tags!($($rest),+); | |
}; | |
} | |
impl_from_tuple!(T0: 0, T1: 1); | |
impl_from_tuple!(T0: 0, T1: 1, T2: 2); | |
impl_from_tuple!(T0: 0, T1: 1, T2: 2, T3: 3); | |
impl_from_tuple!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4); | |
impl_from_tuple!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5); | |
impl_from_tuple!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6); | |
impl_from_tuple!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7); | |
impl_from_tuple!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8); | |
impl_from_tuple!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9); | |
impl_void_tags!(img, link, meta); | |
impl_tags!(a, body, div, h1, h2, h3, h4, h5, h6, head, html, p, span, script, template, title); | |
impl_attrs!(charset, class, content, href, name, rel, src); | |
impl_bool_attrs!(defer); | |
impl_alpine_attrs!(data, show, model, text, r#if, r#for); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment