RsHtml

A compile-time, type-safe, lightweight and flexible template engine for Rust, designed to seamlessly integrate Rust code within HTML templates.

View on GitHub View on Crates.io

Current Version: v0.6.0

🚀 Introduction

RsHtml is a powerful template engine that transforms your HTML templates into highly efficient Rust code at compile time, allowing you to seamlessly use Rust logic and expressions together with HTML to harness the full power of Rust for dynamic content generation. It is designed to help you build flexible and maintainable web applications.

📦 Installation

Add to Cargo.toml

Cargo.toml:

[dependencies]
rshtml = "0.6.0"

# rshtml = { version = "0.6.0", features = ["functions"] }

🧩 Editor Support

You can download the compiled rshtml-analyzer language server suitable for your system from the Releases Page or use following command:

cargo install --git https://github.com/rshtml/rshtml-analyzer.git --tag v0.1.6


Editor support for RsHtml is available for a variety of modern code editors. Click on your supported editor from the list below to visit the repository and find installation instructions:

VS Code VSCodium     Neovim     Zed     Helix

Helix

Add the following language setting to your languages.toml file:

[[language]]
name = "rshtml"
file-types = [{ glob = "*.rs.html" }]
scope = "source.rshtml"
language-servers = [
  "rshtml-analyzer",
  "vscode-html-language-server",
  "superhtml",
]
grammar = "rshtml"
roots = ["Cargo.lock", "Cargo.toml"]
block-comment-tokens = { start = "@*", end = "*@" }
indent = { tab-width = 2, unit = "  " }

[language-server.rshtml-analyzer]
command = "rshtml-analyzer"
args = ["--stdio"]

[[grammar]]
name = "rshtml"
source = { git = "https://github.com/rshtml/tree-sitter-rshtml", rev = "363c52c1630c491a5094ef5b369f12b4b858392a" }

You can download the compiled rshtml-analyzer code suitable for your system from the Releases Page or use following command:

cargo install --git https://github.com/rshtml/rshtml-analyzer.git --tag v0.1.6

In tree-sitter, you must copy the files in the tree-sitter-rshtml/queries/ folder to the runtime/queries/rshtml/ location in the helix config folder.

It should look like this:

~/.config/helix/runtime/queries/rshtml/highlights.scm
~/.config/helix/runtime/queries/rshtml/injections.scm

It can be checked by the hx --health rshtml command.


Support for other editors is planned for the future. If you would like to see support for an editor that isn’t listed, please open an issue to let us know.

✨ v! Macro

The v! macro is a procedural macro that allows you to write HTML while embedding Rust code using rust blocks ({}). It produces a portable view type and internally uses a closure-based implementation.

The type generated by the v! macro implements the View trait, which means it can be passed around as impl View or returned as a return type. Inside the HTML content, you can also embed Rust expressions in attribute values using rust blocks ({}). The rust block is evaluated and its result is injected directly into the output, so it expects an expression. The returned value must implement either the Display or the View trait.
The v! macro is a simple yet powerful tool that allows you to dynamically compose HTML fragments with Rust and build complex views by combining them together.

Simple example usage of the v! macro

use rshtml::{View, v};
use std::fmt;

fn main() -> fmt::Result {
    let template = "RsHtml";
    let hello = v!(<p>Hello {template}</p>);

    let mut out = String::with_capacity(hello.text_size());

    hello.render(&mut out)?;

    print!("{out}");

    Ok(())
}

Some examples of usage:

fn foo() -> impl View {
    let x = 5;
    let s = String::from("hi");

    v!(this is x: {x}, this is s: {s})
}

fn bar() -> Box<dyn View> {
    let x = 5;
    let s = String::from("hey");

    if x == 5 {
        v!(this is x: {x}, this is s: {s}).boxed()
    } else {
        v!(oooo).boxed()
    }
}

fn try() {
    let mut numbers = Vec::new();
    for i in 0..10 {
        numbers.push(v!(<li>{i}</li>));
    }

    let res = v!(
        {card()}

        {bar()}

        <ul>
            {&numbers}
        </ul>
    );
}

📌 Note on Whitespace and Tokens

Since v! is a Rust procedural macro, it processes your HTML content as a stream of Rust tokens. The Rust compiler automatically strips the original whitespace between these tokens during parsing.

