Skip to content

Writing Templates

Templates are Jinja2 files that define the output format for your generated code. embgen automatically discovers templates based on their naming convention.

Template Naming Convention

Templates must follow this naming pattern:

template.<extension>.j2

Examples:

Template File Output Extension CLI Flag
template.h.j2 .h --h
template.py.j2 .py --py
template.md.j2 .md --md
template.c.j2 .c --c
template.rs.j2 .rs --rs

Template Location

Templates must be in a templates/ subdirectory of your domain:

mydomain/
├── __init__.py
├── generator.py
├── models.py
└── templates/
    ├── template.h.j2
    ├── template.py.j2
    └── template.md.j2

Available Context

Your generator's render() method determines what variables are available in templates:

def render(self, config: Any, template: Template) -> str:
    return template.render(
        config=cfg,           # The full config object
        name=cfg.name,        # Individual fields
        items=cfg.items,
        generated_on="...",   # Extra context
    )

Common Variables

Most generators provide:

Variable Type Description
config Your config class Full configuration object
name str Configuration name
generated_on str Timestamp

Jinja2 Basics

Variables

{{ config.name }}
{{ item.description }}

Loops

{% for item in config.items %}
{{ item.name }} = {{ item.value }}
{% endfor %}

Conditionals

{% if item.description %}
// {{ item.description }}
{% endif %}

{% if item.optional %}
{{ item.name }}?
{% else %}
{{ item.name }}
{% endif %}

Filters

{{ name | upper }}          {# MYNAME #}
{{ name | lower }}          {# myname #}
{{ name | title }}          {# Myname #}
{{ items | length }}        {# 5 #}
{{ items | first }}         {# First item #}
{{ items | last }}          {# Last item #}
{{ value | default(0) }}    {# Use 0 if value is undefined #}

Template Examples

C Header Template

// {{ name }} - Generated by embgen
// DO NOT EDIT - Generated on {{ generated_on }}

#ifndef {{ name | upper }}_H
#define {{ name | upper }}_H

#include <stdint.h>
#include <stdbool.h>

{% for item in config.items %}
{% if item.description %}
/** {{ item.description }} */
{% endif %}
#define {{ name | upper }}_{{ item.name | upper }} {{ item.value }}
{% endfor %}

{% for item in config.items %}
{% if item.enums %}
typedef enum {
{% for e in item.enums %}
    {{ item.name | upper }}_{{ e.name | upper }} = {{ e.value }},{% if e.description %} /**< {{ e.description }} */{% endif %}

{% endfor %}
} {{ item.name | title }}Enum;

{% endif %}
{% endfor %}

#endif // {{ name | upper }}_H

Python Template

"""{{ name }} - Generated by embgen.

DO NOT EDIT - Generated on {{ generated_on }}.
"""

from dataclasses import dataclass
from enum import IntEnum
{% if config.items | selectattr("enums") | list %}

{% for item in config.items %}
{% if item.enums %}
class {{ item.name | title }}(IntEnum):
    """{{ item.description or item.name }}"""
{% for e in item.enums %}
    {{ e.name }} = {{ e.value }}{% if e.description %}  # {{ e.description }}{% endif %}

{% endfor %}

{% endif %}
{% endfor %}
{% endif %}

{% for item in config.items %}
@dataclass
class {{ item.name | title }}:
    """{{ item.description or item.name }}"""

    value: int = {{ item.value }}
{% if item.enums %}
    enum: {{ item.name | title }} = {{ item.name | title }}.{{ item.enums[0].name }}
{% endif %}

{% endfor %}

Markdown Template

# {{ name }}

> Generated by embgen on {{ generated_on }}

## Overview

{{ config.description or "No description provided." }}

## Items

| Name | Value | Description |
| ---- | ----- | ----------- |
{% for item in config.items %}
| `{{ item.name }}` | {{ item.value }} | {{ item.description or "-" }} |
{% endfor %}

{% for item in config.items %}
{% if item.enums %}
### {{ item.name }} Values

| Name | Value | Description |
| ---- | ----- | ----------- |
{% for e in item.enums %}
| `{{ e.name }}` | {{ e.value }} | {{ e.description or "-" }} |
{% endfor %}

{% endif %}
{% endfor %}

Advanced Jinja2 Features

Whitespace Control

Use - to strip whitespace:

{%- for item in items -%}
{{ item.name }}
{%- endfor -%}

embgen configures Jinja2 with trim_blocks=True and lstrip_blocks=True by default.

Macros

Define reusable template functions:

{% macro type_to_c(type_name) -%}
{% if type_name == "uint8" %}uint8_t
{% elif type_name == "uint16" %}uint16_t
{% elif type_name == "uint32" %}uint32_t
{% elif type_name == "string" %}char*
{% else %}void*
{% endif %}
{%- endmacro %}

typedef struct {
{% for field in item.fields %}
    {{ type_to_c(field.type) }} {{ field.name }};
{% endfor %}
} {{ item.name }};

Includes

Split templates into reusable parts:

templates/
├── template.h.j2
├── _macros.h.j2      # Shared macros (not discovered as output)
└── _types.h.j2       # Shared type definitions
{% include "_macros.h.j2" %}
{% include "_types.h.j2" %}

// Main template content...

Note

Files starting with _ are not discovered as output templates.

Set Variables

{% set sorted_items = config.items | sort(attribute="value") %}
{% for item in sorted_items %}
...
{% endfor %}

Loop Variables

{% for item in items %}
{{ loop.index }}      {# 1-indexed counter #}
{{ loop.index0 }}     {# 0-indexed counter #}
{{ loop.first }}      {# True if first iteration #}
{{ loop.last }}       {# True if last iteration #}
{{ loop.length }}     {# Total number of items #}
{% endfor %}

Custom Filters

You can add custom filters in your generator:

def render(self, config: Any, template: Template) -> str:
    # Add custom filter to environment
    template.environment.filters["to_snake_case"] = self._to_snake_case
    return template.render(config=cfg)

@staticmethod
def _to_snake_case(s: str) -> str:
    """Convert CamelCase to snake_case."""
    import re
    return re.sub(r'(?<!^)(?=[A-Z])', '_', s).lower()

Usage in template:

{{ "MyClassName" | to_snake_case }}  {# my_class_name #}

Tips and Best Practices

1. Use Descriptive Comments

{# Generate enum definitions #}
{% for item in items %}
...
{% endfor %}

2. Handle Optional Fields

{% if item.description %}
/** {{ item.description }} */
{% endif %}

3. Use Default Values

{{ item.description | default("No description") }}

4. Sort for Consistent Output

{% for item in config.items | sort(attribute="name") %}
...
{% endfor %}

5. Format Long Lines

{# Use multiple lines for complex expressions #}
{% set filtered_items = config.items 
    | selectattr("enabled", "true") 
    | sort(attribute="name") 
    | list %}