Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Data Interpolation

mdt can pull values from project files — package.json, Cargo.toml, YAML configs, and more — into your templates. This means version numbers, package names, and other metadata stay in one place and flow into your documentation automatically.

Setup

Add a [data] section to your mdt.toml:

[data]
package = "package.json"
release = { path = "release-info", format = "json" }
version = { command = "cat VERSION", format = "text", watch = ["VERSION"] }

This maps the file package.json to the namespace package.

  • String values are backward-compatible and infer format from extension.
  • Typed values ({ path, format }) let you explicitly declare a format for files without extensions.
  • Script values ({ command, format, watch }) execute commands and optionally cache stdout based on watched files.

If your package.json contains:

{
	"name": "my-lib",
	"version": "1.2.3",
	"description": "A great library"
}

Then in your template files you can write:

<!-- {@install} -->

Install `{{ package.name }}` version {{ package.version }}:

  npm install {{ package.name }}@{{ package.version }}

{{ package.description }}.

<!-- {/install} -->

After mdt update, targets of install will contain:

Install `my-lib` version 1.2.3:

  npm install my-lib@1.2.3

A great library.

Supported data formats

Format / ExtensionParser
text, .txtRaw text string
json, .jsonJSON
toml, .tomlTOML
yaml, .yamlYAML
yml, .ymlYAML
kdl, .kdlKDL
ini, .iniINI

All formats are converted to a common structure internally. You access values using dot notation regardless of the source format.

Script-backed data sources

[data] entries can run shell commands and use stdout as template data. This is useful for values that come from tooling (for example Nix, git metadata, or generated version files).

[data]
release = { command = "cat VERSION", format = "text", watch = ["VERSION"] }
  • command: shell command executed from the project root.
  • format: parser for stdout (text, json, toml, yaml, yml, kdl, ini).
  • watch: files that control cache invalidation.

When watch files are unchanged, mdt reuses cached script output from .mdt/cache/data-v1.json instead of re-running the command.

  • Script outputs are cached per namespace, command, format, and watch list.
  • If watch is empty, mdt re-runs the script every load (no cache hit).
  • A non-zero script exit status fails data loading with an explicit error.

Inline interpolation patterns

Inline blocks are useful when you need one local value from your data scope without creating a reusable source.

Inline value in prose

Install version <!-- {~releaseVersion:"{{ pkg.version }}"} -->0.0.0<!-- {/releaseVersion} --> today.

Inline value in a table cell

| Package | Version                                                               |
| ------- | --------------------------------------------------------------------- |
| mdt     | <!-- {~mdtVersion:"{{ pkg.version }}"} -->0.0.0<!-- {/mdtVersion} --> |

Inline value with a transformer

CLI version: <!-- {~cliVersionCode:"{{ pkg.version }}"|code} -->`0.0.0`<!-- {/cliVersionCode} -->

Inline value from a script-backed data source

[data]
release = { command = "cat VERSION", format = "text", watch = ["VERSION"] }
Release: <!-- {~releaseValue:"{{ release }}"} -->0.0.0<!-- {/releaseValue} -->

When VERSION is unchanged, mdt reuses cached script output from .mdt/cache/data-v1.json.

TOML example

# mdt.toml
[data]
cargo = "Cargo.toml"
# Cargo.toml
[package]
name = "my-crate"
version = "0.1.0"
edition = "2024"

Template usage:

<!-- {@crateInfo} -->

**{{ cargo.package.name }}** — Rust edition {{ cargo.package.edition }}

<!-- {/crateInfo} -->

YAML example

# mdt.toml
[data]
config = "config.yaml"
# config.yaml
app:
  name: My App
  port: 8080
features:
  - auth
  - logging

Template usage:

<!-- {@appConfig} -->

{{ config.app.name }} runs on port {{ config.app.port }}.

<!-- {/appConfig} -->

Multiple data sources

You can map as many files as you need:

[data]
package = "package.json"
cargo = "Cargo.toml"
config = "config.yaml"
meta = "metadata.kdl"

Each namespace is independent. Use them together in the same template:

<!-- {@versions} -->

| Package | Version                     |
| ------- | --------------------------- |
| npm     | {{ package.version }}       |
| crate   | {{ cargo.package.version }} |

<!-- {/versions} -->

Template syntax

mdt uses minijinja for template rendering. The full minijinja syntax is available:

Variables

{{ namespace.key }}
{{ namespace.nested.deeply.value }}

Undefined variables render as empty strings (mdt uses minijinja’s “chainable” undefined behavior).

Conditionals

{% if package.private %}
This is a private package.
{% else %}
Available on npm.
{% endif %}

Loops

{% for feature in config.features %}
- {{ feature }}
{% endfor %}

Filters

minijinja’s built-in filters work alongside mdt’s transformers:

{{ package.name | upper }}
{{ package.description | truncate(50) }}

When rendering happens

Template variables are rendered before transformers are applied. The flow is:

Provider content
  → Render {{ variables }} via minijinja
  → Apply |transformers
  → Replace target content

This means transformers operate on the already-rendered content. For example, if {{ package.name }} renders to my-lib, then a |trim transformer trims the rendered result.

No data, no rendering

If your project has no mdt.toml or no [data] section, template variable rendering is skipped entirely. Content containing {{ }} syntax passes through unchanged. This keeps mdt fully backwards-compatible for projects that don’t need data interpolation.