Crate laby

source ·
Expand description

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.

// 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:

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:

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, &#39;world&#39;</p>\
    </article>\
");

The above code constructs a basic HTML structure for an article with the title, content and class attribute templated.

  • class attribute: a format! 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 &#39;. 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.

// 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.

// 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:

  1. Write a struct that stores all necessary data for your rendering operation.
  2. Implement the Render trait for that struct.
  3. 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.

Modules

  • Provides types used by laby internally.

Macros

Structs

Enums

  • Renders a document type declaration.

Traits

  • Formats a value into an HTML representation.

Attribute Macros

  • Generates a macro that calls a function with named arguments.