<div align="center">
<img src="https://gitlab.com/encre-org/pochoir/-/raw/main/.assets/logo.svg" />
<h1>pochoir</h1>
<p>A modern mustache template engine featuring a custom server-side Web Components implementation</p>
<a href="https://gitlab.com/encre-org/pochoir/blob/main/LICENSE">
<img alt="MIT License" src="https://img.shields.io/badge/license-MIT-success" />
</a>
<a href="https://gitlab.com/encre-org/pochoir/-/pipelines">
<img alt="Pipeline status" src="https://gitlab.com/encre-org/pochoir/badges/main/pipeline.svg" />
</a>
<a href="https://deps.rs/repo/gitlab/encre-org/pochoir">
<img alt="Dependency status" src="https://deps.rs/repo/gitlab/encre-org/pochoir/status.svg" />
</a>
<a href="https://crates.io/crates/pochoir">
<img alt="Published on crates.io" src="https://img.shields.io/crates/v/pochoir" />
</a>
<a href="https://docs.rs/pochoir">
<img alt="Documentation on docs.rs" src="https://img.shields.io/docsrs/pochoir" />
</a>
</div>
### Features
- Contains a Django-inspired mustache templating engine to embed data
- Contains a custom-designed language with a good Rust compatibility to manipulate
the data used in expressions
- Contains an HTML parser to do some transformations to the pages
- Contains a component system with support for properties, slots,
defining components using `<template>` elements
- Contains a modular API, it is easy to transform the HTML tree with the
`Transformer` API
- Contains several error formatters for parsing and interpreting errors to
improve DX
### Getting started
Add `pochoir` to your `Cargo.toml`:
```toml
[dependencies]
pochoir = "0.15.1"
```
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:
```rust ,ignore
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.
```rust ,ignore
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:
```rust
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.
```rust ,ignore
use pochoir::{Context, ComponentFile, error};
use std::path::Path;
// 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)),
})
})?;
```
### Extensions
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](https://docs.rs/pochoir/latest/pochoir/transformers/index.html).
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](https://docs.rs/pochoir-lang/latest/pochoir_lang/#functions-and-rust-integration).
### Command line interface
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.
### Where to go next?
- Check out [the examples](https://gitlab.com/encre-org/pochoir/-/tree/main/crates/pochoir/examples) to better know what is possible to do with `pochoir`
- Learn the [syntax of components](https://docs.rs/pochoir/latest/pochoir/compiler/index.html) to start making dynamic and composable templates
- Go to the [API reference](https://docs.rs/pochoir) to understand better how the pieces fit together
### Organization
This Cargo workspace contains 8 crates:
- `pochoir-common`: defines some utilities shared between all the other crates
- `pochoir-parser`: a full HTML parser with support for expressions and statements
- `pochoir-lang`: a parser and interpreter for the custom language used in expressions
- `pochoir-template-engine`: a template engine replacing expressions and statements with real content
- `pochoir-macros`: defines derive macros used to implement `FromValue` and `IntoValue` automagically
- `pochoir-extra`: contains some extra `Transformer`s like `EnhancedCss` or `AccessibilityChecker`
- `pochoir-cli`: the binary for the command line interface
- `pochoir`: the main crate exporting all other low-level crates (not `pochoir-extra`) and defining the component system compiler
### About the name
`pochoir` means `stencil` in French and a stencil contains holes which are
filled with something, like a template engine or a component system!
### License
`pochoir` is published under the [MIT license](https://gitlab.com/encre-org/pochoir/blob/main/LICENSE).