I'm 3 days into Rust and decided to write a fluent HTML builder. Lets assume this is a good idea for the sake of the discussion. There are plenty of alternatives out there but they all fall into either the template bucket (e.g. Askama) or the macro bucket (e.g. Maud). I wanted something that's pure Rust for composability.
I'm looking for general feedback but in particular I'm looking for
- API ergonomics improvements (without becoming a macro package) and
- performance improvements.
There are some known TODOs:
- Allow a
HtmlBuilder
to nest anotherHtmlBuilder
(this is how we get composability).
Usage example towards the end of the post.
Code:
/// A fluent builder for HTML
use std::collections::HashMap;
/// A node with children, such as a `<div>`.
#[derive(Debug)]
pub struct Element {
attrs: HashMap<&'static str, String>, // e.g. {"class": "container"}
children: Vec<Tree>, // child elements
tag: &'static str, // e.g. "div"
}
/// A literal value, such as a text string.
#[derive(Debug)]
struct Literal {
// TODO(tibbe): Can we
// - borrow the value for the lifetime of the builder and
// - accept any value that can be converted to a string?
//
// TODO(tibbe): Implement escaping and raw values.
value: String,
}
// A HTML tree:
#[derive(Debug)]
enum Tree {
Element(Element),
Literal(Literal),
}
// The macros aren't core to the library. They just help generate some functions
// (e.g. `class()` as a specialization of `attr()`.
macro_rules! generate_attribute_functions {
($($tag:ident),*) => {
$(
pub fn $tag<V>(self: &mut Self, value: V) -> &mut Element
where
V: Into<String>,
{
self.attr(stringify!($tag), value)
}
)*
};
}
macro_rules! generate_element_functions {
($($tag:ident),*) => {
$(
fn $tag(self: &mut Self) -> &mut Element {
self.element(stringify!($tag))
}
)*
};
}
impl Element {
pub fn new(tag: &'static str) -> Self {
Element {
attrs: HashMap::new(),
children: Vec::new(),
tag,
}
}
pub fn attr<V>(self: &mut Self, name: &'static str, value: V) -> &mut Self
where
V: Into<String>,
{
self.attrs.insert(name, value.into());
self
}
// TODO(tibbe): Generate more of these once the design has stabilized.
generate_attribute_functions!(class, id);
pub fn render(self: &Self, buf: &mut String) {
buf.push('<');
buf.push_str(self.tag);
for (name, value) in self.attrs.iter() {
buf.push(' ');
buf.push_str(name);
buf.push_str("=\"");
buf.push_str(value.as_str());
buf.push('"');
}
buf.push('>');
for child in self.children.iter() {
child.render(buf);
}
buf.push_str("</");
buf.push_str(self.tag);
buf.push('>');
}
}
/// Trait for all types that can contain `Element`s.
///
/// TODO(tibbe): This trair only exists so that `Element` and `HtmlBuilder` don't need to duplicate
/// the implementation of e.g. `element()`. Is this really neccesary?
pub trait Node {
fn children(&self) -> &Vec<Tree>;
fn mut_children(&mut self) -> &mut Vec<Tree>;
fn element(self: &mut Self, tag: &'static str) -> &mut Element {
self.mut_children().push(Tree::Element(Element {
attrs: HashMap::new(),
tag,
children: Vec::new(),
}));
let Some(Tree::Element(element)) = self.mut_children().last_mut() else {
unreachable!();
};
element
}
fn with_children<F>(self: &mut Self, func: F) -> &mut Self
where
F: Fn(&mut Self) -> (),
{
func(self);
self
}
fn str(self: &mut Self, value: &str) {
// TODO(tibbe): Can we instead borrow the value for the lifetime of the builder?
self.mut_children().push(Tree::Literal(Literal {
value: value.to_string(),
}));
}
// TODO(tibbe): Generate more of these once the design has stabilized.
generate_element_functions!(div, span, p, h1, td, tr, th, thead, tbody, table, a, button);
}
impl Node for Element {
fn children(&self) -> &Vec<Tree> {
&self.children
}
fn mut_children(&mut self) -> &mut Vec<Tree> {
&mut self.children
}
}
impl Literal {
pub fn render(self: &Self, buf: &mut String) {
buf.push_str(self.value.as_str());
}
}
impl Tree {
pub fn render(self: &Self, buf: &mut String) {
match self {
Tree::Element(el) => el.render(buf),
Tree::Literal(l) => l.render(buf),
}
}
}
/// HTML builder.
pub struct HtmlBuilder {
children: Vec<Tree>,
}
impl HtmlBuilder {
pub fn new() -> Self {
HtmlBuilder {
children: Vec::new(),
}
}
pub fn render(self: &Self, buf: &mut String) {
for child in self.children.iter() {
child.render(buf);
}
}
}
impl Node for HtmlBuilder {
fn children(&self) -> &Vec<Tree> {
&self.children
}
fn mut_children(&mut self) -> &mut Vec<Tree> {
&mut self.children
}
}
Example usage:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let mut builder = HtmlBuilder::new();
builder.div().class("container").with_children(|b| {
b.p().str("Hello ");
b.p().str("world!");
});
let mut buf = String::new();
builder.render(&mut buf);
assert_eq!(
buf,
"<div class=\"container\"><p>Hello </p><p>world!</p></div>"
);
}
}