Skip to content

x.templating.dtm2 #

dtm2 - Dynamic Template Manager 2

dtm2 is the modern runtime renderer for Dynamic Template Manager. It keeps the original DTM idea: templates are normal files on disk, so they can be edited without recompiling the application.

The main change from x.templating.dtm is architectural. dtm2 caches parsed template trees, not rendered HTML responses. This keeps rendering fast while removing the old async rendered-cache server from the hot path.

Quick Start

Create a templates/ folder in your application and put your templates inside it. DTM2 supports .html, .htm, .xml, .txt, and .text by default.

import x.templating.dtm2

fn main() {
    mut manager := dtm2.initialize(
        template_dir: 'templates'
    )
    placeholders := {
        'title': 'DTM2'
        'body':  '<strong>escaped by default</strong>'
    }
    rendered := manager.expand('page.html', placeholders: &placeholders)
    println(rendered)
}

Example template:

<!doctype html>
<html>
    <head>
        <title>@title</title>
    </head>
    <body>
        <main>@body</main>
    </body>
</html>

The rendered @body value is escaped by default.

Veb Example

import veb
import x.templating.dtm2

pub struct App {
pub mut:
    templates &dtm2.Manager = unsafe { nil }
}

pub struct Context {
    veb.Context
}

fn main() {
    mut app := &App{
        templates: dtm2.initialize(
            template_dir: 'templates'
        )
    }
    veb.run[App, Context](mut app, 18081)
}

@['/']
pub fn (mut app App) index(mut ctx Context) veb.Result {
    placeholders := {
        'title': 'Home'
        'body':  'Hello from DTM2'
    }
    html := app.templates.expand('index.html', placeholders: &placeholders)
    return ctx.html(html)
}

Available Options

Manager options

dtm2.initialize() accepts:

  • template_dir (string): root directory used for relative template paths. If empty, <executable directory>/templates is used.
  • compress_html (bool): enables a lightweight deterministic HTML whitespace compressor. It is enabled by default.
  • reload_modified_templates (bool): when enabled, DTM2 checks the source template and included files before reusing a parsed template tree. It is enabled by default.
  • extension_config_file (string): optional JSON file containing extension mappings. If empty, DTM2 automatically loads dtm2_extensions.json from the configured template_dir when that file exists.

Example:

mut manager := dtm2.initialize(
    template_dir:              'templates'
    compress_html:             true
    reload_modified_templates: true
)

For maximum hot-path throughput in applications where templates are immutable after startup, you can disable reload checks:

mut manager := dtm2.initialize(
    template_dir:              'templates'
    reload_modified_templates: false
)

The recommended runtime model is one long-lived manager per application or rendering context. Reusing the manager is what keeps parsed templates and path resolution cached.

Template extensions

DTM2 has two rendering modes:

  • TemplateType.html: HTML/XML-like output, with default escaping and optional HTML compression.
  • TemplateType.text: raw text output, also escaped by default.

Default mappings:

  • HTML mode: .html, .htm, .xml
  • Text mode: .txt, .text

Project-specific extensions should be configured with a JSON file.

Example templates/dtm2_extensions.json:

{
    'html': ['.view', '.tmpl'],
    'text': ['.mail', '.md']
}

If the file is named dtm2_extensions.json and is placed directly in the configured template_dir, DTM2 loads it automatically:

mut manager := dtm2.initialize(
    template_dir: 'templates'
)

You can also point to an explicit config file:

mut manager := dtm2.initialize(
    template_dir:           'templates'
    extension_config_file: 'config/my_dtm2_extensions.json'
)

DTM2 ships a default config example that can be copied into your own templates/ directory:

vlib/x/templating/dtm2/dtm2_extensions.json

If the JSON file is absent, invalid, or contains invalid entries, DTM2 keeps the built-in defaults and prints a warning for entries that cannot be registered. JSON extension config files are limited to 64 KB and every extension is validated before being registered.

Render options

manager.expand() accepts:

  • placeholders (&map[string]string): values inserted in the template.
  • missing_placeholder_prefix (string): prefix written when a placeholder is missing. The default is @, preserving the original placeholder text.

Example:

placeholders := {
    'title': 'Example'
}
html := manager.expand('page.html',
    placeholders:               &placeholders
    missing_placeholder_prefix: '@'
)

