🚀 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"] }
- To use the helper functions, the
functionsfeature must be enabled.
🧩 Editor Support
- tree-sitter-rshtml: Provides robust and efficient parsing for accurate syntax highlighting and code analysis.
- Language Server: Provides core features like autocompletion, syntax highlighting and error checking.
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
RsHtmlis 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:
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 < and >, 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><p>This is <strong>bold</strong> text.</p></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.
- With an Alias: as ComponentName lets you assign a specific, easy-to-use name for the component within the current template.
- Without an Alias: If you omit as, RsHtml will automatically use
the component’s file name (without the extension) as its name.
For example,
@use "components/Alert.rs.html"makes the component available asAlert.
@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)
- Self-Closing: If a component doesn’t need any child content, you can use a
self-closing tag:
<MyComponent/>. - With Child Content: To pass content to be rendered by
@child_content, use standard opening and closing tags.
@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:
- String Literals:
title="Hello World" - Numbers:
count=42orprice=99.9 - Booleans:
is_active=true - Rust Expressions:
user=@self.current_useroritems=@(vec![1, 2, 3]) - Template Blocks:
header={ <h3>My Header</h3> }
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.