Skip to content

Latest commit

 

History

History
443 lines (359 loc) · 11 KB

File metadata and controls

443 lines (359 loc) · 11 KB
title Generating Content with Directives
summary How to use catalog and include directives to generate and embed content in Markdown files.

Generating Content with Directives

mdsmith can generate content inside your Markdown files. Two directives handle this: <?catalog?> for building file indexes and <?include?> for embedding content from other files. Both regenerate their body on mdsmith fix and flag stale content on mdsmith check.

Building a file index

Use <?catalog?> when you need a list of files with metadata — for example, a table of plans, a docs index, or a rule directory. For the agent-facing docs index, see Progressive Disclosure for AI Agents.

Minimal catalog

The simplest form lists all matching files as a bullet list:

<?catalog
glob: "docs/**/*.md"
?>
- [cli.md](design/cli.md)
- [index.md](development/index.md)
<?/catalog?>

Without row, header, or footer, the directive outputs - [<basename>](<path>) per file, sorted by path.

Custom template

Add row (and optionally header, footer) to control the output format. Use {field} placeholders to pull front matter values from matched files:

<?catalog
glob: "plan/*.md"
sort: id
header: |
  | ID | Title |
  |----|-------|
row: "| {id} | [{title}]({filename}) |"
?>
| 74 | [Directive guide](plan/74_directive-guide.md) |
| 75 | [Single-brace placeholders](plan/75_single-brace-placeholders.md) |
<?/catalog?>

Project a list-typed field into a row

{field} placeholders reach scalar front-matter values only. To project a list — multiple authors per file, multiple peer-linter mappings on a rule README, multiple tags — switch the directive from row: to row-expr:. The value is a CUE expression. Every identifier-safe key in the matched file's front matter binds at top-level scope. The expression must return a string.

<?catalog
glob: "internal/rules/MDS*/README.md"
where: 'category: "heading"'
sort: id
row-expr: |
  "| \(id) | " +
  strings.Join(
    [for m in markdownlint {
      "\(m.id) \([if m.default {"✅"}, if !m.default {"⚪"}][0]) \(m.name)"
    }],
    ", "
  ) + " |"
?>
<?/catalog?>

The strings standard-library package is preimported. CUE has no infix ternary; pick between two values with the [if cond {a}, if !cond {b}][0] list-comprehension idiom. row and row-expr are mutually exclusive on one directive. columns: width constraints apply to row only.

Multiple glob patterns

glob accepts a YAML list to collect files from several directories:

<?catalog
glob:
  - "docs/**/*.md"
  - "plan/*.md"
sort: path
row: "- [{title}]({filename})"
?>

Excluding files

Prefix a glob pattern with ! to exclude matching files. Exclusion patterns use the same glob syntax as include patterns:

<?catalog
glob:
  - "**/*.md"
  - "!drafts/**"
  - "!internal/notes.md"
row: "- [{title}]({filename})"
?>

At least one non-negated (include) pattern is required. Excludes and includes are collected into separate lists — a file matching any exclude pattern is filtered out regardless of where the pattern appears in the list.

Gitignore filtering

By default, files matched by .gitignore rules are excluded from catalog results. To include gitignored files, set gitignore: "false":

<?catalog
glob: "**/*.md"
gitignore: "false"
?>

Filtering with where

The where parameter narrows the matched set by a CUE expression evaluated against each file's parsed front matter. The expression grammar is the same one mdsmith list query accepts, so a CLI query expression drops in unchanged:

<?catalog
glob: "internal/rules/MDS*/README.md"
where: 'nature: "directive"'
row: "- [{name}]({filename})"
?>
<?/catalog?>

The filter runs after globbing and front matter parsing, but before sort and render. Failure modes:

  • An invalid CUE expression triggers an MDS019 diagnostic on the directive's opening line.
  • A file whose front matter is missing the referenced field is excluded (no diagnostic).
  • A field whose value does not satisfy the constraint (wrong type or wrong value) is excluded.

Sorting

Format: [-][numeric:]KEY. A - prefix means descending. Built-in keys: path, filename. Any other key is looked up in front matter. Missing values sort as empty string.

The optional numeric: prefix opts a field into integer comparison. Use it for ID-shaped fields where mixed 2-digit and 3-digit values would otherwise collate lexicographically (100 before 52):

<?catalog
glob: "plan/*.md"
sort: numeric:id
row: "| {id} | [{title}]({filename}) |"
?>

If any matched file's value fails to parse as an integer, the directive falls back to string compare for all entries — no error is raised, so a field that is sometimes numeric stays usable. -numeric:id reverses the order.

What happens when no files match

If empty is defined, its text is used. Otherwise zero lines appear between the markers.

For full parameter reference, see MDS019 catalog.

Embedding file content

Use <?include?> when you want to embed the content of another file — for example, sharing a development guide across README and docs, or including a config file as a code block.