The Placeholder System

Template placeholders use the @name form.

<h1>@title</h1>
<p>@body</p>

In DTM2, placeholder values are strings:

placeholders := {
    'title': 'Hello'
    'count': '42'
}

Values are escaped by default in both HTML and text templates.

Security note for custom non-HTML formats such as SQL: DTM2 is a template renderer, not a domain-specific sanitizer. By default, it escapes HTML-special characters in placeholder values. It does not make SQL queries safe, does not replace prepared statements, and does not validate business-specific formats. If you add .sql or another sensitive extension through configuration, the security of that generated content remains the responsibility of the application.

Explicit HTML Inclusion

The historical _#includehtml suffix is still supported for compatibility. It allows a placeholder to include a restricted set of HTML tags in .html templates.

placeholders := {
    'body_#includehtml': '<p>allowed</p><script>escaped</script>'
}
html := manager.expand('page.html', placeholders: &placeholders)

The template still uses the normal placeholder name:

<main>@body</main>

DTM2 escapes the complete value first, then restores only allowed tags. This preserves the old opt-in behavior without allowing arbitrary raw HTML through. In .txt templates, HTML is always escaped.

Allowed tags:

<div>, </div>, <h1>, </h1>, <h2>, </h2>, <h3>, </h3>, <h4>, </h4>,
<h5>, </h5>, <h6>, </h6>, <p>, </p>, <br>, <hr>, <span>, </span>,
<ul>, </ul>, <ol>, </ol>, <li>, </li>, <dl>, </dl>, <dt>, </dt>,
<dd>, </dd>, <menu>, </menu>, <table>, </table>, <caption>, </caption>,
<th>, </th>, <tr>, </tr>, <td>, </td>, <thead>, </thead>,
<thread>, </thread>, <tbody>, </tbody>, <tfoot>, </tfoot>, <col>, </col>,
<colgroup>, </colgroup>, <header>, </header>, <footer>, </footer>,
<main>, </main>, <section>, </section>, <article>, </article>,
<aside>, </aside>, <details>, </details>, <dialog>, </dialog>,
<data>, </data>, <summary>, </summary>

Includes

Templates can include other templates with a simple line-level directive:

<header>@include 'partials/nav'</header>
<main>@body</main>

Include paths are resolved relative to the current template. If no file extension is provided, .html is added. The final resolved include path must stay inside the manager template_dir; attempts to include files through ../ or absolute paths outside that root fail.

The same boundary applies to templates passed to expand(). Absolute template paths are accepted only when they resolve inside template_dir.

Included files are tracked as dependencies of the parsed template. When reload_modified_templates is enabled, changing an included file invalidates the cached parsed tree.

Backward Compatibility With DTM v1

Existing code that imports x.templating.dtm is kept source-compatible for the migration period. The v1 facade is deprecated, but it now delegates rendering to DTM2 internally.

That means old code can continue to compile:

import x.templating.dtm

mut manager := dtm.initialize()
mut placeholders := map[string]dtm.DtmMultiTypeMap{}
placeholders['title'] = 'Legacy DTM'
placeholders['count'] = 7
html := manager.expand('page.html', placeholders: &placeholders)

For new code, prefer importing x.templating.dtm2 directly:

import x.templating.dtm2

mut manager := dtm2.initialize(template_dir: 'templates')
placeholders := {
    'title': 'Modern DTM'
    'count': '7'
}
html := manager.expand('page.html', placeholders: &placeholders)

Migration notes:

  • Replace import x.templating.dtm with import x.templating.dtm2.
  • Replace DtmMultiTypeMap placeholder maps with map[string]string.
  • Convert numeric values to strings before rendering.
  • Remove stop_cache_handler() calls; DTM2 does not start an async cache server.
  • Keep using _#includehtml only when HTML inclusion is intentional.

Design Notes

DTM2 intentionally keeps rendering and rendered-output caching separate.

The manager caches:

  • canonical template paths;
  • parsed template trees;
  • dependency metadata for root templates and includes.

The manager does not cache rendered HTML responses. If a future rendered-cache layer is needed, it should remain a small optional layer above DTM2 rather than part of the parser/renderer core.

Benchmarks

The local benchmark harness lives in:

vlib/x/templating/dtm2/benchmarks/

