1 unstable release
Uses new Rust 2024
| new 0.1.0 | Jan 22, 2026 |
|---|
#112 in Template engine
80KB
1.5K
SLoC
plait
A fast, type-safe HTML templating library for Rust.
Plait provides compile-time HTML generation with a familiar, JSX-like syntax. Templates are checked at compile time and generate efficient code with minimal runtime overhead.
Quick Start
use plait::html;
let name = "World";
let output = html!(
div class="greeting" {
h1 { "Hello, " (name) "!" }
}
);
assert_eq!(&*output, r#"<div class="greeting"><h1>Hello, World!</h1></div>"#);
Macros
Plait provides four main macros:
html!- Creates anHtmlvalue from a templatecomponent!- Creates a reusable component that renders lazilyrender!- Renders content to an existingHtmlFormatterattrs!- Creates anAttributescollection
Template Syntax
Elements
Elements are written as name { children } for normal elements or name; for void elements:
use plait::html;
let output = html!(
div {
p { "A paragraph" }
br;
input type="text" name="field";
}
);
assert_eq!(&*output, r#"<div><p>A paragraph</p><br><input type="text" name="field"></div>"#);
Attributes
Attributes support several value types:
use plait::html;
let class_name = "container";
let maybe_id: Option<&str> = Some("main");
let is_disabled = true;
let output = html!(
div
class="literal" // Literal string
data-value=(class_name) // Dynamic expression
id=[maybe_id] // Optional (renders if Some)
disabled?[is_disabled] // Boolean (renders if true)
{
"content"
}
);
assert_eq!(&*output, r#"<div class="literal" data-value="container" id="main" disabled>content</div>"#);
Dynamic Content
Expressions in parentheses are escaped by default:
use plait::html;
let user_input = "<script>alert('xss')</script>";
let output = html!(
div { (user_input) }
);
// Content is safely escaped
assert!(!output.contains("<script>"));
Use : raw to include pre-escaped content:
use plait::html;
let trusted_html = "<strong>Bold</strong>";
let output = html!(
div { (trusted_html : raw) }
);
assert!(output.contains("<strong>"));
Control Flow
Conditionals
use plait::html;
let show = true;
let value: Option<&str> = Some("hello");
let output = html!(
div {
@if show {
span { "Visible" }
}
@if let Some(v) = value {
span { (v) }
} @else {
span { "No value" }
}
}
);
assert_eq!(&*output, r#"<div><span>Visible</span><span>hello</span></div>"#);
Loops
use plait::html;
let items = vec!["one", "two", "three"];
let output = html!(
ul {
@for item in &items {
li { (item) }
}
}
);
assert_eq!(&*output, r#"<ul><li>one</li><li>two</li><li>three</li></ul>"#);
Match Expressions
use plait::html;
enum Status { Active, Inactive }
let status = Status::Active;
let output = html!(
span {
@match status {
Status::Active => "Online",
Status::Inactive => "Offline",
}
}
);
assert_eq!(&*output, r#"<span>Online</span>"#);
Custom Components
Use the component! macro to create reusable components as functions:
use plait::{Render, component};
fn button(label: &str, primary: bool) -> impl Render + '_ {
let class = if primary { "btn btn-primary" } else { "btn" };
component! {
button class=(class) { (label) }
}
}
// Use in templates
let output = plait::html!(
div { (button("Click me", true)) }
);
assert_eq!(&*output, r#"<div><button class="btn btn-primary">Click me</button></div>"#);
For components with owned data, use (&value) to borrow:
use plait::{Render, component};
fn greeting(message: String) -> impl Render {
component! {
span { (&message) }
}
}
For more control, implement the Render trait directly. See the Render documentation for details.
Safety
Plait automatically escapes dynamic content to prevent XSS vulnerabilities. The Html and PreEscaped types
represent content that is already safe and will not be escaped again.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Dependencies
~1.1–1.8MB
~35K SLoC