Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/michaeljones/matcha

🍵 A template system for Gleam
https://github.com/michaeljones/matcha

gleam-lang

Last synced: about 2 months ago
JSON representation

🍵 A template system for Gleam

Awesome Lists containing this project

README

        

# Matcha 🍵

Generate type-safe Gleam modules from text-based template files.

This project provides a Rust program that parses a basic template format and outputs Gleam modules
with 'render' functions that can be imported and called to render the template with different
parameters.

## Status

Matcha is basic but currently feature complete and well tested. If the repository looks inactive it
is because it is stable.

## Recommended Usage

Template languages like Matcha are useful to have in general and are particularly useful for generating unstructured
text. Matcha generates well typed code that avoids issues that some more dynamic templating systems have.

That said, if you're planning to generate structured output, it is sensible to use a better suited approach. For data
formats, this should be serialization and deserialization libraries. For formats like HTML, you would likely be better
using a library like [lustre](https://hexdocs.pm/lustre/lustre/element.html#to_string_builder) or
[nakai](https://github.com/nakaixo/nakai).

## Installation

Download pre-built binaries for the latest release from the
[Releases](https://github.com/michaeljones/matcha/releases) page.

Build from source with:

```
cargo install --path .
```

## Usage

Run:

```
matcha
```

At the root of your project and it will walk your project folder structure and compile any template
files it finds.

Template files should have a `.matcha` extension. Templates are compiled into `.gleam` files that can
be imported like any other regular module. The modules expose a `render` function, that returns a
`String`, and `render_builder` function that returns a `StringBuilder`.

Some errors, mostly syntax, will be picked up by the Rust code but it is possible to generate
invalid modules and so the Gleam compiler will pick up further errors.

## Syntax

The syntax is inspired by [Jinja](https://jinja.palletsprojects.com/).

### With

You can use `{>` syntax to add `with` statements to declare parameters and their assoicated types
for the template and the generated render function. All parameters must be declared with `with`
statements.

```
{> with greeting as String
{> with name as String

{{ greeting }}, {{ name }}
```

### String Value

You can use `{{ name }}` syntax to insert the value of `name` into the rendered template.

```jinja
{> with name as String
Hello {{ name }}
```

### String Builder Value

You can use `{[ name ]}` syntax to insert a string builder value into the rendered template. This
has the advantage of using
[string_builder.append_builder](https://hexdocs.pm/gleam_stdlib/gleam/string_builder.html#append_builder)
in the rendered template and so it more efficient for inserting content that is already in a
`StringBuilder`. This can be used to insert content from another template.

```jinja
{> with name as StringBuilder
{[ name ]}
```

### If

You can use `{% %}` blocks to create an if-statement using the `if`, `else` and `endif` keywords.
The `else` is optional.

```jinja
{> with is_admin as Bool
{% if is_admin %}Admin{% else %}User{% endif %}
```

### For

You can use `{% %}` blocks to create for-loops using the `for`, `in` and `endfor` keywords.

```html+jinja
{> with list as List(String)


    {% for entry in list %}
  • {{ entry }}

  • {% endfor %}

```

Additionally you can use the `as` keyword to associate a type with the items being iterated over.
This is necessary if you're using a complex object.

```html+jinja
{> import organisation.{type Organisation}
{> import membership.{type Member}
{> with org as Organisation


    {% for user as Member in organisation.members %}
  • {{ user.name }}

  • {% endfor %}

```

### Import

You can use the `{>` syntax to add import statements to the template. These are used to import types
to use with the `with` syntax below to help Gleam check variables used in the template.

```
{> import my_user.{type MyUser}
```

### Functions

You can use the `{> fn ... {> endfn` syntax to add a local function to your template:

```
{> fn full_name(second_name: String)
Lucy {{ second_name }}
{> endfn
```

The function always returns a `StringBuilder` value so you must use `{[ ... ]}` syntax to insert
them into templates. The function body has its last new line trimmed, so the above function called
as `full_name("Gleam")` would result in `Lucy Gleam` and not `\nLucy Gleam\n` or any other
variation. If you want a trailing new line in the output then add an extra blank line before the `{> endfn`.

The function declaration has no impact on the final template as all lines are removed from the
final text.

Like in normal code, functions make it easier to deal with repeated components within your template.

```
{> fn item(name: String)

  • {{ name }}

  • {> endfn


      {[ item(name: "Alice") ]}
      {[ item(name: "Bob") ]}
      {[ item(name: "Cary") ]}

    ```

    You can use the `pub` keyword to declare the function as public in which case other modules will be
    able to import it from gleam module compiled from the template.

    ```
    {> pub fn user_item(name: String)

  • {{ name }}

  • {> endfn
    ```

    If a template only includes function declarations and no meaningful template content then matcha
    will not add the `render` and `render_builder`. Instead the module will act as a library of
    functions where each function body is a template.

    ## Output

    A template like:

    ```
    {> import my_user.{type User}
    {> with user_obj as User
    Hello{% if user_obj.is_admin %} Admin{% endif %}
    ```

    is compiled to a Gleam module:

    ```gleam
    import gleam/string_builder.{StringBuilder}
    import gleam/list
    import my_user.{User}

    pub fn render_builder(user_obj user_obj: User) -> StringBuilder {
    let builder = string_builder.from_string("")
    let builder = string_builder.append(builder, "Hello")
    let builder = case user_obj.is_admin {
    True -> {
    let builder = string_builder.append(builder, " Admin")
    builder
    }
    False -> builder
    }
    let builder = string_builder.append(builder, "
    ")

    builder
    }

    pub fn render(user_obj user_obj: User) -> String {
    string_builder.to_string(render_builder(user_obj: user_obj))
    }
    ```

    Which you can import and call `render` or `render_builder` on with the appropriate arguments.

    ## Tests

    Rust tests can be run with `cargo test`. They use [insta](http://insta.rs/) for snapshots.

    Gleam tests can be run with `cargo run && gleam test`.