Run it from the repository root:

vlib/x/templating/dtm2/benchmarks/run_dtm2_benchmark.sh

Useful options:

  • DTM2_BENCH_MODE=prod|prod_o2|dev
  • DTM2_BENCH_CASE=all|small_hot|small_cold|many_hot|many_cold|include_hot|include_cold|xml_hot|xml_cold
  • DTM2_BENCH_ITERATIONS=50000
  • DTM2_BENCH_COLD_ITERATIONS=500
  • DTM2_BENCH_PLACEHOLDERS=50
  • DTM2_BENCH_COMPRESS_HTML=true
  • DTM2_BENCH_RELOAD_MODIFIED_TEMPLATES=false
  • DTM2_BENCH_VALIDATE_EACH_ITERATION=false

Benchmark result directories are generated locally under vlib/x/templating/dtm2/benchmarks/results/ and should not be committed.

fn initialize #

fn initialize(params ManagerParams) &Manager

initialize creates a dynamic template manager rooted at params.template_dir.

The returned manager should normally be kept and reused. Reusing it is what gives dtm2 its parsed-template and path-resolution cache benefits.

fn TemplateType.from #

fn TemplateType.from[W](input W) !TemplateType

enum TemplateType #

enum TemplateType {
	html
	text
}

struct ExtensionConfig #

struct ExtensionConfig {
pub:
	html []string
	text []string
}

{ "html": [".html", ".htm", ".xml", ".view"], "text": [".txt", ".mail"] }

struct Manager #

@[heap]
struct Manager {
mut:
	// Base directory for relative template paths.
	template_dir string
	// Enables the lightweight deterministic HTML whitespace compressor.
	compress_html bool
	// When true, source and include files are stat-checked before cache reuse.
	reload_modified_templates bool
	// Maps caller-provided template paths to canonical source paths.
	resolved_template_paths map[string]string
	// Parsed-template cache keyed by canonical source path.
	compiled_templates map[string]&CompiledTemplate
	// User-provided extension-to-render-mode overrides. Built-in extensions are
	// resolved without allocating a per-manager default map.
	template_extensions map[string]TemplateType
}

Manager owns the runtime state of a dtm2 renderer.

It caches resolved template paths and parsed template trees, but never caches rendered HTML responses. This keeps the new engine deterministic and leaves legacy rendered-cache compatibility in x.templating.dtm.

fn (Manager) compiled_template_count #

fn (m &Manager) compiled_template_count() int

compiled_template_count is intentionally exposed for tests and diagnostics. In dtm2 it counts parsed template trees currently held by the manager.

fn (Manager) expand #

fn (mut m Manager) expand(template_path string, params RenderParams) string

expand renders template_path with the provided placeholders.

template_path can be absolute or relative to the manager's template directory. Supported extensions come from the manager extension table. Errors are reported to stderr and return the legacy-compatible Internal Server Error string.

struct ManagerParams #

@[params]
struct ManagerParams {
pub:
	// Root directory used when `expand()` receives a relative template path.
	// If empty, `<executable directory>/templates` is used.
	template_dir string
	// Compresses HTML output by removing newlines/tabs and redundant spaces.
	compress_html bool = true
	// Re-check source files and includes before reusing a parsed template tree.
	// Disable it for maximum hot-path throughput when templates are immutable.
	reload_modified_templates bool = true
	// Additional or overriding extension mappings.
	// Default mappings are: `.html`, `.htm`, `.xml` => HTML mode and
	// `.txt`, `.text` => text mode.
	template_extensions map[string]TemplateType
	// Optional JSON file containing extension mappings. It is merged after the
	// default mappings and before `template_extensions`, so explicit code
	// configuration wins over the file. If empty, DTM2 tries to load
	// `<template_dir>/dtm2_extensions.json` when that file exists.
	extension_config_file string
}

ManagerParams configure a dtm2 Manager.

struct RenderParams #

@[params]
struct RenderParams {
pub:
	// Placeholder values keyed by their template name without the `@` prefix.
	// Values are escaped by default.
	placeholders &map[string]string = &map[string]string{}
	// Prefix written back when a placeholder is missing. The default preserves
	// the original `@placeholder` text.
	missing_placeholder_prefix string = '@'
}

RenderParams configure one render call.