pomade

Add a little Gleam to your Mustache

Package Version Hex Docs

What

pomade is a Mustache library written in Gleam. I made it primarily because I couldn’t find a pure Gleam implementation of the Mustache templating language, but also because I wanted to make something fun and learn Gleam in the process.

Compatibility

Version 1.1.x fully supports the required portions of the Mustache specification:

It additionally supports the following optional features:

pomade passes the full suite of tests for these features.

The optional, as-of-yet unsupported aspects are:

How

Installing it

gleam add pomade@1

Using it

First, import the required libraries:

import gleam/dict
import gleam/string_tree.{type StringTree}

import pomade

In addition to the rendering and compilation API, pomade provides support for heterogeneous hashmaps and values, which Mustache requires as input.

Rendering templates

To render a template, compile its source first:

pub fn precompile() -> Result(StringTree, pomade.Error) {
  let template_source = "Hello, {{target}}!"
  let assert Ok(template) = pomade.compile(template_source)
  render_compiled(template)
}

You can then expand the pre-compiled template:

pub fn render_compiled(
  template: pomade.Template,
) -> Result(StringTree, pomade.Error) {
  let data = pomade.dict(dict.from_list([#("target", pomade.string("world"))]))
  pomade.render(template, data, dict.new())
  // -> Ok(StringTree)
  // -> "Hello, world!"
}

Using partials

You can also render templates with partials, by passing a Dict(String, String) in the partials argument, mapping a partial name to the source of that partial:

pub fn render_with_partials() -> Result(StringTree, pomade.Error) {
  let template_source = "Fly, you {{>other_template}}!"
  let assert Ok(template) = pomade.compile(template_source)
  let data =
    pomade.dict(dict.from_list([#("adjective", pomade.string("fools"))]))
  let partials = dict.from_list([#("other_template", "{{adjective}}")])
  pomade.render(template, data, partials)
  // -> Ok(StringTree)
  // -> "Fly, you fools!"
}

pomade takes care of caching the compiled partial, in a way that’s compliant with the Mustache spec, to save on the costs of compiling the template at runtime every time it’s needed.

Additionally, you can use dynamic names, by prefixing a path with *, which will resolve the name of the partial from whatever value is contained at that path, in the input data:

pub fn render_partials_with_dynamic_names() -> Result(StringTree, pomade.Error) {
  let template_source = "1.21 {{>*dig.down}}!"
  let assert Ok(template) = pomade.compile(template_source)
  let data =
    pomade.dict(
      dict.from_list([
        #(
          "dig",
          pomade.dict(
            dict.from_list([#("down", pomade.string("partial_name"))]),
          ),
        ),
        #("unit", pomade.string("gigawatts")),
      ]),
    )
  let partials = dict.from_list([#("partial_name", "{{unit}}")])
  pomade.render(template, data, partials)
  // -> Ok(StringTree)
  // -> "1.21 gigawatts!"
}

Returning rendered Strings

By default, the API returns StringTrees, but there’s a version that returns Strings:

pub fn render_to_string() -> Result(String, pomade.Error) {
  let template_source = "Goodbye, {{target}}"
  let assert Ok(template) = pomade.compile(template_source)
  let data = pomade.dict(dict.from_list([#("target", pomade.string("horses"))]))
  pomade.render_string(template, data, dict.new())
  // -> Ok("Goodbye, horses") 
}

Using the convenience functions

If you anticipate that you won’t be rendering a template often during the lifespan of your program (as in, for example, a one-off batch program), you can compile and render templates in a single call:

pub fn convenience_at_your_fingertips() -> Result(StringTree, pomade.Error) {
  let template_source = "No. I am your {{relative}}."
  let data =
    pomade.dict(dict.from_list([#("relative", pomade.string("father"))]))
  pomade.compile_and_render(template_source, data, dict.new())
  // -> Ok(StringTree)
  // -> "No. I am your father."
  //
  // Or:
  // pomade.compile_and_render_string(template_source, data, dict.new())
  // -> Ok("No. I am your father.")
}

Information for nerds

Working on it

First, ensure that you’ve fetched the git submodules locally, so that the unit tests can find the Mustache specification test suite:

git submodule update --init

Then, you can run:

gleam test

Or whatever.

Benchmarking it

pomade comes with a few Glychee benchmarks, shamelessly lifted (ported) from the repository of the Ruby version of Mustache. You can find them under dev/pomade, and you can run them like so:

gleam run -m "pomade/compile_template_benchmark"

Results vs bbmustache (via chaplin)

With the following hardware and configuration:

Operating System: macOS
CPU Information: Apple M1
Number of Available Cores: 8
Available memory: 8 GB
Elixir 1.20.4
Erlang 29.1.1
JIT enabled: true

Benchmark suite executing with the following configuration:
warmup: 4 s
time: 4 s
memory time: 8 s
reduction time: 4 s
parallel: 1
inputs: none specified
Estimated total run time: 20 s
Excluding outliers: false

We have the following benchmarks:

Compile template benchmark
Name                                ips        average  deviation         median         99th %
pomade: compile template        80.00 K       12.50 μs    ±19.23%       11.92 μs       17.25 μs
chaplin: compile template       41.72 K       23.97 μs    ±11.32%       23.58 μs          31 μs

Comparison:
pomade: compile template        80.00 K
chaplin: compile template       41.72 K - 1.92x slower +11.47 μs

Memory usage statistics:

Name                         Memory usage
pomade: compile template         27.03 KB
chaplin: compile template         5.68 KB - 0.21x memory usage -21.35156 KB

**All measurements for memory usage were the same**

Reduction count statistics:

Name                      Reduction count
pomade: compile template           1.49 K
chaplin: compile template          0.40 K - 0.26x reduction count -1.09600 K

**All measurements for reduction count were the same**
Render template without HTML escaping
Name                           ips        average  deviation         median         99th %
pomade: html escape        85.57 K       11.69 μs    ±19.94%          11 μs          16 μs
chaplin: html escape       42.53 K       23.51 μs    ±20.85%       22.71 μs       35.29 μs

Comparison:
pomade: html escape        85.57 K
chaplin: html escape       42.53 K - 2.01x slower +11.83 μs

Memory usage statistics:

Name                    Memory usage
pomade: html escape         23.10 KB
chaplin: html escape         7.23 KB - 0.31x memory usage -15.87500 KB

**All measurements for memory usage were the same**

Reduction count statistics:

Name                 Reduction count
pomade: html escape           1.34 K
chaplin: html escape          0.65 K - 0.49x reduction count -0.68400 K

**All measurements for reduction count were the same**
Render template with HTML escaping
Name                           ips        average  deviation         median         99th %
pomade: html escape        74.58 K       13.41 μs    ±29.24%       12.46 μs       20.71 μs
chaplin: html escape       40.05 K       24.97 μs    ±12.63%       24.17 μs       37.71 μs

Comparison:
pomade: html escape        74.58 K
chaplin: html escape       40.05 K - 1.86x slower +11.56 μs

Memory usage statistics:

Name                    Memory usage
pomade: html escape         23.10 KB
chaplin: html escape         8.45 KB - 0.37x memory usage -14.65625 KB

**All measurements for memory usage were the same**

Reduction count statistics:

Name                 Reduction count
pomade: html escape           1.41 K
chaplin: html escape          0.90 K - 0.64x reduction count -0.50800 K

**All measurements for reduction count were the same**
Render pre-compiled template with a collection with 1000 items
Name                                 ips        average  deviation         median         99th %
pomade: render collection         1.63 K        0.61 ms    ±10.54%        0.59 ms        0.81 ms
chaplin: render collection        0.33 K        3.01 ms     ±3.77%        3.01 ms        3.25 ms

Comparison:
pomade: render collection         1.63 K
chaplin: render collection        0.33 K - 4.91x slower +2.39 ms

Memory usage statistics:

Name                          Memory usage
pomade: render collection          0.76 MB
chaplin: render collection         2.98 MB - 3.95x memory usage +2.23 MB

**All measurements for memory usage were the same**

Reduction count statistics:

Name                       Reduction count
pomade: render collection         126.34 K
chaplin: render collection        376.78 K - 2.98x reduction count +250.44 K

**All measurements for reduction count were the same**
Render pre-compiled template with a collection with 1000 partials
Name                      ips        average  deviation         median         99th %
render partials        3.94 K      253.57 μs    ±16.24%      243.69 μs      432.65 μs

Memory usage statistics:

Name               Memory usage
render partials       498.34 KB

**All measurements for memory usage were the same**

Reduction count statistics:

Name            Reduction count
render partials         62.93 K

**All measurements for reduction count were the same**

License

pomade is distributed under the MIT License. You can find more details in LICENSE.

Important

You won’t find any AI slop here.

✨ Search Document