11 unstable releases (3 breaking)
0.4.1 | Apr 6, 2023 |
---|---|
0.4.0 | Apr 6, 2023 |
0.3.1 | Apr 2, 2023 |
0.2.5 | Apr 2, 2023 |
0.1.0 | Nov 26, 2021 |
#292 in Template engine
90KB
1K
SLoC
laby
laby is a small macro library for writing HTML templates in Rust. Documentation
let n = html!(
head!(
title!("laby"),
),
body!(
p!("Hello, world!"),
),
);
let s = render!(DocType::HTML5, n);
<!DOCTYPE html><html><head><title>laby</title></head><body><p>Hello, world!</p></body></html>
lib.rs
:
laby is a small macro library for writing fast HTML templates in Rust. It focuses on three things:
- Simplicity: laby has minimal dependencies, works out of the box without any configuration, and can be easily extended to add extra functionality where necessary.
- Performance: laby generates specialized code that generate HTML. It requires no heap allocation at runtime other than the buffer that the resulting HTML gets rendered to. Any operation that involves extra heap allocations is opt-in. All rendering code is statically type checked at compile time and inlined for performance.
- Familiarity: laby provides macros that can accept any valid Rust code and expand to regular Rust code; learning a new DSL for HTML templating is not necessary. Macros can be nested, composed, formatted by rustfmt, separated into components and returned by functions just like regular Rust code.
Much of laby's high-performance code was inherited from sailfish, an extremely fast HTML templating engine for Rust. However, whereas sailfish processes HTML template files with special syntax, laby provides macros that are embedded into your code directly. Which library to adopt is up to your coding style and personal preference.
laby targets Rust stable and supports embedded environments with no_std. No configuration is required.
Installation
In your project, add the following line to your Cargo.toml
in the [dependencies]
section:
[dependencies]
laby = "0.4"
Additionally, you may want to import laby into your code like this:
use laby::*;
This is purely for convenience because laby exports a large amount of macros, each of which represent an HTML tag. Of course, it is possible to import only the macros you use individually. The rest of this guide assumes that you have imported the necessary macros already.
laby does not provide integration support for popular web frameworks. It returns a plain old
String
as the rendered result, so you are encouraged to write your own macro that writes
that String
to the response stream. Most web frameworks can do this out of the box.
Basics
laby provides procedural macros that generate specialized Rust code at compile time, which in turn generate HTML code when rendered at runtime. In order to use laby effectively, understanding how it transforms your code is necessary. Consider the following example.
# use laby::*;
// construct a tree of html nodes
let n = html!(
head!(
title!("laby"),
),
body!(
class = "dark",
p!("hello, world"),
),
);
// convert the tree into a string
let s = render!(n);
// check the result
assert_eq!(s, "\
<html>\
<head>\
<title>laby</title>\
</head>\
<body class=\"dark\">\
<p>hello, world</p>\
</body>\
</html>\
");
The above code uses the macros html!
, head!
, title!
, body!
and [p!
] to
construct a basic HTML structure. Then, the render!
macro is used to convert the tree into
a String
representation. The result is compared to another string which is spread over
multiple lines for readability.
Notice how the children of a node are passed as regular positional arguments, while the attributes of a node are specified as assignment expressions. This is a perfectly valid Rust syntax, which means it can be formatted using rustfmt.
Under the hood, laby transforms the above code into code that looks something like this:
# use laby::*;
let n = {
struct _html {}
impl Render for _html {
#[inline]
fn render(self, buf: &mut laby::internal::Buffer) {
buf.push_str("<html><head><title>laby</title></head><body class=\"dark\"><p>hello, world</p></body></html>");
}
}
_html {}
};
let s = render!(n);
// assert_eq!(s, ...);
This is, in essence, all that laby macros do; they simply declare a new specialized struct for
a tree of nodes, implement the Render
trait for that struct, construct that struct, and
return the constructed value.
When this code is compiled for release, all that wrapper code is stripped away and the rendering code is inlined, leaving something like this for execution:
# use laby::*;
let mut buf = laby::internal::Buffer::new();
buf.push_str("<html><head><title>laby</title></head><body class=\"dark\"><p>hello, world</p></body></html>");
let s = buf.into_string();
// assert_eq!(s, ...);
Templating
laby accepts any valid expression in place of attribute names and values and child nodes, and can access variables in the local scope just like regular code. It is not limited to only string literals.
The only requirement is for the expression to evaluate to a value that implements the
Render
trait. Refer to the list of foreign impls to see which types
implement this trait out of the box. The evaluated value is stored in the specialized struct
and rendered when the render!
macro is called. Consider the following example.
// retrieve an article from a database
let title = "laby";
let content = "hello, 'world'";
let date = "2030-01-01";
// construct a tree of nodes, with templated expressions
let n = article!(
class = format!("date-{}", date),
h1!({
let mut title = title.to_owned();
title.truncate(30);
title
}),
p!(content),
);
// convert the tree into a string
let s = render!(n);
// check the result
assert_eq!(s, "\
<article class=\"date-2030-01-01\">\
<h1>laby</h1>\
<p>hello, 'world'</p>\
</article>\
");
The above code constructs a basic HTML structure for an article with the title, content and class attribute templated.
class
attribute: aformat!
macro expression is expanded and evaluated.<h1>
node: an expression that truncates the title to at most thirty characters is evaluated.<p>
node: a simple local variable expression is evaluated.
Note, that these expressions are evaluated where the node is constructed (i.e. let n = ...
), not where the render!
macro is called.
Additionally, the apostrophes in the article contents are escaped with the HTML entity '
.
laby escapes all templated expressions by default unless the raw!
macro is used.
Under the hood, laby transforms the above code into code that looks something like this:
let title = "laby";
let content = "hello, 'world'";
let date = "2030-01-01";
let n = {
struct _article<T1, T2, T3> { t1: T1, t2: T2, t3: T3 }
impl<T1, T2, T3> Render for _article<T1, T2, T3>
where T1: Render, T2: Render, T3: Render {
#[inline]
fn render(self, buf: &mut laby::internal::Buffer) {
buf.push_str("<article class=\"");
self.t1.render(buf); // date
buf.push_str("\"><h1>");
self.t2.render(buf); // title
buf.push_str("</h1><p>");
self.t3.render(buf); // content
buf.push_str("</p></article>");
}
}
_article {
t1: format!("date-{}", date),
t2: {
let mut title = title.to_owned();
title.truncate(30);
title
},
t3: content
}
};
let s = render!(n);
// assert_eq!(s, ...);
Notice how the fields of the generated specialized struct are generic over the templated
expressions. When that struct is constructed (i.e. _article { ... }
), the compiler is able to
infer the generic type arguments from the field assignments and monomorphize the struct. Iff
all field expressions evaluate to a value that implements the Render
trait, then that trait
will also be implemented for the generated struct, allowing for it to be rendered by
render!
.
Componentization
Writing a large template for rendering an entire HTML document quickly becomes unwieldy and unmaintainable, so it is often necessary to break up the document into several smaller components. There are two popular techniques around this problem: include and inherit. laby supports both patterns, using the language features provided by Rust.
In practice, these patterns are often mixed and matched together to form a complete and coherent document. Examples of both approaches are explored below.
Template inheritance
This is a top-down approach that breaks down a large document into small components. This leads to a consistent but rigid structure that is difficult to extend or change easily.
# use laby::*;
// a large template that takes small components
fn page(title: impl Render, header: impl Render, body: impl Render) -> impl Render {
html!(
head!(
title!(title),
),
body!(
header!(header),
main!(body),
),
)
}
// a component that *inherits* a large template
fn home() -> impl Render {
page(
"Home",
h1!("About laby"),
p!("laby is an HTML macro library for Rust."),
)
}
assert_eq!(render!(home()), "\
<html>\
<head>\
<title>Home</title>\
</head>\
<body>\
<header>\
<h1>About laby</h1>\
</header>\
<main>\
<p>laby is an HTML macro library for Rust.</p>\
</main>\
</body>\
</html>\
");
Template inclusion
This is a bottom-up approach that consolidates small components to form a large document. This leads to a flexible but possibly inconsistent structure that may also result in more boilerplate code.
# use laby::*;
// small individual components
fn title() -> impl Render {
"Home"
}
fn header() -> impl Render {
h1!("About laby")
}
fn body() -> impl Render {
p!("laby is an HTML macro library for Rust.")
}
// a large component that *includes* the small components
fn home() -> impl Render {
html!(
head!(
title!(title()),
),
body!(
header!(header()),
main!(body()),
),
)
}
assert_eq!(render!(home()), "\
<html>\
<head>\
<title>Home</title>\
</head>\
<body>\
<header>\
<h1>About laby</h1>\
</header>\
<main>\
<p>laby is an HTML macro library for Rust.</p>\
</main>\
</body>\
</html>\
");
Naming arguments
Sometimes components can get big and accept a long list of positional arguments that hurts
readability. laby provides an attribute macro called #[laby]
which lets you call
arbitrary functions with explicitly named arguments and optional values, similar to HTML
macros.
To enable support, simply prepend the attribute before the component function and call it using the generated macro.
#[laby]
fn page(title: impl Render, header: impl Render, body: impl Render) -> impl Render {
html!(
head!(
title!(title),
),
body!(
header!(header),
main!(body),
),
)
}
#[laby]
fn home() -> impl Render {
// `page` function called using the generated `page!` macro
page!(
title = "Home",
header = h1!("About laby"),
body = p!("laby is an HTML macro library for Rust."),
)
}
Extensions
laby can be extended by simply implementing the Render
trait, which is a low-level trait
that represents the smallest unit of a rendering operation. If what laby provides out of the
box is too limiting for your specific use case, or if laby does not provide a Render
implementation for a type you need, implementing this trait yourself may be a viable solution.
The general pattern for creating an extension is like this:
- Write a struct that stores all necessary data for your rendering operation.
- Implement the
Render
trait for that struct. - Provide a simple, short macro that constructs that struct conveniently.
In fact, the macros iter!
, raw!
and disp!
are implemented in this way. They are not
magic; they are simply extensions of laby's core rendering system. You can even ignore laby's
HTML macros and write your own transformations to implement the Render
trait.
License
laby is written by chiya.dev, licensed under the MIT License. Portions of code were taken from sailfish which is written by Ryohei Machida, also licensed under the MIT License. Documentation for HTML tags were taken from MDN, licensed under CC-BY-SA 2.5.
Dependencies
~2.5MB
~51K SLoC