Skip to main content

Templating with ATL

ATL — the Archetect Template Language — is the engine that turns your archetype's template files into a rendered project. If you have used Jinja2, Liquid, or any {{ mustache }}-flavored template language, ATL will feel immediately familiar:

[package]
name = "{{ project_name }}"
description = "{{ description | default(project_name) }}"

This section is a practical guide to writing templates. For the complete, exhaustive syntax and filter listings, see the ATL reference.

Where templates live

Templates are just ordinary files inside your archetype. There is no special extension requirement and no registration step — any directory in your archetype can be rendered as a template tree by calling directory.render from your archetype.lua:

directory.render("contents", context)

By convention, most archetypes keep their main template tree under contents/, with shared fragments under includes/:

my-archetype/
├── archetype.yaml
├── archetype.lua
├── contents/ # rendered by directory.render("contents", ctx)
│ └── {{ project_name }}/ # directory NAMES are templates too
│ ├── Cargo.toml # file contents are templates
│ └── src/
│ └── main.rs
└── includes/ # partials reachable via {% include %}
└── license-header.atl

Everything is a template

When Archetect renders a directory tree, both file contents and file/directory names pass through ATL:

  • A directory literally named {{ project_name }} renders to my_tool (or however the user's answer is cased under that key).
  • Every text file's contents are rendered with the same context.
  • Binary files (images, jars, archives...) are detected automatically and copied through untouched — though their names are still rendered. See Organizing Templates for details.

The context is flat

Templates receive a flat set of variables — you write {{ project_name }}, never context.project_name. Whatever your script puts into the Context is available by name.

Better still, case expansion means a single prompt typically lands in the context under several spellings, each key cased like its value:

KeyValue
project-namemy-tool
project_namemy_tool
projectNamemyTool
ProjectNameMyTool
Project-NameMy-Tool
PROJECT_NAMEMY_TOOL

So instead of applying case filters everywhere, you usually just pick the key whose casing matches the spot you're writing:

pub struct {{ ProjectName }}Config {
// lives in {{ project_name }}/src/config.rs
}

A quick taste

{# Comments never appear in output #}
[workspace]
members = [
".",
{% for m in workspace_members %}
"{{ m }}",
{% endfor %}
]

{% if use_docker %}
# See docker/README.md for container workflow
{% endif %}

authors = ["{{ author_full }}"]
description = "{{ description | default(project_name) }}"

Three delimiters do all the work:

  • {{ ... }} — expressions: variables, filters, function calls
  • {% ... %} — statements: conditionals, loops, includes
  • {# ... #} — comments, stripped from output

Where to go next