Basic include

<?include
file: DEVELOPMENT.md
strip-frontmatter: "true"
?>
Build and test reference for contributors.
<?/include?>

By default, YAML front matter is stripped. Set strip-frontmatter: "false" to keep it.

Code fence wrapping

Include a non-Markdown file wrapped in a fenced code block:

<?include
file: config.yml
wrap: yaml
?>
```yaml
key: value
```
<?/include?>

Heading-level adjustment

heading-level takes "absolute" or an integer 1-6.

Use "absolute" when including under an existing heading, to shift included headings so they nest correctly:

## Project

<?include
file: DEVELOPMENT.md
heading-level: "absolute"
?>
### Build
Steps here.
<?/include?>

Without this parameter, included headings keep their original level, which may break heading hierarchy.

Use an integer to pin the shallowest included heading to that level, whatever the source or parent — handy at the document root, where "absolute" is a no-op:

<?include
file: features.md
heading-level: "2"
?>
## Pinned To Level 2
<?/include?>

heading-level is mutually exclusive with heading-offset.

Heading-offset adjustment

To shift every included heading by a fixed amount, use heading-offset with a signed integer from -6 to 6:

<?include
file: features.md
heading-offset: "1"
?>
## Was An H1 In The Source
<?/include?>

A positive value demotes headings; a negative value promotes them. Unlike heading-level: "absolute", the shift does not depend on a preceding heading, so it also works at the document root — handy when a file's visual title is a logo or image rather than an H1. heading-offset cannot combine with heading-level or extract: — those parameters are mutually exclusive with it.

Link rewriting

Relative links in included content are automatically rewritten to resolve from the including file's directory. Absolute URLs and protocol links are not modified.

Cycle detection

Include chains are tracked. Circular includes and chains deeper than 10 levels are rejected:

cyclic include: a.md -> b.md -> a.md

Include a typed value

When the target file is kind-typed, extract: walks the same JSON projection mdsmith extract produces and splices a single leaf. The value is a dotted path: frontmatter.title reaches the title field; tagline.text reaches the paragraph under the ## Tagline heading; a section whose body is a fenced code block is reached through its .code key.

A paragraph section. The target's ## Tagline heading projects to {"tagline": {"text": "..."}}, so the path tagline.text lands on the prose:

<?include
file: docs/brand/messaging.md
extract: tagline.text
?>
Markdown, fast.
<?/include?>

A fenced code block. A section whose body is a single fenced code block projects its contents under a code key. Given a file setup.md whose ## Setup section holds an install command, the projection is {"setup": {"code": "..."}}, and setup.code splices the code body verbatim:

<?include
file: setup.md
extract: setup.code
?>
go install github.com/jeduden/mdsmith/cmd/mdsmith@latest
<?/include?>

A frontmatter scalar. Every kind-typed file's projection carries a top-level frontmatter object beside the body sections. The path is frontmatter.<key>:

<?include
file: docs/brand/messaging.md
extract: frontmatter.title
?>
mdsmith product messaging
<?/include?>

Single-content-key shortcut. A leaf object that carries exactly one of text, code, inline, items, or rows resolves to the inner value, so extract: tagline and extract: tagline.text splice the same content. An inline value is a typed span list rather than a scalar, so it cannot be spliced yet and surfaces as a lint error. Multi-key wrappers (the JSON projection of a section with several content entries) are ambiguous and surface as a lint error with the available keys listed.

Restrictions. extract: is scalar-only for now — combining it with strip-frontmatter: or heading-level: is rejected. A target file with no resolved kind is rejected too: without a schema there is no projection to walk. A target whose body fails required-structure surfaces the same diagnostic mdsmith check would raise on the target, anchored to the include directive's call site.

For full parameter reference, see MDS021 include.

Placement rules

Both directives are only recognized at document root (parent must be the Document node). Maximum indent is 3 spaces.

Directives are not recognized inside list items, blockquotes, tables, fenced code blocks, or HTML blocks.

4-space indent footgun: 4 or more leading spaces turns the line into a code block. The directive is silently ignored with no error. Always use 0-3 spaces.

Nesting

Same-type nesting is supported. When an included file contains its own generated sections (include, catalog, etc.), the inner markers are treated as literal content of the outer directive. FindMarkerPairs pairs only the outermost markers of each directive type; inner markers are skipped. Cross-type directives between markers may appear but are overwritten by the outer generator on fix.

Placeholder syntax

{field} is the only placeholder syntax. It works in row templates and schema headings.

  • {filename} — relative path from the marker file.
  • {title}, {summary} — looked up in front matter.
  • Missing field — empty string.
  • Case mismatch — "did you mean?" hint.
  • Non-string scalar — formatted to string. Composite values (maps, slices) — empty string.
  • Literal { — write {{. Literal } — write }}.

CUE paths provide nested access for structured front matter values.