Crates.io | pochoir |
lib.rs | pochoir |
version | 0.12.2 |
created_at | 2025-07-04 19:35:25.195807+00 |
updated_at | 2025-08-10 16:42:22.179556+00 |
description | Main crate of the pochoir template engine used to compile and render pochoir files with components |
homepage | |
repository | https://gitlab.com/encre-org/pochoir.git |
max_upload_size | |
id | 1738398 |
size | 242,221 |
A modern mustache template engine featuring a custom server-side Web Components implementation
<template>
elementsTransformer
APIAdd pochoir
to your Cargo.toml
:
[dependencies]
pochoir = "0.12.2"
Then, you need to choose a way to get your source HTML files. You can use
pre-defined providers to do that. For example, the FilesystemProvider
gets
the source HTML from the files in a directory:
use pochoir::{Context, FilesystemProvider};
// The `FilesystemProvider` selects files using two criterias: if they have a
// known extension (they can be configured, by default just `html` files can be
// used) and if they are in one of the inserted path. Here all files in the
// `templates` directory having a `.html` extension will be used
let provider = FilesystemProvider::new().with_path("templates");
let mut context = Context::new();
let _html = provider.compile("index", &mut context)?;
And the StaticMapProvider
stores source files in a map with the component name as key.
use pochoir::{Context, StaticMapProvider};
// The last argument is the path to the file if it was read from
// the filesystem, it is used in error messages to find the HTML file source
let provider = StaticMapProvider::new().with_template("index", "<h1>Index page</h1>", None);
let mut context = Context::new();
let _html = provider.compile("index", &mut context)?;
You can quickly do some really complex things using these providers:
use pochoir::{object, Context, Function, StaticMapProvider, error};
// 1. Declare your sources: they don't need to be static, in a real world usage,
// they would be fetched from the filesystem or from a database.
let provider = StaticMapProvider::new()
.with_template("index", r#"
<main>
<h1>Hi 👋, this is my blog!</h1>
<p>I'm interested in cars, cats and caps.
This is a list of my latest blog posts:</p>
<ul>
{% for post in posts %}
<li><ui-card post="{{ post }}" /></li>
{% endfor %}
</ul>
</main>"#, None)
.with_template("ui-card", r#"
<a href="/posts/{{ slugify(post.title) }}" class="card">
<div class="card-thumbnail">
<img src="{{ post.thumbnail }}">
</div>
<div class="card-footer">
<h5>{{ post.title }}</h5>
<p>{{ truncate(post.description, 80) }}</p>
</div>
</a>"#, None);
// 2. Then define the context (all the variables used in expressions) using Rust
// types, they will be automagically transformed to values used in the
// language (without being serialized!) using the `IntoValue` trait
let mut context = Context::new();
context.insert("posts", vec![
// Constructing objects is done using a macro or with methods of the `Object` structure
object! {
"title" => "A beautiful castle",
"description" => "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"thumbnail" => "https://picsum.photos/seed/41/300",
},
object! {
"title" => "A beautiful beach",
"description" => "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"thumbnail" => "https://picsum.photos/seed/44/300",
},
]);
// 3. Rust functions can also be included in the Context and called in the
// expressions. Here `insert_inherited` is used in order for the component
// `ui-card` to inherit this function from the parent template. Arguments are
// normal Rust types which are converted automagically from the values used in
// the language using the `FromValue` trait
context.insert_inherited("truncate", Function::new(|value: String, max: usize| {
Ok(if value.len() <= max {
value
} else {
value[..max].to_string()
})
}));
// 4. Finally, compile the whole template into a single HTML string. When an error happens,
// a complete, formatted error is displayed in the terminal using ANSI escape codes
let _html = provider
.compile("index", &mut context)
.map_err(|e| {
error::display_ansi_error(
&e,
&provider.get(e.component_name())
.expect("component should not be removed from the provider")
.data,
)
});
But if you want more control over how the source files are fetched, you can use
the closure API by directly using the pochoir::compile
function. The closure
takes the name of the component and returns a ComponentFile
with the file name and
data.
use pochoir::{Context, ComponentFile, error};
use std::path::Path;
let html = pochoir::compile("index", &mut Context::new(), |name| {
// In a real world usage, you would fetch the HTML from a complex, dynamic,
// pipeline of sources, maybe from the network.
// A `ComponentFile` is used to associate some data with a path to a file,
// if it was fetched from the filesystem. You can use
// `ComponentFile::new_inline` if you don't want to provide a path, in this
// case the path will simply be `inline`
Ok(match name {
"index" => ComponentFile::new_inline("<h1>Index page</h1><my-button />"),
"my-button" => ComponentFile::new(Path::new("my-button.html"), "<button>Click me!</button>"),
_ => return Err(error::component_not_found(name)),
})
})?;
The main way of extending pochoir
is by using transformers. They are used
to transform the HTML tree and can be used to do various, repeated tasks. For
example, they can be used to enhance the CSS <style>
elements used in components
by providing scoped CSS, minification, autoprefixing and bundling, like what is
done in the EnhancedCss
structure of the pochoir-extra
crate. Learn more
about transformers in the API reference.
Another way is by defining custom Rust functions that will be inserted using
Context::insert
and used in templates. Learn more
about custom functions in the API reference.
If you want to try pochoir
without worrying about having to start a Rust
project, you can try the CLI by running cargo install --git https://gitlab.com/encre-org/pochoir.git pochoir-cli
.
You can then try to develop some website using live-reloading by running pochoir --watch serve
in a directory containing some HTML files using pochoir
template expressions or by running pochoir build
to build a production version inside a directory. Keep in mind that extra transformers could not be used in the CLI.
pochoir
This Cargo workspace contains 8 crates:
pochoir-common
: defines some utilities shared between all the other cratespochoir-parser
: a full HTML parser with support for expressions and statementspochoir-lang
: a parser and interpreter for the custom language used in expressionspochoir-template-engine
: a template engine replacing expressions and statements with real contentpochoir-macros
: defines derive macros used to implement FromValue
and IntoValue
automagicallypochoir-extra
: contains some extra Transformer
s like EnhancedCss
or AccessibilityChecker
pochoir-cli
: the binary for the command line interfacepochoir
: the main crate exporting all other low-level crates (not pochoir-extra
) and defining the component system compilerpochoir
means stencil
in French and a stencil contains holes which are
filled with something, like a template engine or a component system!
pochoir
is published under the MIT license.