Skip to content

Instantly share code, notes, and snippets.

@evanrelf
Last active February 23, 2024 07:17
Show Gist options
  • Save evanrelf/09bba9478160da66764fc9d4ca794522 to your computer and use it in GitHub Desktop.
Save evanrelf/09bba9478160da66764fc9d4ca794522 to your computer and use it in GitHub Desktop.
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