To prevent distinct tokens (like text and punctuation) from merging together and breaking the output, the macro safely inserts a single space between them.

For example: Whether you write hello. (no space) or hello . (multiple spaces), the macro reads them as the exact same two distinct tokens (hello and .). As a result, both will produce the exact same output with a single space: hello .

The View Trait

The View trait is the core trait used by the v! macro. By implementing the View trait for your own struct, you can integrate your custom types with the v! macro and use it seamlessly within templates.

Usage Example:

struct Home {
    title: String,
    count: i32,
}

impl View for Home {
    fn render(&self, out: &mut dyn Write) -> fmt::Result {
        v!(<div>Home Page, title:{&self.title}, count:{self.count}</div>)(out)
    }
}

#[test]
fn view_trait() {
    let mut out = String::with_capacity(24);

    let home = Home {
        title: "home title".to_owned(),
        count: 7,
    };

    home.render(&mut out).unwrap();

    assert_eq!(
        out,
        "<div> Home Page , title : home title , count : 7 </div>"
    )
}

The view_iter() Iterator Extension

To pass iterator results into a view without calling collect, use the view_iter() extension function.
This function is provided as a trait extension for iterator types and allows views to consume iterators directly.

Example

let card_views = cards
    .iter()
    .map(|card| v!(<div class="card">{&card.title}</div>))
    .view_iter(); // extension function

v! {
    <div>
        { card_views }
    </div>
}

The view_iter() function enables efficient rendering of iterator output inside views, avoiding unnecessary allocations.

The boxed() Function

The boxed() function is provided for wrapping views inside a Box.
It can be used to erase concrete view types and unify return types when working with conditional branches.

Example
fn do_boxed() -> Box<dyn View> {
    let x = 5;
    let s = String::from("hi");

    if x == 5 {
        v!(this is x: {x}, this is s: {s}).boxed()
    } else {
        v!(oooo).boxed()
    }
}

The boxed() function enables returning dynamically dispatched views by boxing them into a Box<dyn View>.

🧱 View Derive Macro

1. Define a Struct

The View derive macro automatically handles the implementation of the View trait for your structs. With this implementation, it becomes usable together with the v! macro. It processes the rs.html file while doing this. You can specify the path relative to CARGO_MANIFEST_DIR or the current directory using the path parameter. If the path parameter is not provided, it removes Page from the struct name, converts it to snake_case, adds the .rs.html extension, and looks in the views folder; for example, for a struct named HomePage, the inferred path will be views/home.rs.html. You can provide the path parameter like this: #[view(path="index.rs.html")].

Extract file:

The extract parameter can be used to extract the Rust sections of an rs.html file into the target directory and include them via a macro. This approach simplifies error handling and improves compiler diagnostics. By default, extract is set to false.

#[view(extract)] or #[view(extract = true)]

Struct Definition:

use rshtml::View;

#[derive(View)]
// #[view(path="views/index.rs.html", extract)]
struct HomePage {
    username: String,
    items: Vec<String>,
}

2. Render The Template

Once your struct is defined, you can render its corresponding template by creating an instance of the struct and then calling the .render(out) method. The out parameter expects a type that implements rshtml::Write. Types implementing this trait must also implement fmt::Write. Any type that implements fmt::Write is already automatically provided with an implementation of rshtml::Write by RsHtml.

Accessing Data and Logic in the View

Inside your .rs.html template, you have full access to the fields and methods of your struct instance through the self keyword.

    <p>Welcome, @self.title</p>
    <span>Your formatted name is: @self.get_formatted_name()</span>

Template Rendering:

fn main() {
    let page = HomePage {
        title: "RsHtml".to_string(),
        items: vec!["Item 1".to_string(), "Item 2".to_string()],
    };

    let mut out = String::with_capacity(out.text_size());

    page.render(&mut out).unwrap();
    println!("{}", out);
}

Core Syntax Reference

Expressions

@expression / @(expression)

RsHtml allows seamless integration of Rust logic through expressions, which are evaluated at render time. All expressions begin with the @ prefix.

Rust’s Ownership Rules Still Apply

A core principle of RsHtml is that it doesn’t hide Rust’s power—or its rules. The template content is translated into a Rust function at compile time. Consequently, all expressions you write must adhere to Rust’s strict ownership and borrowing model.

