Crates.io | rust_html |
lib.rs | rust_html |
version | 1.1.5 |
source | src |
created_at | 2024-12-01 20:41:24.115956 |
updated_at | 2024-12-03 22:17:41.38721 |
description | Minimal compile-safe HTML templating library |
homepage | https://github.com/evgiz/rust_html |
repository | https://github.com/evgiz/rust_html |
max_upload_size | |
id | 1467816 |
size | 82,958 |
A HTML templating library for reusable components in web applications
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);
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.
Install from crates.io using cargo:
cargo add rust_html
The library has only 5 exported functions/types:
rhtml!
: The main macro for creating templatesTemplate
: represents a reusable HTML templateRender
: trait for implementing reusable struct
componentsUnescaped
: string wrapper for inserting unescaped valuesTemplateGroup
: wrapper to insert a Vec<Template>
[!NOTE]
TheTemplate
struct itself does not implement theDisplay
trait. To print or return the HTML value as aString
you can useString::from(my_template)
or justmy_template.into()
where applicable.
rhtml!
macroThe 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 }}
).
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>
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
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>
"#};
}
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><script>alert('hi')</script></div>
If you need the unescaped value, you can use the Unescaped
wrapper.
[!CAUTION]
Never useUnescaped
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>
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.
Look at the AWWY website for more examples.
Run tests:
cargo test -p rust_html_tests
Run doc tests:
cargo test -p rust_html_macros