chore(skill): Own tttk-authoring in the repo

Move the skill to .agents/skills/tttk-authoring and point gen:skill-reference at it. Activate it by symlinking ~/.agents/skills/tttk-authoring to the in-repo copy.

Extend the reference drift guard to cover the skill copy now that it is versioned, and document activation in docs/development.md.
This commit is contained in:
2026-09-12 17:19:38 +08:00
parent 6d94a99b42
commit d8ccaecade
15 changed files with 870 additions and 16 deletions
+138
View File
@@ -0,0 +1,138 @@
---
name: tttk-authoring
description: Author and edit TTRPG game design documents for the tttk (Tabletop Toolkit) CLI. Use when writing or modifying markdown content that uses tttk custom elements (:md-dice, :md-table, :md-deck, :md-pins, :md-commander, etc.), custom fenced code blocks (role=spark-table / declare / file / tag), companion CSV data files, or when previewing content with `tttk serve` and validating it with `tttk check`.
---
# Authoring tttk game design documents
`tttk` (Tabletop Toolkit) renders a folder of markdown into an interactive TTRPG
web app. A game design document is **ordinary GFM markdown** plus exactly two
extensions:
1. **Custom elements** written with `marked-directive` syntax: `:name[content]{attrs}`.
2. **Custom fenced code blocks** whose info string carries a `role=` attribute.
Everything else — headings, lists, tables, blockquotes, alerts, mermaid — is
standard markdown. Do not invent element names, props, or roles; check
`reference.md` when unsure. Deeper topics have their own files: `yaml-tag.md`
(deck config), `csv.md` (data files), `layout.md` (columns), and `markdown.md`
(alerts and mermaid).
## Workflow
- Content lives in a directory (commonly `content/`). Preview with hot reload:
`npx tttk serve ./content` (default port 3000, override with `-p 8080`).
- **Validate after every edit:** `npx tttk check ./content`. It exits `1` when
there are errors and prints `file:line` diagnostics. Use `--json` for
machine-readable output and `--quiet` for errors only. This is the primary
verification loop: write → check → fix.
- The scanner picks up `.md`, `.csv`, `.yarn`, and `.svg` files.
- All paths in directives are **relative to the current markdown file**
(`./data.csv`, `../shared/npcs.csv`, `./images/map.png`).
- A typical game/adventure folder:
```
my-adventure/
├── design.md # design notes (plain markdown)
├── adventure.md # the document players/GM read
├── locations.csv # data referenced by :md-table
├── cast.csv
└── route.csv
```
## Directive syntax
```
:name[content]{key="value" .class #id}
```
- `[content]` is the directive body — a path, dice formula, color, etc.
- `{key="value"}` sets attributes. `{.class}` and `{#id}` are shorthands.
- Container directives use `::: name` … `:::`; nest deeper with `::::`, `:::::`.
Use the **registered element names**: `:md-dice`, not `:dice`. The full list is
in `reference.md`.
## Code block roles
Fenced blocks are dispatched by the `role=` attribute in the info string:
| Info string | Behavior |
|---|---|
| ` ```csv role=spark-table ` | Body is CSV; replaced with `:md-table[./csv_<hash>]`. First column header must be a dice formula (`d6`, `2d6+1`, …). |
| ` ```markdown role=spark-table ` | Body is a GFM pipe table; converted to CSV, then same as above. |
| ` ```csv role=declare ` | Defines reactive variables / tag modifiers. Block is stripped. |
| ` ```<lang> role=file ` | Body stored as text content, block stripped, referenced by its derived id. |
| ` ```yaml role=tag ` | Kept intact; rendered as a custom element from YAML (`tag:`, `body:`, …). |
| ` ```<lang> ` (no role) | Rendered as a normal visible code block. |
| unknown role | Warned about and stripped. |
## Rules that prevent broken output
- Quote any attribute value containing spaces or `"`:
`{pins="A:30,40 B:10,30"}`.
- In CSV, wrap fields containing `,`, `"`, or a newline in double quotes and
escape `"` as `""`. Lines starting with `#` are comments.
- `label` and `body` are the conventional CSV columns. Put titles inside `body`
as markdown headings; use **bold labels** for sub-fields, not headings.
- A `group` column in a table CSV automatically produces group tabs — no
attribute needed.
- Reference each CSV once per document; don't repeat the same `:md-table`.
- For card decks, use the `yaml role=tag` form with `tag: md-deck` (see
`yaml-tag.md`). The `:md-deck[...]{...}` directive is legacy and can't express
template layers or card backs.
- Keep CSV files clean — header row plus data. Put deck config and templates in
the YAML block's `data-config`, not in CSV front matter (see `csv.md`).
- `{{prop}}` in a CSV `body` interpolates another column (or front-matter key)
from the same row; with `{remix}` it pulls from a random row instead.
## Validation
`npx tttk check ./content` validates against the same content registry the app
uses, so a clean check means the app resolves the content too. Errors exit `1`:
| Code | Meaning |
|---|---|
| `TTTK-S002` | container directive `:::` never closed |
| `TTTK-D001` | unknown `:md-*` element name |
| `TTTK-D002` | malformed attribute block (e.g. an unquoted value with a space) |
| `TTTK-R001` | unknown code-block `role=` (the block is silently dropped) |
| `TTTK-R003` | `role=declare` failed to parse |
| `TTTK-R005` | `role=tag` YAML is invalid |
| `TTTK-R006` | `role=tag` missing or naming an unknown element |
| `TTTK-C001` | content reference does not resolve |
| `TTTK-C004` | `:md-embed` cycle |
| `TTTK-L001` | link target file does not exist |
| `TTTK-V001` | CSV failed to parse (e.g. an unclosed quote) |
Warnings are usually real too:
| Code | Meaning |
|---|---|
| `TTTK-R002` | spark-table first header is not a dice formula |
| `TTTK-R004` | `role=declare` row skipped (empty `key` or `expr`) |
| `TTTK-C002` | reference resolves to an unexpected file type |
| `TTTK-L002` | `#section` matches no heading |
| `TTTK-A001` | icon missing under `assets/` |
| `TTTK-A002` | image missing |
| `TTTK-V002` | CSV row column count differs from the header |
| `TTTK-V003` | duplicate `label` values |
| `TTTK-V004` | `{{prop}}` is not a CSV column or front-matter key |
| `TTTK-V005` | spark-table roll value is not a number or range |
`TTTK-C003` (info) flags the same CSV referenced twice by `:md-table` in one
document. Icon checks assume no `assets/` directory exists above the checked
directory. The full code catalog lives in `reference.md`.
## Reference
- `reference.md` — **generated** catalog of every custom element, inline
directive, code block role, and dice notation. Do not edit by hand;
regenerate from the repo with `npm run gen:skill-reference`.
- `yaml-tag.md` — `role=tag` blocks, `data-config` layers, card backs, and the
legacy `:md-deck` layer syntax.
- `csv.md` — CSV authoring: columns, `{{prop}}` interpolation, quoting, and the
discouraged front-matter form.
- `layout.md` — column layout markers.
- `markdown.md` — GitHub alerts and mermaid diagrams.
- `examples/adventure.md` and `examples/encounters.csv` — a complete minimal
document and its data file.
+42
View File
@@ -0,0 +1,42 @@
# CSV data files
**Keep CSV files clean** — a header row and data rows, nothing else. Put deck
configuration and templates in the `yaml role=tag` block's `data-config`, not in
the CSV. This keeps data portable and presentation in one place. See
`reference.md` for the element and role catalog.
```csv
label,body,name,type
1,"### 火球术卷轴
一张泛黄的羊皮纸。",火球术卷轴,消耗品
```
- `label` — unique row id (number or text).
- `body` — main content, supports markdown.
- Any other columns are custom fields.
- `{{prop}}` in `body` interpolates another column from the same row (or a
random row when the table uses `{remix}`).
- Wrap values containing `,`, `"`, or newlines in double quotes; escape `"` as
`""`. Lines beginning with `#` are comments.
## Front matter (avoid)
The parser also accepts YAML front matter delimited by `---`, which can declare
`fields`, a `deck:` block, and arbitrary keys merged into every row. **Prefer
not to use it** — it mixes presentation into the data file. Use the YAML tag
block instead.
```csv
---
fields:
- name: name
description: 卡牌名称
deck:
size: 63x88
grid: 5x5
game_system: D&D 5e
---
label,body,name,type
1,"### 火球术卷轴",火球术卷轴,消耗品
```
@@ -0,0 +1,68 @@
# The Hollow Mill
## Adventure brief
The mill on the village edge has stood silent for a year. Last week its wheel
began turning again — on its own.
> [!WARNING]
> The mill's guardian is not hostile by default. Attacking it forfeits the
> peaceful resolution.
## Locations
:md-table[./locations.csv]
## Random encounters
:md-table[./encounters.csv]{roll=true}
## The mill guardian
:md-dice[1d20+3]{key="guardian-attack"}
The guardian strikes with :[sword] for :md-dice[2d6+2]{key="guardian-damage"}.
## Encounter table (inline)
```csv role=spark-table roll=true
d6,Encounter
1,A crow watches from the rafters.
2,The floorboards groan underfoot.
3,A cold draft carries the smell of grain.
4,The wheel outside stops for one breath.
5,A sack of flour splits open.
6,The guardian's lantern flickers to life.
```
## Route
```mermaid
graph TD
A[Village] --> B[Mill Road]
B --> C{Mill Door}
C -->|Force| D[Ground Floor]
C -->|Listen| E[Secret Cellar]
D --> F[Wheel Room]
E --> F
```
## Guardian card
```yaml role=tag
tag: md-deck
body: ./guardian.csv
data-config:
size: 63x88
grid: 5x5
layers:
- prop: name
pos: 1,1-5,1
font: 12
- template: |
**{{type}}** — {{hp}} HP
{{description}}
pos: 1,3-5,8
font: 3
align: l
```
@@ -0,0 +1,36 @@
group,label,body
Mill Road,1,"### A crow on the fence
A crow fixes you with one eye, then flies toward the mill.
**Interaction**: Follow it (Perception 3/1)
**Risk**: None"
Mill Road,2,"### Broken cart
An overturned cart blocks half the road.
**Interaction**: Clear it (Strength 3/2)
**Risk**: Failure costs 1 Stamina"
Mill Road,3,"### Cold spot
The air turns suddenly cold.
**Interaction**: Press on (Constitution 3/1)
**Risk**: Failure deals 1 Constitution damage"
Wheel Room,1,"### The turning wheel
The wheel grinds without water.
**Interaction**: Jam the gears (Strength 3/3)
**Risk**: Failure deals 1 Agility damage"
Wheel Room,2,"### Flour dust
Thick dust fills the air.
**Interaction**: Cover your mouth (Constitution 3/1)
**Risk**: Failure deals 1 Constitution damage"
1 group label body
2 Mill Road 1 ### A crow on the fence A crow fixes you with one eye, then flies toward the mill. **Interaction**: Follow it (Perception 3/1) **Risk**: None
3 Mill Road 2 ### Broken cart An overturned cart blocks half the road. **Interaction**: Clear it (Strength 3/2) **Risk**: Failure costs 1 Stamina
4 Mill Road 3 ### Cold spot The air turns suddenly cold. **Interaction**: Press on (Constitution 3/1) **Risk**: Failure deals 1 Constitution damage
5 Wheel Room 1 ### The turning wheel The wheel grinds without water. **Interaction**: Jam the gears (Strength 3/3) **Risk**: Failure deals 1 Agility damage
6 Wheel Room 2 ### Flour dust Thick dust fills the air. **Interaction**: Cover your mouth (Constitution 3/1) **Risk**: Failure deals 1 Constitution damage
@@ -0,0 +1,2 @@
label,name,type,hp,description
1,The Hollow Miller,Spirit,10,"Bound to the wheel by an old promise. Speaks only in the sound of grinding stone."
1 label name type hp description
2 1 The Hollow Miller Spirit 10 Bound to the wheel by an old promise. Speaks only in the sound of grinding stone.
@@ -0,0 +1,47 @@
label,body
Village,"### Village
**Type**: Starting location
A quiet village at the edge of the marsh. The miller's daughter asks after her
father.
**Interaction**: Ask about the mill (Charisma 3/1)
**Supplies**: Rope, torches, one healing draught"
Mill Road,"### Mill Road
**Type**: Route
A muddy track winding between hedgerows toward the mill.
**Interaction**: Spot tracks (Perception 3/1)
**Encounters**: 3 random encounters"
Ground Floor,"### Ground Floor
**Type**: Hub
Sacks of grain, a cold hearth, and a ladder to the wheel room.
**Interaction**:
- Search the hearth (Perception 3/2) — find a brass key
- Climb the ladder (Agility 3/1)"
Secret Cellar,"### Secret Cellar
**Type**: Archive
A low cellar beneath the millstones. Ledgers line the walls.
**Interaction**: Read the ledgers (Intelligence 3/2) — learn the guardian's name
**Clue**: The last entry is dated one year ago, in the miller's hand"
Wheel Room,"### Wheel Room
**Type**: Finale
The great wheel turns in silence, though no water reaches it.
**Interaction**:
- Speak the guardian's name (Charisma 3/3) — peaceful resolution
- Break the wheel (Strength 3/4) — hostile resolution"
1 label body
2 Village ### Village **Type**: Starting location A quiet village at the edge of the marsh. The miller's daughter asks after her father. **Interaction**: Ask about the mill (Charisma 3/1) **Supplies**: Rope, torches, one healing draught
3 Mill Road ### Mill Road **Type**: Route A muddy track winding between hedgerows toward the mill. **Interaction**: Spot tracks (Perception 3/1) **Encounters**: 3 random encounters
4 Ground Floor ### Ground Floor **Type**: Hub Sacks of grain, a cold hearth, and a ladder to the wheel room. **Interaction**: - Search the hearth (Perception 3/2) — find a brass key - Climb the ladder (Agility 3/1)
5 Secret Cellar ### Secret Cellar **Type**: Archive A low cellar beneath the millstones. Ledgers line the walls. **Interaction**: Read the ledgers (Intelligence 3/2) — learn the guardian's name **Clue**: The last entry is dated one year ago, in the miller's hand
6 Wheel Room ### Wheel Room **Type**: Finale The great wheel turns in silence, though no water reaches it. **Interaction**: - Speak the guardian's name (Charisma 3/3) — peaceful resolution - Break the wheel (Strength 3/4) — hostile resolution
+20
View File
@@ -0,0 +1,20 @@
# Column layout
Wrap content in columns using line markers. This is a tttk extension on top of
standard markdown; see `reference.md` for the element and role catalog.
```markdown
|---
Left column
-|-
Right column
--|
```
| Marker | Meaning |
|---|---|
| `\|---` | Start the column container |
| `-\|-` | Divider between columns |
| `--\|` | End the container |
Extra dashes add a `-N` class suffix (e.g. `-|--` → `col-2`) for custom widths.
+39
View File
@@ -0,0 +1,39 @@
# Alerts and mermaid
Standard markdown features that `tttk` renders. They are not tttk syntax — see
`reference.md` for the element and role catalog.
## GitHub-style alerts
```markdown
> [!NOTE]
> 这是一条备注
> [!TIP]
> 提示内容
> [!IMPORTANT]
> 重要信息
> [!WARNING]
> 警告信息
> [!CAUTION]
> 危险警告
```
## Mermaid diagrams
Fenced `mermaid` blocks render as diagrams:
````markdown
```mermaid
graph TD
A[开始] --> B{选择}
B -->|选项 1| C[结果 1]
B -->|选项 2| D[结果 2]
```
````
Supported: flowchart, sequenceDiagram, classDiagram, stateDiagram, erDiagram,
pie.
+365
View File
@@ -0,0 +1,365 @@
# tttk syntax reference
Generated from `src/cli/validate/known.ts` by `npm run gen:reference`.
Do not edit by hand.
Complete catalog of the custom elements, inline directives, and code block
roles that `tttk` understands. Everything not listed here is standard GFM
markdown. Element names are prefixed `md-` and must match exactly.
## Contents
- [Directive syntax](#directive-syntax)
- [Custom elements](#custom-elements)
- [Inline directives](#inline-directives)
- [Code block roles](#code-block-roles)
- [Dice notation](#dice-notation)
- [CSV data files](#csv-data-files)
## Directive syntax
```text
:name[content]{key="value" .class #id}
```
| Part | Meaning | Example |
|---|---|---|
| `:name` | Registered element name | `:md-dice` |
| `[content]` | Directive body | `[2d6+d8]` |
| `{attrs}` | Attributes | `{key="attack"}` |
| `{.class}` | Shorthand for `class` | `{.text-red-500}` |
| `{#id}` | Shorthand for `id` | `{#my-dice}` |
Container directives:
```markdown
::: container
Content here, including nested directives.
:dice[1d20]
:::
```
Nest with more colons (`::::`, `:::::`).
## Custom elements
| Directive | Body | Key attributes |
|---|---|---|
| `:md-dice` | dice formula | `key` |
| `:md-table` | CSV path or inline id | `roll`, `remix` |
| `:md-deck` | CSV path | `size`, `grid`, `layers` |
| `:md-link` | path, optional #section | — |
| `:md-embed` | path, optional #section | `headingBase` |
| `:md-bg` | image path or CSS color | `fit` |
| `:md-border` | color or border image | `width`, `slice` |
| `:md-font` | font family | `source`, `weight`, `color` |
| `:md-pins` | image path | `pins`, `fixed`, `labelStart` |
| `:md-commander` | command template CSV | `placeholder`, `class`, `height` |
| `:md-yarn-spinner` | Yarn Spinner file | `start` |
### `:md-dice`
Clickable dice roller; `key` stores the result in the URL.
Clicking the text rolls; clicking the 🎲 resets to the formula. With `key`, the result is stored in the URL as `?dice-<key>=<value>`.
- Body: dice formula
- Attributes: `key`
```markdown
:md-dice[1d20+5]{key="attack"}
```
```markdown
:md-dice[2d6+3]
```
```markdown
:md-dice[3d6kh1]
```
### `:md-table`
Tab-separated view of a CSV with optional group tabs.
`roll` adds a 🎲 random-tab button; `remix` makes `{{prop}}` references pull from a random row. A `group` column in the CSV produces group tabs automatically.
- Body: CSV path or inline id
- Attributes: `roll`, `remix`
```markdown
:md-table[./npcs.csv]
```
```markdown
:md-table[./encounters.csv]{roll=true}
```
```markdown
:md-table[./quests.csv]{roll=true remix=true}
```
### `:md-deck`
Card layout from a CSV (legacy; prefer `yaml role=tag`).
Prefer the `yaml role=tag` form — it is the only form that supports template layers, per-layer alignment, and card backs. The directive form is legacy and only supports the compact `layers` string.
- Body: CSV path
- Attributes: `size`, `grid`, `layers`
```markdown
:md-deck[./cards.csv]{size="63x88" grid="5x5" layers="title:1,1-5,1f8 body:1,5-5,8f3"}
```
### `:md-link`
Clickable link that expands the target article inline.
- Body: path, optional #section
```markdown
:md-link[./rules.md]
```
```markdown
:md-link[./rules.md#combat]
```
### `:md-embed`
Renders the target article or section inline.
`headingBase` shifts the heading levels of the embedded content.
- Body: path, optional #section
- Attributes: `headingBase`
```markdown
:md-embed[./rules.md#combat]{headingBase=1}
```
### `:md-bg`
Sets the surrounding article card background.
`fit` is one of `cover` (default), `contain`, `fill`, `none`, `scale-down`.
- Body: image path or CSS color
- Attributes: `fit`
- Body may also be a CSS color (e.g. `#2a1a3a`, `crimson`).
```markdown
:md-bg[./images/dungeon-bg.jpg]{fit="cover"}
```
```markdown
:md-bg[#2a1a3a]
```
### `:md-border`
Applies a border to the article card.
Body is a CSS color or a border-image path. Class flags select sides (`t b l r all`), style (`solid dashed dotted double groove ridge inset outset none`), and image repeat (`stretch repeat round space`).
- Body: color or border image
- Attributes: `width`, `slice`
- Body may also be a CSS color (e.g. `#2a1a3a`, `crimson`).
```markdown
:md-border[crimson]{.b}
```
```markdown
:md-border[./frame.png]{width="2mm" slice="10" .all.round}
```
### `:md-font`
Sets the article font.
`source` is `google`, `emfont`, or `local` (default).
- Body: font family
- Attributes: `source`, `weight`, `color`
```markdown
:md-font[Noto Sans SC]{source="google" weight="700" color="#1a1a2e"}
```
### `:md-pins`
Numbered markers on an image.
`pins` is a space-separated list of `label:x,y` (percent coordinates). `fixed` makes it read-only; otherwise click to add and click a pin to remove. `labelStart` sets the first label (letter or number).
- Body: image path
- Attributes: `pins`, `fixed`, `labelStart`
```markdown
:md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30 C:50,80" fixed}
```
```markdown
:md-pins[./images/dungeon.png]{labelStart="1"}
```
### `:md-commander`
Interactive command history / tracker panel.
The body is a CSV of command templates (`command,parameter,label,description,insertedText`).
- Body: command template CSV
- Attributes: `placeholder`, `class`, `height`
```markdown
:md-commander[./commands.csv]{placeholder="输入命令…" height="400px"}
```
### `:md-yarn-spinner`
Runs a Yarn Spinner dialogue file.
`start` names the entry node (default `start`).
- Body: Yarn Spinner file
- Attributes: `start`
```markdown
:md-yarn-spinner[./story.yarn]{start="start"}
```
## Inline directives
### `:[name]`
Inserts an icon looked up under `assets/` (searching upward from the file).
Default extension is `.png`; `.svg`, `.gif`, `.jpg`, `.jpeg`, and `.webp` are also supported. `label` overlays text, `repeat` duplicates N times, and `join` places a tag name between repeats.
- Body: icon name
- Attributes: `label`, `repeat`, `join`, `class`
```markdown
:[attack] :[shield.svg] :[potion]
```
```markdown
:[attack]{label=ATK repeat=3 join=br}
```
### `:cmd[command]`
Renders a clickable span that dispatches a journal command.
- Body: journal command
- Attributes: `label`
```markdown
:cmd[roll 1d20+5]{label=Roll initiative}
```
```markdown
:cmd[spark dungeon room-type]{label=Random room}
```
## Code block roles
The `role=` attribute on the info string determines how a fenced block is
handled.
### `role=spark-table`
CSV (or markdown pipe table) rendered as `:md-table`.
The first column header must be a dice formula; each row's first cell is the roll value (a number, or a range like `1-3`). Extra fence attributes are forwarded to the generated `:md-table`, e.g. ` ```csv role=spark-table roll=true `.
````markdown
```csv role=spark-table
d6,结果
1,遭遇强盗
2,平安无事
```
````
````markdown
```markdown role=spark-table
| d6 | 结果 |
|----|------|
| 1 | 遭遇强盗 |
| 2 | 平安无事 |
```
````
### `role=declare`
Reactive variable / tag-modifier definitions.
Columns (any order): `tag`, `threshold`, `key`, `expr`. A row with an empty `tag` declares a variable; a row with a `#tag` applies a modifier when that tag is active.
````markdown
```csv role=declare
tag,threshold,key,expr
,,$hp,$con*5+$mod_hp
#warrior,2,$mod_hp,20
```
````
### `role=file`
Stored as inline text content and stripped.
Referenced by the derived inline id, e.g. `:md-embed[./text_<hash>]`.
````markdown
```text role=file
Stored as inline content.
```
````
### `role=tag`
YAML rendered as a custom element.
`tag:` names the element, `body:` becomes its content, and any other key becomes an attribute. Fence-line attributes override the YAML body. See `yaml-tag.md`.
````markdown
```yaml role=tag
tag: md-deck
body: ./cards.csv
size: 63x88
grid: 5x5
```
````
### No role / unknown role
No role → a normal visible code block. Unknown role → warned about and
stripped.
## Dice notation
Used by `:md-dice`, spark-table headers, and the journal `/roll`:
| Notation | Meaning |
|---|---|
| `2d6` | two six-sided dice |
| `1d20+5` | d20 plus modifier |
| `3d6kh1` | keep highest 1 |
| `3d6dl2-1` | drop lowest 2, minus 1 |
| `{d4, d8}` | choose one of the listed dice |
## CSV data files
Keep CSV files clean — a header row and data rows. Put deck configuration
and templates in a `yaml role=tag` block's `data-config`.
- `label` — unique row id (number or text).
- `body` — main content, supports markdown.
- Any other columns are custom fields.
- `{{prop}}` in `body` interpolates another column (or front-matter key)
from the same row; with `{remix}` it pulls from a random row.
- Wrap values containing `,`, `"`, or newlines in double quotes; escape
`"` as `""`. Lines beginning with `#` are comments.
+66
View File
@@ -0,0 +1,66 @@
# YAML tag blocks
`` ```yaml role=tag `` renders a custom element from YAML. `tag` names the
element, `body` becomes its content, and any other key becomes an attribute.
Fence-line attributes override the YAML body. See `reference.md` for the
element and role catalog.
````markdown
```yaml role=tag
tag: md-deck
body: ./cards.csv
data-config:
size: 63x88
grid: 5x5
layers:
- prop: title
pos: 1,1-5,1
font: 12
- template: |
**{{name}}** — {{type}}
{{description}}
pos: 1,3-5,8
font: 3
align: l
```
````
Simple cases can stay on the fence line:
````markdown
```yaml role=tag tag=md-dice body=3d6
```
````
## `data-config` layers
Layers accept:
- `prop` — read a CSV column, or `template` — markdown with `{{var}}`
substitution.
- `pos` — `x1,y1-x2,y2` grid span (1-based).
- `font` / `fontSize` — font size in mm.
- `orientation` — `n` / `s` / `e` / `w`.
- `align` — `l` / `c` / `r`, optionally prefixed `t` / `b`.
- `visible` — show or hide the layer.
Use `back_layers` for card backs.
## Legacy `:md-deck` directive
`:md-deck[./cards.csv]{...}` still works but is **legacy** — prefer the YAML tag
block above. It only supports the compact `layers` string and cannot express
template layers or backs.
```markdown
:md-deck[./cards.csv]{size="63x88" grid="5x5" layers="title:1,1-5,1f8 body:1,5-5,8f3"}
```
Compact layer syntax: `prop:x1,y1-x2,y2[f<size>][nsew][[t|b]lcr]`
- `prop` — CSV column to render.
- `x1,y1-x2,y2` — grid span (1-based).
- `f<size>` — font size in mm, e.g. `f8`.
- `nsew` — text orientation (default `n`).
- `l` / `c` / `r` — horizontal align, optionally prefixed `t` / `b` for vertical
(`tl`, `bc`, …).