#html-templating #html #template #component #templating #template-string #rhtml

macro rust_html_macros

Minimal compile-safe HTML templating library

6 stable releases

1.1.4 Dec 3, 2024
1.1.2 Dec 2, 2024
1.1.1 Dec 1, 2024
1.0.0 Dec 1, 2024

#671 in Template engine

Download history 54/week @ 2024-11-25 440/week @ 2024-12-02 26/week @ 2024-12-09

520 downloads per month
Used in rust_html

MIT license

28KB
483 lines

rust_html

A HTML templating library for reusable components in web applications

About

rust_html is a tiny templating library that let's you easily create reusable HTML templates and components:

use rust_html::{rhtml, Template};

let card_component = |title: &str| {
    rhtml! { r#"
        <div class="card">
            {title}
        </div>
    "#}
};

let title = "My Website";
let my_template: Template = rhtml! {r#"
    <div class="page">
        <h1>{title}</h1>
        {card_component("Card A")}
        {card_component("Card B")}
        {card_component("Card C")}
    </div>
"#};

let html_string: String = my_template.into();
println!("{}", html_string);

Why use rust_html?

  • Valid HTML syntax is enforced at compile-time
  • Runtime rust values are automatically escaped to protect against injection attacks
  • You can inject any expression or literal (not just identifiers)

The library is designed for creating reusable components for SSR (server-side rendering), and is particularly nice in combination with front end libraries like alpine.js or htmx. Unlike some other templating libraries, you can use the standard HTML syntax directly and keep the templates next to your other Rust code.

Installation

Install from crates.io using cargo:

cargo add rust_html

Usage

Types

The library has only 5 exported functions/types:

  • rhtml!: The main macro for creating templates
  • Template: represents a reusable HTML template
  • Render: trait for implementing reusable struct components
  • Unescaped: string wrapper for inserting unescaped values
  • TemplateGroup: wrapper to insert a Vec<Template>

[!NOTE]
The Template struct itself does not implement the Display trait. To print or return the HTML value as a String you can use String::from(my_template) or just my_template.into() where applicable.

The rhtml! macro

The rhtml! macro accepts a single string literal as input, typically "my text here" or r#"my text here"# which is a bit more convenient for HTML. The macro returns a Template struct that ensures injection safety when reusing template within templates.

Inside the macro string you can inject anything that implements either the std::fmt::Display or Render trait by using brackets {}. You can escape brackets inside the HTML by using two of them in a row ({{ or }}).

Example - Reusable Components

use rust_html::{rhtml, Template, TemplateGroup};

/// Reusable card component with a title property
fn card_component(title: &str) -> Template {
    rhtml! { r#"<div class="card">{title}</div>"# }
}

/// Reusable card group component that creates N cards
fn card_row_component(n_cards: u32, container_class: &str) -> Template {
    // For injecting lists of templates, we can use a TemplateGroup
    let cards: TemplateGroup = (0..n_cards)
        .map(|card_index| {
            let title = format!("Card {}", card_index);
            card_component(&title)
        })
        .collect();
    rhtml! { r#"
        <div class="{container_class}">
            {cards}
        </div>
    "# }
}

// Server endpoint
fn your_endpoint() -> String {
    let page_template: Template = rhtml! { r#"
        <div class="page">
            {card_row_component(3, "my_card_row")}
        </div>
    "# };
    // Convert the `Template` to `String`
    // This is typically only done in the endpoint just before
    // returning the full HTML. Make sure you also return a
    // `Content-Type` of `text/html` in your response
    page_template.into()
}

The your_endpoint function will return the following HTML:

<div class="page">
  <div class="my_card_row">
    <div class="card">Card 0</div>
    <div class="card">Card 1</div>
    <div class="card">Card 2</div>
  </div>
</div>

Expressions inside a template

In some cases you might want to include simple logic in your template directly. You can use any valid Rust expression inside the macro:

use rust_html::rhtml;

fn main() {
    let age = 32;
    let page = rhtml! { r#"
        <div>
            Bob is {if age >= 18 { "an adult" } else { "not an adult" }}
        </div>
    "# };
    println!("{}", String::from(page));
    // Output is '<div>Bob is an adult</div>'
}

To prevent ambiguity you must only use { and } for opening/closing scopes in the injected rust code - not inside literals or comments. The following is therefore not valid:

rhtml! { r#"{ if true { "}" } else { "" } }"# };
//                       ^
//                       Bracket not allowed

Structs as reusable components

You can also use structs as components by implementing the Render trait.

use rust_html::{rhtml, Render, Template};

#[derive(Clone)]
struct CardComponent {
    title: String,
    content: String,
}

// Implement rust_html rendering for our component
impl Render for CardComponent {
    fn render(&self) -> Template {
        rhtml! {r#"
            <div class="card">
                <h1>{self.title}</h1>
                <p>{self.content}</p>
            </div>
        "#}
    }
}

fn main() {
    let my_card = CardComponent {
        title: "Welcome".to_string(),
        content: "This is a card".to_string(),
    };
    let page = rhtml! {r#"
        <div class="page">
            {my_card}
        </div>
    "#};
}

Escaping

Template input is escaped by default to prevent injection attacks, for instance if a user were to register with a name that contains a <script> tag. The following snippet:

let sketchy_user_input = "<script>alert('hi')</script>";
let page = rhtml! {r#"<div>{sketchy_user_input}</div>"#};
println!("{}", String::from(page));

Generates a string where dangerous characters are escaped:

<div>&lt;script&gt;alert(&#x27;hi&#x27;)&lt;&#x2F;script&gt;</div>

Unescaping

If you need the unescaped value, you can use the Unescaped wrapper.

[!CAUTION]
Never use Unescaped on untrusted user input or if you don't know what you're doing.

use rust_html::{rhtml, Unescaped};
let sketchy_user_input = "<script>alert('hi')</script>";
let unescaped = Unescaped(sketchy_user_input.to_string());
let page = rhtml! {r#"<div>{unescaped}</div>"#};
println!("{}", String::from(page));

...which results in this string:

<div>
  <script>
    alert("hi");
  </script>
</div>

Integration with web frameworks

Integrating with any web framework is trivial - simply convert the template string to the response type for the given framework. If you're using Axum you can add the axum feature to get support for their IntoResponse trait.

  • maud: rust syntax for HTML
  • askama: jinja like templating library
  • tera: jinja2 like templating library
  • handlebars-rust: handlebars templating language for rust

Look at the AWWY website for more examples.

Contributing

Run tests:

cargo test -p rust_html_tests

Run doc tests:

cargo test -p rust_html_macros

Dependencies

~3.5–8.5MB
~83K SLoC