feat(cli): Add tttk check validator
Statically validate a directory of tttk documents and report structured diagnostics with file/line positions, exit codes, and --json output. Checks cover code-block roles, unresolved content references, tag blocks, element names, links, anchors, and assets. Runs over the same content registry as the app.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 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
|
||||
* intended to be 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;
|
||||
}
|
||||
|
||||
/** 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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
name: "md-deck",
|
||||
body: "CSV path",
|
||||
attrs: ["size", "grid", "layers"],
|
||||
summary: "Card layout from a CSV (legacy; prefer `yaml role=tag`).",
|
||||
refKind: "csv",
|
||||
},
|
||||
{
|
||||
name: "md-link",
|
||||
body: "path, optional #section",
|
||||
attrs: [],
|
||||
summary: "Clickable link that expands the target article inline.",
|
||||
refKind: "md",
|
||||
},
|
||||
{
|
||||
name: "md-embed",
|
||||
body: "path, optional #section",
|
||||
attrs: ["headingBase"],
|
||||
summary: "Renders the target article or section inline.",
|
||||
refKind: "md",
|
||||
},
|
||||
{
|
||||
name: "md-bg",
|
||||
body: "image path or CSS color",
|
||||
attrs: ["fit"],
|
||||
summary: "Sets the surrounding article card background.",
|
||||
refKind: "image",
|
||||
},
|
||||
{
|
||||
name: "md-border",
|
||||
body: "color or border image",
|
||||
attrs: ["width", "slice"],
|
||||
summary: "Applies a border to the article card.",
|
||||
refKind: "image",
|
||||
},
|
||||
{
|
||||
name: "md-font",
|
||||
body: "font family",
|
||||
attrs: ["source", "weight", "color"],
|
||||
summary: "Sets the article font.",
|
||||
refKind: "none",
|
||||
},
|
||||
{
|
||||
name: "md-pins",
|
||||
body: "image path",
|
||||
attrs: ["pins", "fixed", "labelStart"],
|
||||
summary: "Numbered markers on an image.",
|
||||
refKind: "image",
|
||||
},
|
||||
{
|
||||
name: "md-commander",
|
||||
body: "command template CSV",
|
||||
attrs: ["placeholder", "class", "height"],
|
||||
summary: "Interactive command history / tracker panel.",
|
||||
refKind: "csv",
|
||||
},
|
||||
{
|
||||
name: "md-yarn-spinner",
|
||||
body: "Yarn Spinner file",
|
||||
attrs: ["start"],
|
||||
summary: "Runs a Yarn Spinner dialogue file.",
|
||||
refKind: "yarn",
|
||||
},
|
||||
];
|
||||
|
||||
export const ELEMENT_NAMES: ReadonlySet<string> = new Set(
|
||||
KNOWN_ELEMENTS.map((e) => e.name),
|
||||
);
|
||||
|
||||
const ELEMENT_BY_NAME: ReadonlyMap<string, ElementSpec> = 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<string> = new Set([
|
||||
"md-bg",
|
||||
"md-border",
|
||||
]);
|
||||
|
||||
export interface RoleSpec {
|
||||
role: string;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
/** Code-block `role=` values the scanner understands. */
|
||||
export const ROLE_SPECS: readonly RoleSpec[] = [
|
||||
{ role: "spark-table", summary: "CSV (or markdown pipe table) rendered as `:md-table`." },
|
||||
{ role: "declare", summary: "Reactive variable / tag-modifier definitions." },
|
||||
{ role: "file", summary: "Stored as inline text content and stripped." },
|
||||
{ role: "tag", summary: "YAML rendered as a custom element." },
|
||||
];
|
||||
|
||||
export const KNOWN_ROLES: ReadonlySet<string> = new Set(
|
||||
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. */
|
||||
export const REF_EXTENSIONS: Record<Exclude<RefKind, "none">, 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);
|
||||
}
|
||||
Reference in New Issue
Block a user