diff --git a/docs/check-plan.md b/docs/check-plan.md index dd12347..655cc8b 100644 --- a/docs/check-plan.md +++ b/docs/check-plan.md @@ -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 the code catalog (`reference.md`) with the diagnostic codes. - `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 diff --git a/docs/reference.md b/docs/reference.md index 882254c..1a7a70c 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -3,8 +3,18 @@ Generated from `src/cli/validate/known.ts` by `npm run gen:reference`. Do not edit by hand. -Everything not listed here is standard GFM markdown. Element names are -prefixed `md-` and must match exactly. +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 @@ -12,9 +22,24 @@ prefixed `md-` and must match exactly. :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 with `::::`, `:::::`. +| 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 @@ -36,6 +61,8 @@ prefixed `md-` and must match exactly. 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-=`. + - Body: dice formula - 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. +`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` @@ -74,6 +103,8 @@ Tab-separated view of a CSV with optional group tabs. 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` @@ -99,6 +130,8 @@ Clickable link that expands the target article inline. Renders the target article or section inline. +`headingBase` shifts the heading levels of the embedded content. + - Body: path, optional #section - Attributes: `headingBase` @@ -110,6 +143,8 @@ Renders the target article or section inline. 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`). @@ -126,6 +161,8 @@ Sets the surrounding article card background. 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`). @@ -142,6 +179,8 @@ Applies a border to the article card. Sets the article font. +`source` is `google`, `emfont`, or `local` (default). + - Body: font family - Attributes: `source`, `weight`, `color` @@ -153,6 +192,8 @@ Sets the article font. 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` @@ -168,6 +209,8 @@ Numbered markers on an image. 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` @@ -179,6 +222,8 @@ Interactive command history / tracker panel. Runs a Yarn Spinner dialogue file. +`start` names the entry node (default `start`). + - Body: Yarn Spinner file - Attributes: `start` @@ -188,24 +233,38 @@ Runs a Yarn Spinner dialogue file. ## Inline directives -### Icons — `:[name]` +### `:[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. + +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] ``` -### Command links — `:cmd[command]{label=...}` +```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 @@ -215,6 +274,8 @@ handled. 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,结果 @@ -236,6 +297,8 @@ d6,结果 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 @@ -248,6 +311,8 @@ tag,threshold,key,expr Stored as inline text content and stripped. +Referenced by the derived inline id, e.g. `:md-embed[./text_]`. + ````markdown ```text role=file Stored as inline content. @@ -258,6 +323,8 @@ Stored as inline content. 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 @@ -272,6 +339,18 @@ grid: 5x5 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 diff --git a/docs/skill-reference-plan.md b/docs/skill-reference-plan.md new file mode 100644 index 0000000..55cd761 --- /dev/null +++ b/docs/skill-reference-plan.md @@ -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. \ No newline at end of file diff --git a/package.json b/package.json index 841c3cb..58d30a5 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "ttrpg": "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:skill-reference": "node --loader ts-node/esm ./scripts/generate-reference.ts --out ~/.agents/skills/tttk-authoring/reference.md", "test": "jest", "prepare": "npx husky", "prepublishOnly": "npm run build" diff --git a/scripts/generate-reference.ts b/scripts/generate-reference.ts index 3b427df..9e1989e 100644 --- a/scripts/generate-reference.ts +++ b/scripts/generate-reference.ts @@ -7,6 +7,7 @@ */ import { mkdirSync, writeFileSync } from "fs"; +import { homedir } from "os"; import { dirname, resolve } from "path"; 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; } -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 }); writeFileSync(out, renderReference(), "utf-8"); console.log(`wrote ${out}`); diff --git a/src/cli/validate/known.ts b/src/cli/validate/known.ts index 9c7fb50..5b961f6 100644 --- a/src/cli/validate/known.ts +++ b/src/cli/validate/known.ts @@ -18,6 +18,8 @@ export interface ElementSpec { /** Key attribute names. */ attrs: string[]; summary: string; + /** Authoring prose rendered under the summary. */ + details?: string; refKind: RefKind; /** Canonical usage examples, rendered into the reference. */ examples: string[]; @@ -30,6 +32,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [ body: "dice formula", attrs: ["key"], 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-=`.", refKind: "none", examples: [ ':md-dice[1d20+5]{key="attack"}', @@ -42,6 +46,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [ body: "CSV path or inline id", attrs: ["roll", "remix"], 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", examples: [ ":md-table[./npcs.csv]", @@ -54,6 +60,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [ body: "CSV path", attrs: ["size", "grid", "layers"], 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", examples: [ ':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", attrs: ["headingBase"], summary: "Renders the target article or section inline.", + details: "`headingBase` shifts the heading levels of the embedded content.", refKind: "md", examples: [":md-embed[./rules.md#combat]{headingBase=1}"], }, @@ -80,6 +89,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [ body: "image path or CSS color", attrs: ["fit"], summary: "Sets the surrounding article card background.", + details: + "`fit` is one of `cover` (default), `contain`, `fill`, `none`, `scale-down`.", refKind: "image", 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", attrs: ["width", "slice"], 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", examples: [ ":md-border[crimson]{.b}", @@ -99,6 +112,7 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [ body: "font family", attrs: ["source", "weight", "color"], summary: "Sets the article font.", + details: "`source` is `google`, `emfont`, or `local` (default).", refKind: "none", examples: [ ':md-font[Noto Sans SC]{source="google" weight="700" color="#1a1a2e"}', @@ -109,6 +123,8 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [ body: "image path", attrs: ["pins", "fixed", "labelStart"], 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", examples: [ ':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", attrs: ["placeholder", "class", "height"], summary: "Interactive command history / tracker panel.", + details: + "The body is a CSV of command templates (`command,parameter,label,description,insertedText`).", refKind: "csv", examples: [':md-commander[./commands.csv]{placeholder="输入命令…" height="400px"}'], }, @@ -128,6 +146,7 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [ body: "Yarn Spinner file", attrs: ["start"], summary: "Runs a Yarn Spinner dialogue file.", + details: "`start` names the entry node (default `start`).", refKind: "yarn", examples: [':md-yarn-spinner[./story.yarn]{start="start"}'], }, @@ -151,11 +170,49 @@ export const COLOR_BODY_ELEMENTS: ReadonlySet = new Set([ "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 = new Set(["cmd"]); + export interface RoleSpec { role: string; /** Example info string, e.g. "csv role=spark-table". */ info: string; summary: string; + /** Authoring prose rendered under the summary. */ + details?: string; /** Canonical fenced-block examples, rendered into the reference. */ examples: string[]; } @@ -166,6 +223,8 @@ export const ROLE_SPECS: readonly RoleSpec[] = [ role: "spark-table", info: "csv role=spark-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: [ "```csv role=spark-table\nd6,结果\n1,遭遇强盗\n2,平安无事\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", info: "csv role=declare", 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: [ "```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", info: " role=file", summary: "Stored as inline text content and stripped.", + details: + "Referenced by the derived inline id, e.g. `:md-embed[./text_]`.", examples: ["```text role=file\nStored as inline content.\n```"], }, { role: "tag", info: "yaml role=tag", 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: [ "```yaml role=tag\ntag: md-deck\nbody: ./cards.csv\nsize: 63x88\ngrid: 5x5\n```", ], @@ -199,9 +264,6 @@ export const KNOWN_ROLES: ReadonlySet = new Set( ROLE_SPECS.map((r) => r.role), ); -/** Inline directives that are not custom elements. */ -export const KNOWN_INLINE: ReadonlySet = new Set(["cmd"]); - /** Expected file extensions per reference kind. */ export const REF_EXTENSIONS: Record, readonly string[]> = { @@ -211,6 +273,20 @@ export const REF_EXTENSIONS: Record, readonly string[]> 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. */ export function isColorValue(value: string): boolean { const v = value.trim(); @@ -230,4 +306,4 @@ export function isColorValue(value: string): boolean { export function looksLikeColor(value: string): boolean { const v = value.trim(); return isColorValue(v) || /^[A-Za-z]+$/.test(v); -} +} \ No newline at end of file diff --git a/src/cli/validate/reference.ts b/src/cli/validate/reference.ts index 568d512..f5f65ef 100644 --- a/src/cli/validate/reference.ts +++ b/src/cli/validate/reference.ts @@ -2,15 +2,18 @@ * Render the `known.ts` catalog to markdown. * * `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 - * `npm run gen:reference -- --out `. + * with `npm run gen:reference`. The skill copy is refreshed with + * `npm run gen:skill-reference`. */ import { COLOR_BODY_ELEMENTS, + DICE_NOTATION, + INLINE_SPECS, KNOWN_ELEMENTS, ROLE_SPECS, type ElementSpec, + type InlineSpec, } from "./known.js"; /** 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}`; } +/** `- 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 { const lines: string[] = []; lines.push(`### \`:${element.name}\``); lines.push(""); lines.push(element.summary); lines.push(""); - lines.push(`- Body: ${element.body}`); - if (element.attrs.length) { - lines.push(`- Attributes: ${element.attrs.map((a) => `\`${a}\``).join(", ")}`); + if (element.details) { + lines.push(element.details); + lines.push(""); } + lines.push(...specBullets(element.body, element.attrs)); if (COLOR_BODY_ELEMENTS.has(element.name)) { 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(); } +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. */ export function renderReference(): string { const lines: string[] = []; @@ -52,18 +84,41 @@ export function renderReference(): string { lines.push("Do not edit by hand."); 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("## Directive syntax"); 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("- `[content]` is the directive body — a path, dice formula, color, etc."); - lines.push("- `{key=\"value\"}` sets attributes; `{.class}` and `{#id}` are shorthands."); - lines.push("- Container directives use `::: name` … `:::`; nest with `::::`, `:::::`."); + lines.push("| Part | Meaning | Example |"); + lines.push("|---|---|---|"); + 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("## Custom elements"); @@ -84,24 +139,10 @@ export function renderReference(): string { lines.push("## Inline directives"); lines.push(""); - lines.push("### Icons — `:[name]`"); - 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(""); + for (const spec of INLINE_SPECS) { + lines.push(inlineSection(spec)); + lines.push(""); + } lines.push("## Code block roles"); lines.push(""); @@ -115,6 +156,10 @@ export function renderReference(): string { lines.push(""); lines.push(role.summary); lines.push(""); + if (role.details) { + lines.push(role.details); + lines.push(""); + } for (const example of role.examples) { lines.push(fence(example)); lines.push(""); @@ -128,6 +173,17 @@ export function renderReference(): string { lines.push("stripped."); 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(""); lines.push( @@ -149,4 +205,4 @@ export function renderReference(): string { lines.push(""); return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"; -} +} \ No newline at end of file