/** * Catalog of tttk syntax: custom elements, inline directives, and code-block * roles. * * This is the single source of truth for validation. `docs/reference.md` is * generated from it (see docs/check-plan.md §7.11); a drift test asserts this * list matches the `customElement(...)` call sites. */ /** What a directive's body refers to, used for reference checks. */ export type RefKind = "csv" | "md" | "yarn" | "image" | "none"; export interface ElementSpec { /** Registered custom element name, e.g. "md-dice". */ name: string; /** Human description of the body. */ body: string; /** Key attribute names. */ attrs: string[]; summary: string; refKind: RefKind; /** Canonical usage examples, rendered into the reference. */ examples: string[]; } /** The registered `md-*` custom elements (mirrors `customElement(...)` calls). */ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [ { name: "md-dice", body: "dice formula", attrs: ["key"], summary: "Clickable dice roller; `key` stores the result in the URL.", refKind: "none", examples: [ ':md-dice[1d20+5]{key="attack"}', ":md-dice[2d6+3]", ":md-dice[3d6kh1]", ], }, { name: "md-table", body: "CSV path or inline id", attrs: ["roll", "remix"], summary: "Tab-separated view of a CSV with optional group tabs.", refKind: "csv", examples: [ ":md-table[./npcs.csv]", ":md-table[./encounters.csv]{roll=true}", ":md-table[./quests.csv]{roll=true remix=true}", ], }, { name: "md-deck", body: "CSV path", attrs: ["size", "grid", "layers"], summary: "Card layout from a CSV (legacy; prefer `yaml role=tag`).", refKind: "csv", examples: [ ':md-deck[./cards.csv]{size="63x88" grid="5x5" layers="title:1,1-5,1f8 body:1,5-5,8f3"}', ], }, { name: "md-link", body: "path, optional #section", attrs: [], summary: "Clickable link that expands the target article inline.", refKind: "md", examples: [":md-link[./rules.md]", ":md-link[./rules.md#combat]"], }, { name: "md-embed", body: "path, optional #section", attrs: ["headingBase"], summary: "Renders the target article or section inline.", refKind: "md", examples: [":md-embed[./rules.md#combat]{headingBase=1}"], }, { name: "md-bg", body: "image path or CSS color", attrs: ["fit"], summary: "Sets the surrounding article card background.", refKind: "image", examples: [':md-bg[./images/dungeon-bg.jpg]{fit="cover"}', ":md-bg[#2a1a3a]"], }, { name: "md-border", body: "color or border image", attrs: ["width", "slice"], summary: "Applies a border to the article card.", refKind: "image", examples: [ ":md-border[crimson]{.b}", ':md-border[./frame.png]{width="2mm" slice="10" .all.round}', ], }, { name: "md-font", body: "font family", attrs: ["source", "weight", "color"], summary: "Sets the article font.", refKind: "none", examples: [ ':md-font[Noto Sans SC]{source="google" weight="700" color="#1a1a2e"}', ], }, { name: "md-pins", body: "image path", attrs: ["pins", "fixed", "labelStart"], summary: "Numbered markers on an image.", refKind: "image", examples: [ ':md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30 C:50,80" fixed}', ':md-pins[./images/dungeon.png]{labelStart="1"}', ], }, { name: "md-commander", body: "command template CSV", attrs: ["placeholder", "class", "height"], summary: "Interactive command history / tracker panel.", refKind: "csv", examples: [':md-commander[./commands.csv]{placeholder="输入命令…" height="400px"}'], }, { name: "md-yarn-spinner", body: "Yarn Spinner file", attrs: ["start"], summary: "Runs a Yarn Spinner dialogue file.", refKind: "yarn", examples: [':md-yarn-spinner[./story.yarn]{start="start"}'], }, ]; export const ELEMENT_NAMES: ReadonlySet = new Set( KNOWN_ELEMENTS.map((e) => e.name), ); const ELEMENT_BY_NAME: ReadonlyMap = new Map( KNOWN_ELEMENTS.map((e) => [e.name, e]), ); export function elementFor(name: string): ElementSpec | undefined { return ELEMENT_BY_NAME.get(name); } /** Elements whose body may also be a CSS color rather than a file path. */ export const COLOR_BODY_ELEMENTS: ReadonlySet = new Set([ "md-bg", "md-border", ]); export interface RoleSpec { role: string; /** Example info string, e.g. "csv role=spark-table". */ info: string; summary: string; /** Canonical fenced-block examples, rendered into the reference. */ examples: string[]; } /** Code-block `role=` values the scanner understands. */ export const ROLE_SPECS: readonly RoleSpec[] = [ { role: "spark-table", info: "csv role=spark-table", summary: "CSV (or markdown pipe table) rendered as `:md-table`.", examples: [ "```csv role=spark-table\nd6,结果\n1,遭遇强盗\n2,平安无事\n```", "```markdown role=spark-table\n| d6 | 结果 |\n|----|------|\n| 1 | 遭遇强盗 |\n| 2 | 平安无事 |\n```", ], }, { role: "declare", info: "csv role=declare", summary: "Reactive variable / tag-modifier definitions.", examples: [ "```csv role=declare\ntag,threshold,key,expr\n,,$hp,$con*5+$mod_hp\n#warrior,2,$mod_hp,20\n```", ], }, { role: "file", info: " role=file", summary: "Stored as inline text content and stripped.", examples: ["```text role=file\nStored as inline content.\n```"], }, { role: "tag", info: "yaml role=tag", summary: "YAML rendered as a custom element.", examples: [ "```yaml role=tag\ntag: md-deck\nbody: ./cards.csv\nsize: 63x88\ngrid: 5x5\n```", ], }, ]; 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[]> = { csv: [".csv"], md: [".md"], yarn: [".yarn"], image: [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"], }; /** Whether a directive body is a CSS color rather than a path. */ export function isColorValue(value: string): boolean { const v = value.trim(); return ( /^#([0-9a-f]{3,8})$/i.test(v) || /^(rgb|rgba|hsl|hsla|var)\(/i.test(v) || v === "transparent" || v === "currentColor" ); } /** * Whether a color-capable element body looks like a color rather than a path. * A single alphabetic word is treated as a CSS named color (e.g. "crimson"); * image paths contain a separator or an extension. */ export function looksLikeColor(value: string): boolean { const v = value.trim(); return isColorValue(v) || /^[A-Za-z]+$/.test(v); }