Simple Expressions

Simple variable access, function calls, or field lookups can be written directly following the @ prefix.

<h1>Welcome, @self.username</h1>
<p>You have @self.items.len() items.</p>

Parenthesized Expressions

For more complex expressions that might be ambiguous, you should enclose them in parentheses: @(...). This ensures the entire expression is parsed and evaluated as a single unit.

<p>Final price: @(item.cost + item.tax)</p>
<p>@((self.value * 10).pow(2))</p>

Control Flows & Loops

ℹ️ Inside these blocks, the closing brace } character has a special meaning, as it marks the end of the block. You can use { and } in a balanced way, meaning that every opening brace must be closed.

Conditions: @if / else / else if

The syntax for RsHtml control flows, is defined as @<directive> <rust-expression> { <inner-template> } ..

@if self.items.is_empty() {
    <p>You have no items.</p>
} else {
    <p>Here are your items.</p>
}
@if self.count == 0 {
    <p>You have no items.</p>
} else if self.count == 1 {
    <p>You have one item.</p>
} else {
    <p>Here are your items.</p>
}
Loops: @for / @while

RsHtml allows you to use Rust’s native @for and @while loops to generate repetitive template content.

The syntax mirrors standard Rust. You can write your loop expression, followed by a block { … } containing the template to be rendered for each iteration. Inside the block, you can freely mix HTML with other Rust expressions, which must be prefixed with @.

<ul>
    @for item in &self.items {
        <li>@item</li>
    }

    <table>
        @for (index, user) in self.users.iter().enumerate() {
            <tr>
                <td>@(index + 1)</td>
                <td>@user.name</td>
            </tr>
        }
    </table>
</ul>

With continue and break directives:

@for i in 0..10 {
    @if i == 3 {
        @continue
    }

    @if i == 8 {
        @break
    }

    <p>it is @i</p>
}
@while count < 10 {
    <p> Counter is: @count </p>
    @(count += 1)
}

Rust Code Blocks

@{...}

You can embed larger chunks of Rust logic using @{ ... } blocks. This allows you to declare variables and perform complex operations.

@{
    let s = "hello";

     fn inline_function() -> String {
        "inline function".to_string()
     }

     for i in 0..10 {
        println!("Item {}", i);
     }

     let message = "I Love RsHtml!";
}

<p>@message</p>

Rendering & Escaping

@# ~ @@

