mdma

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: MIT Imports: 9 Imported by: 0

README

mdma-go

Render one or more Markdown strings from an .mdma template and a typed inputs map.

This is the Go reference implementation of MDMA, a from-scratch templating engine with no dependencies. See the language specification and docs for the full grammar, filter reference, and worked examples.

Install

go get github.com/Dastfox/mdma-go

Usage

Values passed as inputs (and returned from a template) follow Go's usual dynamic-JSON shape: string, float64, bool, nil, []any, and map[string]any. This is exactly what encoding/json produces when unmarshaling into any, so inputs sourced from JSON need no conversion.

import mdma "github.com/Dastfox/mdma-go"

source, _ := os.ReadFile("release-notes.mdma")
result, err := mdma.Render(string(source), map[string]any{
	"project":  "Acme SDK",
	"version":  "3.0.0",
	"date":     "2026-07-01",
	"added":    []any{"WebSocket support"},
	"breaking": true,
	"releases": []any{
		map[string]any{"version": "2.1.0", "date": "2026-06-01", "added": []any{"Dark mode"}},
	},
})

slug, _ := result.Get("slug")                   // "Acme SDK-3.0.0"        (string)
notes, _ := result.Get("release-notes")         // rendered markdown        (string)
entries, _ := result.Get("changelog-entry")     // one string per release   ([]string, from `multiple`)

A multiple block can also declare name to key each item by a computed name instead of array position:

<changelog-by-version
multiple: entry in releases
name: entry.version
>

### {{ entry.version }} — {{ entry.date }}
named, _ := result.Get("changelog-by-version")
om := named.(*mdma.OrderedMap)
om.Get("2.1.0") // "### 2.1.0 — 2026-06-01\n"
om.Keys()       // in declaration/computed order, e.g. ["2.1.0", "2.0.0"]

RenderTemplate(tmpl, inputs) renders an already-parsed template (from ParseFile) — same semantics as Render, minus the parse:

tmpl, _ := mdma.ParseFile(source) // parse once ...
mdma.RenderTemplate(tmpl, map[string]any{"project": "Acme SDK", "version": "3.0.0", "date": "2026-07-01"}) // ... render many times

RenderFile(path, inputs) reads path as UTF-8 and renders it — equivalent to Render(string(contents), inputs):

result, _ := mdma.RenderFile("release-notes.mdma", map[string]any{"project": "Acme SDK", "version": "3.0.0", "date": "2026-07-01"})

WriteOutput(result, outputDir, block) writes a Render/RenderFile result to .md files. Pass block = "" to write every top-level block (in file declaration order); pass a block name to write only that one. A string-valued block becomes {outputDir}/{block}.md; a multiple block becomes a directory {outputDir}/{block}/ with one file per item — {name}.md if the block declared name, otherwise {index}.md. Returns the list of paths written.

result, _ := mdma.RenderFile("release-notes.mdma", inputs)
mdma.WriteOutput(result, "out/", "")               // every block
mdma.WriteOutput(result, "out/", "release-notes")  // just that one

GetInputs(source) returns the template's @inputs declarations, and ValidateInputs(source, inputs) checks an inputs map against them without rendering — returning the resolved inputs (defaults applied) or an error (*mdma.MissingInputError / *mdma.TypeError):

decls, _ := mdma.GetInputs(source)
// []mdma.InputDecl{{Name: "project", Type: "string", HasDefault: false}, ...}

resolved, err := mdma.ValidateInputs(source, map[string]any{"project": "Acme SDK", "version": "3.0.0", "date": "2026-07-01"})
// resolved inputs, with declared defaults applied

ParseFile(source) is also exported for lower-level access to the parsed template (inputs and blocks).

Render and friends return one of these error types on failure. Every one implements the mdma.Error marker interface (errors.As(err, &target) works with either a concrete type or mdma.Error itself as a catch-all):

Type Condition
*mdma.MissingInputError a required input (no default) was not supplied
*mdma.TypeError an input's runtime type doesn't match its declared type, or a name expression evaluates to something other than a string/number
*mdma.ReferenceError a forward block reference, or an undefined variable
*mdma.FilterError a filter was applied to a value of the wrong type
*mdma.SyntaxError the .mdma source doesn't conform to the grammar (including name used without a preceding multiple)
*mdma.DuplicateNameError two items in a multiple block computed the same name value

WriteOutput's path-traversal guard (a malicious computed name like "../../evil") is the one exception: it returns a plain error, not an mdma.Error, matching the Python and TypeScript implementations.

