docs(skill): Generate the tttk reference catalog

Enrich known.ts with element/role details, inline specs, and dice notation, and render them from reference.ts. Add gen:skill-reference and ~ expansion in --out so the skill copy can be regenerated.

The skill's reference.md is now generated; its extra authoring content moved to layout.md, yaml-tag.md, csv.md, and markdown.md.
This commit is contained in:
2026-09-12 17:13:42 +08:00
parent 71542e7453
commit 6d94a99b42
7 changed files with 433 additions and 45 deletions
+3 -1
View File
@@ -368,7 +368,9 @@ Exit code is the contract that makes `check` usable in an agent loop.
write → check → fix. Remove the "compile is a no-op stub" note. Cross-link write → check → fix. Remove the "compile is a no-op stub" note. Cross-link
the code catalog (`reference.md`) with the diagnostic codes. the code catalog (`reference.md`) with the diagnostic codes.
- `reference.md` is generated from `known.ts` (§7.11) — do not hand-edit. The - `reference.md` is generated from `known.ts` (§7.11) — do not hand-edit. The
skill's copy is refreshed by `npm run gen:reference`. skill's copy is refreshed by `npm run gen:skill-reference`; deeper authoring
topics live in the skill's `yaml-tag.md`, `csv.md`, `layout.md`, and
`markdown.md` (see `docs/skill-reference-plan.md`).
## 10. Resolved decisions ## 10. Resolved decisions
+88 -9
View File
@@ -3,8 +3,18 @@
Generated from `src/cli/validate/known.ts` by `npm run gen:reference`. Generated from `src/cli/validate/known.ts` by `npm run gen:reference`.
Do not edit by hand. Do not edit by hand.
Everything not listed here is standard GFM markdown. Element names are Complete catalog of the custom elements, inline directives, and code block
prefixed `md-` and must match exactly. 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 ## Directive syntax
@@ -12,9 +22,24 @@ prefixed `md-` and must match exactly.
:name[content]{key="value" .class #id} :name[content]{key="value" .class #id}
``` ```
- `[content]` is the directive body — a path, dice formula, color, etc. | Part | Meaning | Example |
- `{key="value"}` sets attributes; `{.class}` and `{#id}` are shorthands. |---|---|---|
- Container directives use `::: name` … `:::`; nest with `::::`, `:::::`. | `: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 ## Custom elements
@@ -36,6 +61,8 @@ prefixed `md-` and must match exactly.
Clickable dice roller; `key` stores the result in the URL. 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 - Body: dice formula
- Attributes: `key` - Attributes: `key`
@@ -55,6 +82,8 @@ Clickable dice roller; `key` stores the result in the URL.
Tab-separated view of a CSV with optional group tabs. 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 - Body: CSV path or inline id
- Attributes: `roll`, `remix` - Attributes: `roll`, `remix`
@@ -74,6 +103,8 @@ Tab-separated view of a CSV with optional group tabs.
Card layout from a CSV (legacy; prefer `yaml role=tag`). 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 - Body: CSV path
- Attributes: `size`, `grid`, `layers` - Attributes: `size`, `grid`, `layers`
@@ -99,6 +130,8 @@ Clickable link that expands the target article inline.
Renders the target article or section inline. Renders the target article or section inline.
`headingBase` shifts the heading levels of the embedded content.
- Body: path, optional #section - Body: path, optional #section
- Attributes: `headingBase` - Attributes: `headingBase`
@@ -110,6 +143,8 @@ Renders the target article or section inline.
Sets the surrounding article card background. Sets the surrounding article card background.
`fit` is one of `cover` (default), `contain`, `fill`, `none`, `scale-down`.
- Body: image path or CSS color - Body: image path or CSS color
- Attributes: `fit` - Attributes: `fit`
- Body may also be a CSS color (e.g. `#2a1a3a`, `crimson`). - Body may also be a CSS color (e.g. `#2a1a3a`, `crimson`).
@@ -126,6 +161,8 @@ Sets the surrounding article card background.
Applies a border to the article card. 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 - Body: color or border image
- Attributes: `width`, `slice` - Attributes: `width`, `slice`
- Body may also be a CSS color (e.g. `#2a1a3a`, `crimson`). - Body may also be a CSS color (e.g. `#2a1a3a`, `crimson`).
@@ -142,6 +179,8 @@ Applies a border to the article card.
Sets the article font. Sets the article font.
`source` is `google`, `emfont`, or `local` (default).
- Body: font family - Body: font family
- Attributes: `source`, `weight`, `color` - Attributes: `source`, `weight`, `color`
@@ -153,6 +192,8 @@ Sets the article font.
Numbered markers on an image. 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 - Body: image path
- Attributes: `pins`, `fixed`, `labelStart` - Attributes: `pins`, `fixed`, `labelStart`
@@ -168,6 +209,8 @@ Numbered markers on an image.
Interactive command history / tracker panel. Interactive command history / tracker panel.
The body is a CSV of command templates (`command,parameter,label,description,insertedText`).
- Body: command template CSV - Body: command template CSV
- Attributes: `placeholder`, `class`, `height` - Attributes: `placeholder`, `class`, `height`
@@ -179,6 +222,8 @@ Interactive command history / tracker panel.
Runs a Yarn Spinner dialogue file. Runs a Yarn Spinner dialogue file.
`start` names the entry node (default `start`).
- Body: Yarn Spinner file - Body: Yarn Spinner file
- Attributes: `start` - Attributes: `start`
@@ -188,24 +233,38 @@ Runs a Yarn Spinner dialogue file.
## Inline directives ## Inline directives
### Icons — `:[name]` ### `:[name]`
Inserts an icon looked up under `assets/` (searching upward from the file). 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. 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 ```markdown
:[attack] :[shield.svg] :[potion] :[attack] :[shield.svg] :[potion]
``` ```
### Command links — `:cmd[command]{label=...}` ```markdown
:[attack]{label=ATK repeat=3 join=br}
```
### `:cmd[command]`
Renders a clickable span that dispatches a journal command. Renders a clickable span that dispatches a journal command.
- Body: journal command
- Attributes: `label`
```markdown ```markdown
:cmd[roll 1d20+5]{label=Roll initiative} :cmd[roll 1d20+5]{label=Roll initiative}
``` ```
```markdown
:cmd[spark dungeon room-type]{label=Random room}
```
## Code block roles ## Code block roles
The `role=` attribute on the info string determines how a fenced block is The `role=` attribute on the info string determines how a fenced block is
@@ -215,6 +274,8 @@ handled.
CSV (or markdown pipe table) rendered as `:md-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 ````markdown
```csv role=spark-table ```csv role=spark-table
d6,结果 d6,结果
@@ -236,6 +297,8 @@ d6,结果
Reactive variable / tag-modifier definitions. 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 ````markdown
```csv role=declare ```csv role=declare
tag,threshold,key,expr tag,threshold,key,expr
@@ -248,6 +311,8 @@ tag,threshold,key,expr
Stored as inline text content and stripped. Stored as inline text content and stripped.
Referenced by the derived inline id, e.g. `:md-embed[./text_<hash>]`.
````markdown ````markdown
```text role=file ```text role=file
Stored as inline content. Stored as inline content.
@@ -258,6 +323,8 @@ Stored as inline content.
YAML rendered as a custom element. 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 ````markdown
```yaml role=tag ```yaml role=tag
tag: md-deck tag: md-deck
@@ -272,6 +339,18 @@ grid: 5x5
No role → a normal visible code block. Unknown role → warned about and No role → a normal visible code block. Unknown role → warned about and
stripped. 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 ## CSV data files
Keep CSV files clean — a header row and data rows. Put deck configuration Keep CSV files clean — a header row and data rows. Put deck configuration
+164
View File
@@ -0,0 +1,164 @@
# tttk skill reference — reorganization plan
Status: **implemented.** Follows the `tttk check` work that made
`docs/reference.md` a generated artifact (see `docs/check-plan.md` §7.11).
Landed: `known.ts` carries element/role `details`, inline specs, and dice
notation; `reference.ts` renders them; `scripts/generate-reference.ts` expands
`~` in `--out`; `npm run gen:skill-reference` refreshes the skill copy. The
skill's `reference.md` is now generated and byte-identical to
`docs/reference.md`, with the extra content moved to `layout.md`, `yaml-tag.md`,
`csv.md`, and `markdown.md`.
## 1. Goal
Make the `tttk-authoring` skill's `reference.md` a **generated** file — the same
catalog the validator uses — and relocate the authoring content it does not
cover into focused companion files. One source of truth (`known.ts`), no
hand-maintained element list, and no content lost in the swap.
## 2. Current state
The skill (`~/.agents/skills/tttk-authoring/`) has a hand-written
`reference.md` (~465 lines) and `SKILL.md`. The repo now generates
`docs/reference.md` from `src/cli/validate/known.ts` via `npm run
gen:reference`.
What the generated doc already covers:
- Directive syntax (brief)
- Custom elements: table + per-element summary, body, attrs, examples
- Inline directives: icons, `:cmd` (brief)
- Code block roles: `spark-table`, `declare`, `file`, `tag` (brief)
- CSV data files (brief)
What only the hand-written skill copy has (the "extra content"):
| # | Section | Notes |
|---|---|---|
| A | Directive syntax table + container example | richer than generated |
| B | Per-element prose | md-dice behavior, md-table `roll`/`remix`/`group`, md-deck legacy layer syntax, md-bg `fit` values, md-border class flags, md-font `source` values, md-pins behavior, md-commander CSV columns |
| C | Icon attributes | `label`, `repeat`, `join`, `class` |
| D | Column layout | `\|---` / `-\|-` / `--\|` markers |
| E | `role=declare` column semantics | `tag`/`threshold`/`key`/`expr` |
| F | YAML tag blocks | `data-config`, layers, `back_layers`, fence-line overrides |
| G | CSV authoring | clean-CSV guidance, `label`/`body`, front matter (avoid) |
| H | Alerts and mermaid | GitHub alerts, mermaid diagrams |
| I | Dice notation | notation table |
| J | Validation codes | already duplicated in `SKILL.md` |
## 3. Target skill layout
```
tttk-authoring/
├── SKILL.md # workflow, rules, validation codes, links
├── reference.md # GENERATED from known.ts — elements, roles, inline, dice, CSV basics
├── layout.md # column layout
├── yaml-tag.md # role=tag, data-config, deck layers and backs
├── csv.md # CSV authoring and front matter
├── markdown.md # alerts and mermaid
└── examples/ # unchanged
```
`reference.md` is the only generated file; the rest are hand-written and linked
from `SKILL.md`.
## 4. Content mapping
| Source | Destination |
|---|---|
| A directive syntax | generator (`reference.ts`) — add the part/meaning table + container example |
| B per-element prose | `known.ts` `details` field → rendered into `reference.md` |
| C icon attributes | `known.ts` inline specs → rendered into `reference.md` |
| E declare semantics | `known.ts` `RoleSpec.details` → rendered into `reference.md` |
| I dice notation | `known.ts` `DICE_NOTATION` → rendered into `reference.md` |
| D column layout | `layout.md` |
| F YAML tag blocks | `yaml-tag.md` |
| G CSV authoring | `csv.md` |
| H alerts and mermaid | `markdown.md` |
| J validation codes | dropped — `SKILL.md` owns the code tables |
**Decision (recommended):** fold B/C/E/I into `known.ts` rather than a separate
`components.md`. It keeps the generated reference self-sufficient and avoids a
second, drifting description of the same elements. The alternative — a
hand-written `components.md` — is rejected because it reintroduces the drift the
generator exists to prevent.
## 5. Generator changes
`src/cli/validate/known.ts`:
- `ElementSpec.details?: string` — one paragraph of authoring prose.
- `ElementSpec.attrs` → optional richer `{ name, description }[]` so the
reference can render an attribute table (keep the plain list if not worth it).
- `InlineSpec` for `:[icon]` and `:cmd` with body/attrs/summary/examples.
- `RoleSpec.details?: string` for `declare` column semantics.
- `DICE_NOTATION: { notation, meaning }[]`.
`src/cli/validate/reference.ts`:
- Render `details`, the attribute table, inline specs, and dice notation.
- Add a `Contents` TOC.
- Upgrade the directive-syntax section (table + container example).
`scripts/generate-reference.ts`:
- Expand a leading `~` in `--out` to `os.homedir()` so the skill copy can be
regenerated cross-platform.
`package.json`:
- `gen:reference` — unchanged (writes `docs/reference.md`).
- `gen:skill-reference` — `gen:reference -- --out ~/.agents/skills/tttk-authoring/reference.md`.
## 6. New skill file outlines
- **`layout.md`** — column markers, the `-N` width suffix, a worked example.
- **`yaml-tag.md`** — `role=tag` shape (`tag:`, `body:`, other keys as attrs),
fence-line overrides, `data-config` layer fields (`prop`/`template`, `pos`,
`font`/`fontSize`, `orientation`, `align`, `visible`), `back_layers`, and the
legacy `:md-deck` compact layer syntax.
- **`csv.md`** — clean-CSV guidance, `label`/`body` conventions, `group` tabs,
`{{prop}}` interpolation, quoting rules, and the discouraged front-matter form.
- **`markdown.md`** — GitHub alerts and mermaid (standard markdown, not tttk
syntax).
## 7. SKILL.md changes
- Update the "Reference" section to link `reference.md` plus the four new files.
- Keep the validation code tables in `SKILL.md` (they are the agent's quick
reference); `reference.md` no longer carries them.
- Reword "check `reference.md` when unsure" to point at the right file per topic.
## 8. Steps
1. Enrich `known.ts` (details, inline specs, role details, dice notation).
2. Update `reference.ts` and `generate-reference.ts`; regenerate
`docs/reference.md`; keep the `known.test.ts` drift guard green.
3. Extract D/F/G/H from the old skill `reference.md` into `layout.md`,
`yaml-tag.md`, `csv.md`, `markdown.md`.
4. Regenerate the skill's `reference.md` with `npm run gen:skill-reference`.
5. Update `SKILL.md` links.
6. Verify (§9).
## 9. Verification
- `npm run gen:reference` leaves `docs/reference.md` unchanged (idempotent).
- `npx jest` passes, including the reference drift guard.
- Diff the old skill `reference.md` against the new file set and confirm every
section in §2's table has a destination — no content silently dropped.
- `npx tttk check ./content` still runs clean (the catalog changes are
documentation-only, but the drift guard protects `known.ts`).
## 10. Risks
- **Content loss during extraction.** Mitigate with the §4 mapping as a
checklist and a manual diff before deleting the old file.
- **`known.ts` bloat.** Keep `details` to one paragraph per element; long-form
material belongs in the companion files.
- **Skill path portability.** `~` expansion in the generator; the skill lives
outside the repo, so the drift guard cannot cover it — document that
`gen:skill-reference` is the refresh command.
- **Two references.** `docs/reference.md` and the skill copy are both generated
from `known.ts`; they must be regenerated together. Note this in
`docs/check-plan.md` §9.
+1
View File
@@ -30,6 +30,7 @@
"ttrpg": "node --loader ts-node/esm ./src/cli/index.ts", "ttrpg": "node --loader ts-node/esm ./src/cli/index.ts",
"tttk": "node --loader ts-node/esm ./src/cli/index.ts", "tttk": "node --loader ts-node/esm ./src/cli/index.ts",
"gen:reference": "node --loader ts-node/esm ./scripts/generate-reference.ts", "gen:reference": "node --loader ts-node/esm ./scripts/generate-reference.ts",
"gen:skill-reference": "node --loader ts-node/esm ./scripts/generate-reference.ts --out ~/.agents/skills/tttk-authoring/reference.md",
"test": "jest", "test": "jest",
"prepare": "npx husky", "prepare": "npx husky",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
+11 -1
View File
@@ -7,6 +7,7 @@
*/ */
import { mkdirSync, writeFileSync } from "fs"; import { mkdirSync, writeFileSync } from "fs";
import { homedir } from "os";
import { dirname, resolve } from "path"; import { dirname, resolve } from "path";
import { renderReference } from "../src/cli/validate/reference.js"; import { renderReference } from "../src/cli/validate/reference.js";
@@ -15,7 +16,16 @@ function argValue(flag: string): string | undefined {
return index >= 0 ? process.argv[index + 1] : undefined; return index >= 0 ? process.argv[index + 1] : undefined;
} }
const out = resolve(argValue("--out") ?? "docs/reference.md"); /** Expand a leading `~` so the skill copy can be targeted cross-platform. */
function expandHome(path: string): string {
if (path === "~") return homedir();
if (path.startsWith("~/") || path.startsWith("~\\")) {
return resolve(homedir(), path.slice(2));
}
return path;
}
const out = resolve(expandHome(argValue("--out") ?? "docs/reference.md"));
mkdirSync(dirname(out), { recursive: true }); mkdirSync(dirname(out), { recursive: true });
writeFileSync(out, renderReference(), "utf-8"); writeFileSync(out, renderReference(), "utf-8");
console.log(`wrote ${out}`); console.log(`wrote ${out}`);
+80 -4
View File
@@ -18,6 +18,8 @@ export interface ElementSpec {
/** Key attribute names. */ /** Key attribute names. */
attrs: string[]; attrs: string[];
summary: string; summary: string;
/** Authoring prose rendered under the summary. */
details?: string;
refKind: RefKind; refKind: RefKind;
/** Canonical usage examples, rendered into the reference. */ /** Canonical usage examples, rendered into the reference. */
examples: string[]; examples: string[];
@@ -30,6 +32,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
body: "dice formula", body: "dice formula",
attrs: ["key"], attrs: ["key"],
summary: "Clickable dice roller; `key` stores the result in the URL.", summary: "Clickable dice roller; `key` stores the result in the URL.",
details:
"Clicking the text rolls; clicking the 🎲 resets to the formula. With `key`, the result is stored in the URL as `?dice-<key>=<value>`.",
refKind: "none", refKind: "none",
examples: [ examples: [
':md-dice[1d20+5]{key="attack"}', ':md-dice[1d20+5]{key="attack"}',
@@ -42,6 +46,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
body: "CSV path or inline id", body: "CSV path or inline id",
attrs: ["roll", "remix"], attrs: ["roll", "remix"],
summary: "Tab-separated view of a CSV with optional group tabs.", summary: "Tab-separated view of a CSV with optional group tabs.",
details:
"`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.",
refKind: "csv", refKind: "csv",
examples: [ examples: [
":md-table[./npcs.csv]", ":md-table[./npcs.csv]",
@@ -54,6 +60,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
body: "CSV path", body: "CSV path",
attrs: ["size", "grid", "layers"], attrs: ["size", "grid", "layers"],
summary: "Card layout from a CSV (legacy; prefer `yaml role=tag`).", summary: "Card layout from a CSV (legacy; prefer `yaml role=tag`).",
details:
"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.",
refKind: "csv", refKind: "csv",
examples: [ examples: [
':md-deck[./cards.csv]{size="63x88" grid="5x5" layers="title:1,1-5,1f8 body:1,5-5,8f3"}', ':md-deck[./cards.csv]{size="63x88" grid="5x5" layers="title:1,1-5,1f8 body:1,5-5,8f3"}',
@@ -72,6 +80,7 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
body: "path, optional #section", body: "path, optional #section",
attrs: ["headingBase"], attrs: ["headingBase"],
summary: "Renders the target article or section inline.", summary: "Renders the target article or section inline.",
details: "`headingBase` shifts the heading levels of the embedded content.",
refKind: "md", refKind: "md",
examples: [":md-embed[./rules.md#combat]{headingBase=1}"], examples: [":md-embed[./rules.md#combat]{headingBase=1}"],
}, },
@@ -80,6 +89,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
body: "image path or CSS color", body: "image path or CSS color",
attrs: ["fit"], attrs: ["fit"],
summary: "Sets the surrounding article card background.", summary: "Sets the surrounding article card background.",
details:
"`fit` is one of `cover` (default), `contain`, `fill`, `none`, `scale-down`.",
refKind: "image", refKind: "image",
examples: [':md-bg[./images/dungeon-bg.jpg]{fit="cover"}', ":md-bg[#2a1a3a]"], examples: [':md-bg[./images/dungeon-bg.jpg]{fit="cover"}', ":md-bg[#2a1a3a]"],
}, },
@@ -88,6 +99,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
body: "color or border image", body: "color or border image",
attrs: ["width", "slice"], attrs: ["width", "slice"],
summary: "Applies a border to the article card.", summary: "Applies a border to the article card.",
details:
"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`).",
refKind: "image", refKind: "image",
examples: [ examples: [
":md-border[crimson]{.b}", ":md-border[crimson]{.b}",
@@ -99,6 +112,7 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
body: "font family", body: "font family",
attrs: ["source", "weight", "color"], attrs: ["source", "weight", "color"],
summary: "Sets the article font.", summary: "Sets the article font.",
details: "`source` is `google`, `emfont`, or `local` (default).",
refKind: "none", refKind: "none",
examples: [ examples: [
':md-font[Noto Sans SC]{source="google" weight="700" color="#1a1a2e"}', ':md-font[Noto Sans SC]{source="google" weight="700" color="#1a1a2e"}',
@@ -109,6 +123,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
body: "image path", body: "image path",
attrs: ["pins", "fixed", "labelStart"], attrs: ["pins", "fixed", "labelStart"],
summary: "Numbered markers on an image.", summary: "Numbered markers on an image.",
details:
"`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).",
refKind: "image", refKind: "image",
examples: [ examples: [
':md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30 C:50,80" fixed}', ':md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30 C:50,80" fixed}',
@@ -120,6 +136,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
body: "command template CSV", body: "command template CSV",
attrs: ["placeholder", "class", "height"], attrs: ["placeholder", "class", "height"],
summary: "Interactive command history / tracker panel.", summary: "Interactive command history / tracker panel.",
details:
"The body is a CSV of command templates (`command,parameter,label,description,insertedText`).",
refKind: "csv", refKind: "csv",
examples: [':md-commander[./commands.csv]{placeholder="输入命令…" height="400px"}'], examples: [':md-commander[./commands.csv]{placeholder="输入命令…" height="400px"}'],
}, },
@@ -128,6 +146,7 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
body: "Yarn Spinner file", body: "Yarn Spinner file",
attrs: ["start"], attrs: ["start"],
summary: "Runs a Yarn Spinner dialogue file.", summary: "Runs a Yarn Spinner dialogue file.",
details: "`start` names the entry node (default `start`).",
refKind: "yarn", refKind: "yarn",
examples: [':md-yarn-spinner[./story.yarn]{start="start"}'], examples: [':md-yarn-spinner[./story.yarn]{start="start"}'],
}, },
@@ -151,11 +170,49 @@ export const COLOR_BODY_ELEMENTS: ReadonlySet<string> = new Set([
"md-border", "md-border",
]); ]);
export interface InlineSpec {
/** Directive form, e.g. ":[name]". */
name: string;
body: string;
attrs: string[];
summary: string;
details?: string;
examples: string[];
}
/** Inline directives that are not custom elements. */
export const INLINE_SPECS: readonly InlineSpec[] = [
{
name: ":[name]",
body: "icon name",
attrs: ["label", "repeat", "join", "class"],
summary:
"Inserts an icon looked up under `assets/` (searching upward from the file).",
details:
"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.",
examples: [":[attack] :[shield.svg] :[potion]", ":[attack]{label=ATK repeat=3 join=br}"],
},
{
name: ":cmd[command]",
body: "journal command",
attrs: ["label"],
summary: "Renders a clickable span that dispatches a journal command.",
examples: [
":cmd[roll 1d20+5]{label=Roll initiative}",
":cmd[spark dungeon room-type]{label=Random room}",
],
},
];
export const KNOWN_INLINE: ReadonlySet<string> = new Set(["cmd"]);
export interface RoleSpec { export interface RoleSpec {
role: string; role: string;
/** Example info string, e.g. "csv role=spark-table". */ /** Example info string, e.g. "csv role=spark-table". */
info: string; info: string;
summary: string; summary: string;
/** Authoring prose rendered under the summary. */
details?: string;
/** Canonical fenced-block examples, rendered into the reference. */ /** Canonical fenced-block examples, rendered into the reference. */
examples: string[]; examples: string[];
} }
@@ -166,6 +223,8 @@ export const ROLE_SPECS: readonly RoleSpec[] = [
role: "spark-table", role: "spark-table",
info: "csv role=spark-table", info: "csv role=spark-table",
summary: "CSV (or markdown pipe table) rendered as `:md-table`.", summary: "CSV (or markdown pipe table) rendered as `:md-table`.",
details:
"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 `.",
examples: [ examples: [
"```csv role=spark-table\nd6,结果\n1,遭遇强盗\n2,平安无事\n```", "```csv role=spark-table\nd6,结果\n1,遭遇强盗\n2,平安无事\n```",
"```markdown role=spark-table\n| d6 | 结果 |\n|----|------|\n| 1 | 遭遇强盗 |\n| 2 | 平安无事 |\n```", "```markdown role=spark-table\n| d6 | 结果 |\n|----|------|\n| 1 | 遭遇强盗 |\n| 2 | 平安无事 |\n```",
@@ -175,6 +234,8 @@ export const ROLE_SPECS: readonly RoleSpec[] = [
role: "declare", role: "declare",
info: "csv role=declare", info: "csv role=declare",
summary: "Reactive variable / tag-modifier definitions.", summary: "Reactive variable / tag-modifier definitions.",
details:
"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.",
examples: [ examples: [
"```csv role=declare\ntag,threshold,key,expr\n,,$hp,$con*5+$mod_hp\n#warrior,2,$mod_hp,20\n```", "```csv role=declare\ntag,threshold,key,expr\n,,$hp,$con*5+$mod_hp\n#warrior,2,$mod_hp,20\n```",
], ],
@@ -183,12 +244,16 @@ export const ROLE_SPECS: readonly RoleSpec[] = [
role: "file", role: "file",
info: "<lang> role=file", info: "<lang> role=file",
summary: "Stored as inline text content and stripped.", summary: "Stored as inline text content and stripped.",
details:
"Referenced by the derived inline id, e.g. `:md-embed[./text_<hash>]`.",
examples: ["```text role=file\nStored as inline content.\n```"], examples: ["```text role=file\nStored as inline content.\n```"],
}, },
{ {
role: "tag", role: "tag",
info: "yaml role=tag", info: "yaml role=tag",
summary: "YAML rendered as a custom element.", summary: "YAML rendered as a custom element.",
details:
"`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`.",
examples: [ examples: [
"```yaml role=tag\ntag: md-deck\nbody: ./cards.csv\nsize: 63x88\ngrid: 5x5\n```", "```yaml role=tag\ntag: md-deck\nbody: ./cards.csv\nsize: 63x88\ngrid: 5x5\n```",
], ],
@@ -199,9 +264,6 @@ export const KNOWN_ROLES: ReadonlySet<string> = new Set(
ROLE_SPECS.map((r) => r.role), ROLE_SPECS.map((r) => r.role),
); );
/** Inline directives that are not custom elements. */
export const KNOWN_INLINE: ReadonlySet<string> = new Set(["cmd"]);
/** Expected file extensions per reference kind. */ /** Expected file extensions per reference kind. */
export const REF_EXTENSIONS: Record<Exclude<RefKind, "none">, readonly string[]> = export const REF_EXTENSIONS: Record<Exclude<RefKind, "none">, readonly string[]> =
{ {
@@ -211,6 +273,20 @@ export const REF_EXTENSIONS: Record<Exclude<RefKind, "none">, readonly string[]>
image: [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"], image: [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"],
}; };
export interface DiceNotation {
notation: string;
meaning: string;
}
/** Dice notation accepted by `:md-dice`, spark-table headers, and `/roll`. */
export const DICE_NOTATION: readonly DiceNotation[] = [
{ notation: "2d6", meaning: "two six-sided dice" },
{ notation: "1d20+5", meaning: "d20 plus modifier" },
{ notation: "3d6kh1", meaning: "keep highest 1" },
{ notation: "3d6dl2-1", meaning: "drop lowest 2, minus 1" },
{ notation: "{d4, d8}", meaning: "choose one of the listed dice" },
];
/** Whether a directive body is a CSS color rather than a path. */ /** Whether a directive body is a CSS color rather than a path. */
export function isColorValue(value: string): boolean { export function isColorValue(value: string): boolean {
const v = value.trim(); const v = value.trim();
@@ -230,4 +306,4 @@ export function isColorValue(value: string): boolean {
export function looksLikeColor(value: string): boolean { export function looksLikeColor(value: string): boolean {
const v = value.trim(); const v = value.trim();
return isColorValue(v) || /^[A-Za-z]+$/.test(v); return isColorValue(v) || /^[A-Za-z]+$/.test(v);
} }
+86 -30
View File
@@ -2,15 +2,18 @@
* Render the `known.ts` catalog to markdown. * Render the `known.ts` catalog to markdown.
* *
* `docs/reference.md` is generated from this; do not hand-edit it. Regenerate * `docs/reference.md` is generated from this; do not hand-edit it. Regenerate
* with `npm run gen:reference`. The skill copy can be refreshed with * with `npm run gen:reference`. The skill copy is refreshed with
* `npm run gen:reference -- --out <path>`. * `npm run gen:skill-reference`.
*/ */
import { import {
COLOR_BODY_ELEMENTS, COLOR_BODY_ELEMENTS,
DICE_NOTATION,
INLINE_SPECS,
KNOWN_ELEMENTS, KNOWN_ELEMENTS,
ROLE_SPECS, ROLE_SPECS,
type ElementSpec, type ElementSpec,
type InlineSpec,
} from "./known.js"; } from "./known.js";
/** Wrap `code` in a fenced block, widening the fence when it contains one. */ /** Wrap `code` in a fenced block, widening the fence when it contains one. */
@@ -19,16 +22,26 @@ function fence(code: string, lang = "markdown"): string {
return `${ticks}${lang}\n${code}\n${ticks}`; return `${ticks}${lang}\n${code}\n${ticks}`;
} }
/** `- Body: …` / `- Attributes: …` bullet list shared by all directive specs. */
function specBullets(body: string, attrs: string[]): string[] {
const lines = [`- Body: ${body}`];
if (attrs.length) {
lines.push(`- Attributes: ${attrs.map((a) => `\`${a}\``).join(", ")}`);
}
return lines;
}
function elementSection(element: ElementSpec): string { function elementSection(element: ElementSpec): string {
const lines: string[] = []; const lines: string[] = [];
lines.push(`### \`:${element.name}\``); lines.push(`### \`:${element.name}\``);
lines.push(""); lines.push("");
lines.push(element.summary); lines.push(element.summary);
lines.push(""); lines.push("");
lines.push(`- Body: ${element.body}`); if (element.details) {
if (element.attrs.length) { lines.push(element.details);
lines.push(`- Attributes: ${element.attrs.map((a) => `\`${a}\``).join(", ")}`); lines.push("");
} }
lines.push(...specBullets(element.body, element.attrs));
if (COLOR_BODY_ELEMENTS.has(element.name)) { if (COLOR_BODY_ELEMENTS.has(element.name)) {
lines.push("- Body may also be a CSS color (e.g. `#2a1a3a`, `crimson`)."); lines.push("- Body may also be a CSS color (e.g. `#2a1a3a`, `crimson`).");
} }
@@ -40,6 +53,25 @@ function elementSection(element: ElementSpec): string {
return lines.join("\n").trimEnd(); return lines.join("\n").trimEnd();
} }
function inlineSection(spec: InlineSpec): string {
const lines: string[] = [];
lines.push(`### \`${spec.name}\``);
lines.push("");
lines.push(spec.summary);
lines.push("");
if (spec.details) {
lines.push(spec.details);
lines.push("");
}
lines.push(...specBullets(spec.body, spec.attrs));
lines.push("");
for (const example of spec.examples) {
lines.push(fence(example));
lines.push("");
}
return lines.join("\n").trimEnd();
}
/** Render the full reference document. */ /** Render the full reference document. */
export function renderReference(): string { export function renderReference(): string {
const lines: string[] = []; const lines: string[] = [];
@@ -52,18 +84,41 @@ export function renderReference(): string {
lines.push("Do not edit by hand."); lines.push("Do not edit by hand.");
lines.push(""); lines.push("");
lines.push( lines.push(
"Everything not listed here is standard GFM markdown. Element names are", "Complete catalog of the custom elements, inline directives, and code block",
); );
lines.push("prefixed `md-` and must match exactly."); lines.push(
"roles that `tttk` understands. Everything not listed here is standard GFM",
);
lines.push("markdown. Element names are prefixed `md-` and must match exactly.");
lines.push("");
lines.push("## Contents");
lines.push("");
lines.push("- [Directive syntax](#directive-syntax)");
lines.push("- [Custom elements](#custom-elements)");
lines.push("- [Inline directives](#inline-directives)");
lines.push("- [Code block roles](#code-block-roles)");
lines.push("- [Dice notation](#dice-notation)");
lines.push("- [CSV data files](#csv-data-files)");
lines.push(""); lines.push("");
lines.push("## Directive syntax"); lines.push("## Directive syntax");
lines.push(""); lines.push("");
lines.push(fence(":name[content]{key=\"value\" .class #id}", "text")); lines.push(fence(':name[content]{key="value" .class #id}', "text"));
lines.push(""); lines.push("");
lines.push("- `[content]` is the directive body — a path, dice formula, color, etc."); lines.push("| Part | Meaning | Example |");
lines.push("- `{key=\"value\"}` sets attributes; `{.class}` and `{#id}` are shorthands."); lines.push("|---|---|---|");
lines.push("- Container directives use `::: name` … `:::`; nest with `::::`, `:::::`."); lines.push("| `:name` | Registered element name | `:md-dice` |");
lines.push("| `[content]` | Directive body | `[2d6+d8]` |");
lines.push("| `{attrs}` | Attributes | `{key=\"attack\"}` |");
lines.push("| `{.class}` | Shorthand for `class` | `{.text-red-500}` |");
lines.push("| `{#id}` | Shorthand for `id` | `{#my-dice}` |");
lines.push("");
lines.push("Container directives:");
lines.push("");
lines.push(fence("::: container\nContent here, including nested directives.\n:dice[1d20]\n:::"));
lines.push("");
lines.push("Nest with more colons (`::::`, `:::::`).");
lines.push(""); lines.push("");
lines.push("## Custom elements"); lines.push("## Custom elements");
@@ -84,24 +139,10 @@ export function renderReference(): string {
lines.push("## Inline directives"); lines.push("## Inline directives");
lines.push(""); lines.push("");
lines.push("### Icons — `:[name]`"); for (const spec of INLINE_SPECS) {
lines.push(""); lines.push(inlineSection(spec));
lines.push( lines.push("");
"Inserts an icon looked up under `assets/` (searching upward from the file).", }
);
lines.push(
"Default extension is `.png`; `.svg`, `.gif`, `.jpg`, `.jpeg`, and `.webp`",
);
lines.push("are also supported.");
lines.push("");
lines.push(fence(":[attack] :[shield.svg] :[potion]"));
lines.push("");
lines.push("### Command links — `:cmd[command]{label=...}`");
lines.push("");
lines.push("Renders a clickable span that dispatches a journal command.");
lines.push("");
lines.push(fence(":cmd[roll 1d20+5]{label=Roll initiative}"));
lines.push("");
lines.push("## Code block roles"); lines.push("## Code block roles");
lines.push(""); lines.push("");
@@ -115,6 +156,10 @@ export function renderReference(): string {
lines.push(""); lines.push("");
lines.push(role.summary); lines.push(role.summary);
lines.push(""); lines.push("");
if (role.details) {
lines.push(role.details);
lines.push("");
}
for (const example of role.examples) { for (const example of role.examples) {
lines.push(fence(example)); lines.push(fence(example));
lines.push(""); lines.push("");
@@ -128,6 +173,17 @@ export function renderReference(): string {
lines.push("stripped."); lines.push("stripped.");
lines.push(""); lines.push("");
lines.push("## Dice notation");
lines.push("");
lines.push("Used by `:md-dice`, spark-table headers, and the journal `/roll`:");
lines.push("");
lines.push("| Notation | Meaning |");
lines.push("|---|---|");
for (const dice of DICE_NOTATION) {
lines.push(`| \`${dice.notation}\` | ${dice.meaning} |`);
}
lines.push("");
lines.push("## CSV data files"); lines.push("## CSV data files");
lines.push(""); lines.push("");
lines.push( lines.push(
@@ -149,4 +205,4 @@ export function renderReference(): string {
lines.push(""); lines.push("");
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"; return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
} }