Raw Rendering (The @# Prefix)

By default, RsHtml prioritizes security. Any output from a Rust expression (@self.my_var) is automatically HTML-escaped. This means characters like < and > are converted to &lt; and &gt;, which prevents Cross-Site Scripting (XSS) attacks by ensuring that string variables cannot inject malicious HTML.

However, there are times when you need to render raw HTML that you’ve generated in your Rust code and trust completely. To bypass the default escaping mechanism, you can prefix your expression with @#.

<div>@self.my_var</div>

<div>@#self.my_var</div>

Renders as:

<div>&lt;p&gt;This is &lt;strong&gt;bold&lt;&#x2F;strong&gt; text.&lt;&#x2F;p&gt;</div>
<div><p>This is <strong>bold</strong> text.</p></div>

The @ Character

The @ symbol always has a special meaning. To output a literal @ character anywhere in your template, you must escape it as @@.

<p>Follow us on: @@rshtml_engine</p>

Renders as: <p>Follow us on: @rshtml_engine</p>

Components

Components are reusable, self-contained pieces of UI that encapsulate both markup and logic. They are the building blocks of a modern, maintainable architecture.

A component is simply another .rs.html template that can accept parameters (props) and render child content.

Defining a Component

A component is defined in its own .rs.html file. It can access parameters passed to it and can specify where to render any “child content” it receives.

Template Parameters @(name: Type)

Component parameters must be defined at the very top of the file, excluding whitespace. The parameters passed to the component should be specified here. Parameters are defined using the syntax @(count: i32, name: &str, title). If a type is not specified as in the third parameter of the example, it implies that the parameter expects a type implementing View.

Furthermore, if a component parameter is passed as a block (e.g., <Component title = {this is block}/>), it is treated as impl View. Example usage includes @(title: impl View) or simply @(title).

When providing component parameters at the call site, the name is significant rather than the order. A parameter passed with the name title will be captured by the name title. If no additional processing is required on the parameters and they are intended solely for rendering to the screen, they can be utilized directly without explicit typing.

components/Card.rs.html

@(title, footer, content: String) @* Component parameters *@

<p>
@title
</p>

@footer

@(content.to_uppercase())

Card usage

<Card title="title" footer={<p>footer</p>}, content=@content />

@child_content Directive

The @child_content / @child_content() directive is a special marker used inside a component’s template. It indicates the exact location where the nested content, passed from the parent template, should be rendered.

@child_content
@child_content() @* Parentheses are also allowed *@

components/Card.rs.html

@(title) @* This component expects a 'title' parameter. *@
<div class="card">
    <div class="card-header">
        @title
    </div>
    <div class="card-body">
        @child_content 
    </div>
</div>

Importing Components

@use "path/to/component.rs.html" as Component

Before you can use a component, you must import it using the @use directive.

@use "components/Card.rs.html" as Card

@use "components/Alert.rs.html"

@use "components/Alert"

@use "components/Alert" as Alert

Using Components

<Component title=@self.title is_ok=true />

Important Naming Convention: When using the tag syntax, the component name must begin with a capital letter (PascalCase). This is the critical rule that allows RsHtml to distinguish a custom component like <UserProfile> from a standard HTML tag like <p>.

  • Correct: <Alert message="..."/>
  • Incorrect: <alert ... /> (This would be treated as a literal HTML tag)
@use "components/Alert.rs.html"

<Alert message="This is a warning."/>

<Alert message="Operation successful!">
    <p>Your data was saved correctly.</p>
    <a href="/home">Go back</a>
</Alert>

Passing Data (Parameters & Attributes)

Data can be passed to components using attributes: <Component attributes />. RsHtml supports several data types:

This powerful feature allows you to pass not just simple values, but also complex Rust data and even other rendered chunks of HTML as parameters.

@use "components/ComplexCard.rs.html" as Card

<Card
    title="Dynamic Card"
    is_published=true
    view_count=@self.page_views
    header={
        <div class="custom-header">
            Rendered from a template block! @self.data
        </div>
    }
/>

🛠️ Helper Functions

RsHtml includes a set of built-in helper functions that are automatically available in all your templates. These utilities are designed to simplify common tasks like JSON serialization and date/time formatting.

To take advantage of built-in helper functions within your templates, you first need to enable the functions feature. This requires two steps:

1. Enable the feature in Cargo.toml

rshtml = { version = "*", features = ["functions"] }

2. Import the functions in your Rust code

use rshtml::{View, functions::*};

json()

json<T: Serialize>(value: &T) -> String

Serializes a given Rust value into a JSON string, ready for use in JavaScript.

<script>
    const userData = @#json(&self.current_user);
    console.log("User ID:", userData.id);
</script>

Renders as:

<script>
    const userData = {"id":1,"username":"Ferris"};
    console.log("User ID:", userData.id);
</script>

json_let()

json_let<T: Serialize>(name: &str, value: &T) -> String

Converts a given Rust value into JSON and wraps it directly in a JavaScript let variable declaration.

<script>
    @#json_let("user", &self.current_user);
    console.log("Username:", user.username);
</script>

Renders as:

<script>
  let user = {"id":1,"username":"Ferris"};
  console.log("Username:", user.username);
</script>

time()

time(value: &impl Display) -> RsDateTime

Takes a date/time value and converts it into a special RsDateTime object that can be easily formatted with chainable methods.

By default, it formats the date and time in a YYYY-MM-DD HH:MM:SS format.

<p>Published on: @time(&self.post_created_at)</p>

The .pretty() Formatter:

For a more human-readable date format, you can chain the .pretty() method.

<p>Published on: @time(&self.post_created_at).pretty()</p>

Renders a date like: Jan 01, 2025

Formatting can be done with the format method:

<p>Published on: @time(&self.post_created_at).format("%A, %B %e, %Y")</p>

Renders something like: Wednesday, January 1, 2025

🤝 Contributing

Contributions are welcome! Please visit our GitHub repository to open issues or submit pull requests.

📜 License

RsHtml is licensed under your choice of the MIT License or the Apache License.