Behavioral notes not obvious from spec.md

  • The blank line conventionally left between one block's content and the next block's header (or EOF) is treated as file formatting, not part of either block's rendered value — it's stripped from both ends of the block body before parsing. This is required for block references ({{ blockname }}) to be safely embeddable inline; otherwise every block value would carry a stray trailing newline from that separator. Blank lines inside a body are preserved exactly as written.
  • Whitespace control ({%-/-%}) is applied per-tag, exactly as written — a conditional branch that renders empty does not retroactively remove surrounding blank-line text unless that text is trimmed by an adjacent -.
  • Accessing a missing property on an object/object[] value (e.g. entry.description when description wasn't set) yields nil rather than an error — objects are untyped maps, so this is normal and is what makes | default(...) useful on them. An undefined root identifier (typo'd variable/block/input name) still raises *mdma.ReferenceError.
  • default([]) and other array literals ([a, b]) are supported in expressions even though the formal grammar doesn't enumerate an array-literal production — the filter reference relies on this syntax ({{ list | default([]) }}).
  • multiple is a reserved word (can't be used as a block or input name), but name is not — name: is only ever recognized in its fixed position inside a multiple block's header, so an input or block literally named name (e.g. name: string) is unaffected.
  • Comparisons (> >= < <=) are only defined for number-number and string-string operands; comparing any other combination returns a *mdma.TypeError rather than panicking. This is stricter than the Python and TypeScript implementations (which respectively raise a raw TypeError or apply JS's loose relational coercion here) — Go has no polymorphic ordering operator, so this is the one place mdma-go deliberately narrows otherwise-undefined behavior instead of mirroring a host-language quirk.
  • Numbers always format without exponential notation (strconv.FormatFloat with 'f'), unlike JavaScript's default Number.toString(), which switches to exponential notation for very large or very small magnitudes.

Development

go build ./...
go vet ./...
go test ./...

Documentation

Overview

Package mdma is a from-scratch Go implementation of the MDMA templating language: a typed Markdown templating format for generating one or more Markdown strings from a declared @inputs schema.

See https://dastfox.github.io/mdma/ for the full language specification. This package mirrors python-mdma and typescript-mdma module-for-module.

Index

Constants

View Source
const Version = "0.2.0"

Version is the current mdma-go package version, bumped alongside release tags.

Variables

This section is empty.

Functions

func ValidateInputs

func ValidateInputs(source string, inputs map[string]any) (map[string]any, error)

ValidateInputs checks an inputs map against an .mdma source's @inputs declarations without rendering. Returns the resolved inputs (declared defaults applied). Returns *MissingInputError when a required input is absent and *TypeError when a value does not match its declared type.

func WriteOutput

func WriteOutput(result *RenderResult, outputDir string, block string) ([]string, error)

WriteOutput writes one or all rendered blocks from a Render() result to .md files.

block == "" (default) writes every top-level block, in file declaration order; block = "name" writes only that one. A string-valued block is written to {outputDir}/{block}.md. A `multiple` block ([]string, or an *OrderedMap if it also declared `name`) is written to {outputDir}/{block}/, one file per item -- {name}.md if the block declared `name`, otherwise {index}.md.

Returns a *ReferenceError if block isn't present in result.

Types

type ArrayLiteral

type ArrayLiteral struct{ Items []Expr }

ArrayLiteral is an `[a, b, ...]` expression.

type BinOp

type BinOp struct {
	Op          string // "and" | "or" | "==" | "!=" | ">" | ">=" | "<" | "<="
	Left, Right Expr
}

BinOp is a binary `and`/`or`/comparison expression.

type Block

type Block struct {
	Name           string
	MultipleVar    string // "" if this isn't a `multiple` block
	MultipleSource string
	NameExpr       Expr // non-nil only alongside MultipleVar
	Body           []Node
}

Block is a single parsed <name> block.

type DuplicateNameError

type DuplicateNameError struct{ ComputedName, BlockName string }

DuplicateNameError reports two items in a `multiple:`/`name:` block that computed the same name.

func (*DuplicateNameError) Error

func (e *DuplicateNameError) Error() string

type Error

type Error interface {
	error
	// contains filtered or unexported methods
}

Error is implemented by every mdma-specific error type. Use errors.As with a concrete type (*SyntaxError, *MissingInputError, etc.), or with this interface as a catch-all for "any mdma error".

type Expr

type Expr interface {
	// contains filtered or unexported methods
}

Expr is an expression AST node: Literal, ArrayLiteral, Var, Not, BinOp, or FilterCall.

type ExprNode

type ExprNode struct{ Expr Expr }

ExprNode is a `{{ expr }}` interpolation.

type FilterCall

type FilterCall struct {
	Target Expr
	Name   string
	Args   []Expr
}

FilterCall is a `target | name(args...)` expression.

type FilterError

type FilterError struct{ FilterName, ExpectedType string }

FilterError reports a filter applied to a value of the wrong type.

func (*FilterError) Error

func (e *FilterError) Error() string

type ForNode

type ForNode struct {
	VarName  string
	Iterable Expr
	Body     []Node
}

ForNode is a `{% for var in iterable %}...{% endfor %}` loop.

type IfBranch

type IfBranch struct {
	Condition Expr
	Body      []Node
}

IfBranch is one `if`/`elif`/`else` arm. Condition is nil for the else arm.

type IfNode

type IfNode struct{ Branches []IfBranch }

IfNode is an `{% if %}...{% elif %}...{% else %}...{% endif %}` chain, flattened into an ordered list of branches evaluated top-to-bottom.

type InputDecl

type InputDecl struct {
	Name       string
	Type       string // one of: string, string[], number, number[], boolean, object, object[]
	HasDefault bool
	Default    any
}

InputDecl is a single @inputs declaration.

func GetInputs

func GetInputs(source string) ([]InputDecl, error)

GetInputs parses an .mdma source string and returns its @inputs declarations.

type Literal

type Literal struct{ Value any }

Literal is a string, number, or boolean literal.

type MissingInputError

type MissingInputError struct{ Name string }

MissingInputError reports a required input (no default) missing at render time.

func (*MissingInputError) Error

func (e *MissingInputError) Error() string

type Node

type Node interface {
	// contains filtered or unexported methods
}

Node is a block-body AST node: TextNode, ExprNode, IfNode, or ForNode.

type Not

type Not struct{ Operand Expr }

Not is a `not <operand>` expression.

type OrderedMap

type OrderedMap struct {
	// contains filtered or unexported fields
}

OrderedMap is a string-keyed, insertion-ordered map. Go maps have no defined iteration order, but a `multiple`+`name` block's computed order is semantically part of the render result, so it's tracked explicitly here.

func (*OrderedMap) Get

func (m *OrderedMap) Get(key string) (string, bool)

Get returns the value for key and whether it was present.

func (*OrderedMap) Has

func (m *OrderedMap) Has(key string) bool

Has reports whether key has been set.

func (*OrderedMap) Keys

func (m *OrderedMap) Keys() []string

Keys returns the keys in insertion order.

func (*OrderedMap) Len

func (m *OrderedMap) Len() int

Len returns the number of entries.

func (*OrderedMap) Set

func (m *OrderedMap) Set(key, value string)

Set adds or overwrites key with value, appending key to the iteration order only the first time it's set.

type ParsedTemplate

type ParsedTemplate struct {
	Inputs []InputDecl
	Blocks []Block
}

ParsedTemplate is the result of ParseFile: an @inputs schema plus the file's blocks, in declaration order.

func ParseFile

func ParseFile(source string) (*ParsedTemplate, error)

ParseFile parses a full .mdma source string into its @inputs declarations and blocks.

type ReferenceError

type ReferenceError struct{ Message string }

ReferenceError reports a block or variable reference that couldn't be resolved.

func ForwardBlockError

func ForwardBlockError(name string) *ReferenceError

ForwardBlockError reports a reference to a block declared later in the file, which hasn't rendered yet.

func UndefinedError

func UndefinedError(name string) *ReferenceError

UndefinedError reports a reference to a name that is neither a binding, a block, nor a declared input.

func (*ReferenceError) Error

func (e *ReferenceError) Error() string

type RenderResult

type RenderResult struct {
	// contains filtered or unexported fields
}

RenderResult is the output of Render/RenderTemplate/RenderFile: each block's name mapped to its rendered value, preserving file declaration order (Go maps have no iteration order, but WriteOutput's default -- write every block -- relies on that order, matching Python dict / JS object insertion-order behavior).

func Render

func Render(source string, inputs map[string]any) (*RenderResult, error)

Render parses and renders an .mdma source string against an inputs map.

Returns a *RenderResult mapping each block name to its rendered value. A `multiple` block renders to a []string, or -- if it also declares `name` -- to an *OrderedMap keyed by each item's computed name.

func RenderFile

func RenderFile(path string, inputs map[string]any) (*RenderResult, error)

RenderFile reads path as UTF-8 and renders it. Equivalent to Render(string(contents), inputs).

func RenderTemplate

func RenderTemplate(tmpl *ParsedTemplate, inputs map[string]any) (*RenderResult, error)

RenderTemplate renders an already-parsed template (from ParseFile) against an inputs map. Same semantics as Render, minus the parse -- parse once, render many times.

func (*RenderResult) Get

func (r *RenderResult) Get(name string) (RenderedValue, bool)

Get returns the rendered value for a block name.

func (*RenderResult) Names

func (r *RenderResult) Names() []string

Names returns all block names in file declaration order.

type RenderedValue

type RenderedValue = any

RenderedValue is the rendered output of a single block: a string (plain block), a []string (`multiple` block), or an *OrderedMap (`multiple` block that also declares `name`).

type SyntaxError

type SyntaxError struct{ Message string }

SyntaxError reports .mdma source that does not conform to the grammar.

func (*SyntaxError) Error

func (e *SyntaxError) Error() string

type TextNode

type TextNode struct{ Text string }

TextNode is literal output text.

type TypeError

type TypeError struct{ Expected, Actual string }

TypeError reports a value whose runtime type doesn't match what's expected.

func (*TypeError) Error

func (e *TypeError) Error() string

type Var

type Var struct{ Path []string }

Var is a dotted identifier chain, e.g. `entry.version` -> ["entry", "version"].

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL