Last active
February 23, 2024 07:17
-
-
Save evanrelf/09bba9478160da66764fc9d4ca794522 to your computer and use it in GitHub Desktop.
This file contains 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
use std::fmt::{self, Display}; | |
// TODO: Use tuples instead of arrays for heterogeneous lists | |
// TODO: Escaping | |
#[derive(Debug, PartialEq)] | |
pub enum Html { | |
Node(Node), | |
Text(String), | |
} | |
impl Display for Html { | |
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
match self { | |
Html::Node(node) => write!(f, "{}", node), | |
Html::Text(text) => write!(f, "{}", text), | |
} | |
} | |
} | |
#[derive(Debug, PartialEq)] | |
pub struct Node { | |
pub name: String, | |
pub attributes: Vec<Attribute>, | |
pub children: Vec<Html>, | |
} | |
impl Node { | |
pub fn new(name: impl Into<String>) -> Node { | |
Node { | |
name: name.into(), | |
attributes: Vec::new(), | |
children: Vec::new(), | |
} | |
} | |
} | |
impl Display for Node { | |
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
write!(f, "<{}", self.name)?; | |
for attribute in &self.attributes { | |
write!(f, " {}", attribute)?; | |
} | |
write!(f, ">")?; | |
for child in &self.children { | |
write!(f, "{}", child)?; | |
} | |
write!(f, "</{}>", self.name)?; | |
Ok(()) | |
} | |
} | |
#[derive(Debug, PartialEq)] | |
pub struct Attribute { | |
pub name: String, | |
pub value: Option<String>, | |
} | |
impl Display for Attribute { | |
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
if let Some(value) = &self.value { | |
write!(f, "{}=\"{}\"", self.name, value) | |
} else { | |
write!(f, "{}", self.name) | |
} | |
} | |
} | |
pub trait BuildNode { | |
fn attributes(self, attributes: impl IntoAttributes) -> Node; | |
fn children(self, children: impl IntoChildren) -> Node; | |
} | |
impl BuildNode for Node { | |
fn attributes(mut self, attributes: impl IntoAttributes) -> Node { | |
self.attributes.extend(attributes.into_attributes()); | |
self | |
} | |
fn children(mut self, children: impl IntoChildren) -> Node { | |
self.children.extend(children.into_children()); | |
self | |
} | |
} | |
impl BuildNode for &str { | |
fn attributes(self, attributes: impl IntoAttributes) -> Node { | |
Node::new(self).attributes(attributes) | |
} | |
fn children(self, children: impl IntoChildren) -> Node { | |
Node::new(self).children(children) | |
} | |
} | |
trait IntoHtml { | |
fn into_html(self) -> Html; | |
} | |
impl IntoHtml for Html { | |
fn into_html(self) -> Html { | |
self | |
} | |
} | |
impl IntoHtml for Node { | |
fn into_html(self) -> Html { | |
Html::Node(self) | |
} | |
} | |
impl IntoHtml for &str { | |
fn into_html(self) -> Html { | |
Html::Text(String::from(self)) | |
} | |
} | |
impl IntoHtml for String { | |
fn into_html(self) -> Html { | |
Html::Text(self) | |
} | |
} | |
trait IntoAttribute { | |
fn into_attribute(self) -> Attribute; | |
} | |
impl IntoAttribute for Attribute { | |
fn into_attribute(self) -> Attribute { | |
self | |
} | |
} | |
impl IntoAttribute for &str { | |
fn into_attribute(self) -> Attribute { | |
Attribute { | |
name: String::from(self), | |
value: None, | |
} | |
} | |
} | |
impl IntoAttribute for String { | |
fn into_attribute(self) -> Attribute { | |
Attribute { | |
name: self, | |
value: None, | |
} | |
} | |
} | |
impl<K, V> IntoAttribute for (K, V) | |
where | |
K: Into<String>, | |
V: Into<String>, | |
{ | |
fn into_attribute(self) -> Attribute { | |
let (name, value) = self; | |
Attribute { | |
name: name.into(), | |
value: Some(value.into()), | |
} | |
} | |
} | |
trait IntoChildren { | |
fn into_children(self) -> Vec<Html>; | |
} | |
impl<T> IntoChildren for T | |
where | |
T: IntoHtml, | |
{ | |
fn into_children(self) -> Vec<Html> { | |
[self.into_html()].into_children() | |
} | |
} | |
impl<T, const N: usize> IntoChildren for [T; N] | |
where | |
T: IntoHtml, | |
{ | |
fn into_children(self) -> Vec<Html> { | |
self.into_iter().map(|x| x.into_html()).collect() | |
} | |
} | |
impl<T> IntoChildren for Vec<T> | |
where | |
T: IntoHtml, | |
{ | |
fn into_children(self) -> Vec<Html> { | |
self.into_iter().map(|x| x.into_html()).collect() | |
} | |
} | |
trait IntoAttributes { | |
fn into_attributes(self) -> Vec<Attribute>; | |
} | |
impl<T> IntoAttributes for T | |
where | |
T: IntoAttribute, | |
{ | |
fn into_attributes(self) -> Vec<Attribute> { | |
[self.into_attribute()].into_attributes() | |
} | |
} | |
impl<T, const N: usize> IntoAttributes for [T; N] | |
where | |
T: IntoAttribute, | |
{ | |
fn into_attributes(self) -> Vec<Attribute> { | |
self.into_iter().map(|x| x.into_attribute()).collect() | |
} | |
} | |
impl<A> IntoAttributes for Vec<A> | |
where | |
A: IntoAttribute, | |
{ | |
fn into_attributes(self) -> Vec<Attribute> { | |
self.into_iter().map(|a| a.into_attribute()).collect() | |
} | |
} | |
#[cfg(test)] | |
mod tests { | |
use super::*; | |
#[test] | |
fn desugar() { | |
let sugar = "span" | |
.attributes("cool") | |
.attributes(("style", "color: red")) | |
.attributes([("id", "message"), ("class", "big")]) | |
.children("Graphic design is my passion") | |
.children(vec![ | |
Node::new("hr").into_html(), | |
"center" | |
.children(String::from("(c) 2024 Evan Relf")) | |
.into_html(), | |
]); | |
let raw = Node { | |
name: String::from("span"), | |
attributes: vec![ | |
Attribute { | |
name: String::from("cool"), | |
value: None, | |
}, | |
Attribute { | |
name: String::from("style"), | |
value: Some(String::from("color: red")), | |
}, | |
Attribute { | |
name: String::from("id"), | |
value: Some(String::from("message")), | |
}, | |
Attribute { | |
name: String::from("class"), | |
value: Some(String::from("big")), | |
}, | |
], | |
children: vec![ | |
Html::Text(String::from("Graphic design is my passion")), | |
Html::Node(Node { | |
name: String::from("hr"), | |
attributes: Vec::new(), | |
children: Vec::new(), | |
}), | |
Html::Node(Node { | |
name: String::from("center"), | |
attributes: Vec::new(), | |
children: vec![Html::Text(String::from("(c) 2024 Evan Relf"))], | |
}), | |
], | |
}; | |
assert_eq!(sugar, raw); | |
} | |
#[test] | |
fn display() { | |
let rust = "div" | |
.attributes([ | |
"enabled".into_attribute(), | |
("id", "foo").into_attribute(), | |
("class", "bar").into_attribute(), | |
]) | |
.children([ | |
"h1".children("Title").into_html(), | |
"Hello".into_html(), | |
" ".into_html(), | |
"world".into_html(), | |
]); | |
let string = r#"<div enabled id="foo" class="bar"><h1>Title</h1>Hello world</div>"#; | |
assert_eq!(rust.to_string(), string); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment