/** * Declare parser — parses ```csv role=declare code blocks from markdown. * * Shared between CLI completions scanner and browser-side completions. * * Format (columns can be in any order; `tag` and `threshold` are optional): * tag,threshold,key,expr * ,,$hp,$con*5+$mod_hp ← variable declaration * ,,$ac,10+$dex ← variable declaration * #warrior,2,$mod_hp,20 ← tag modifier (threshold 2) * #warrior,,$mod_str,1 ← tag modifier (threshold defaults to 1) * * When `tag` is empty: $key is a reactively computed variable. * When `tag` is present: when #tag met (source's tagmap value >= threshold), * $key gets expr added to its base. */ import { parse } from "csv-parse/browser/esm/sync"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface VarDeclaration { key: string; // "$hp" (always starts with $) expression: string; // "$con*5+$mod_hp" } export interface TagModifier { tag: string; // "#warrior" target: string; // "$mod_hp" expression: string; // "20" threshold: number; // minimum tagmap count to activate (default 1) } export interface DeclareResult { variables: VarDeclaration[]; tagModifiers: TagModifier[]; } /** * Parse a single ```csv role=declare block body. */ export function parseDeclareCsv(csv: string, source: string): DeclareResult { const trimmed = csv.trim(); if (!trimmed) return { variables: [], tagModifiers: [] }; // Validate that required columns exist (order-agnostic, tag is optional) const firstLine = trimmed.split(/\r?\n/)[0]; const headers = firstLine.split(",").map((h) => h.trim().toLowerCase()); if (!headers.includes("key") || !headers.includes("expr")) { throw new Error( `${source}: role=declare blocks must have "key" and "expr" columns. Got: ${headers.join(",")}`, ); } const records = parse(trimmed, { columns: true, trim: true, skipEmptyLines: true, }) as Array<{ tag?: string; threshold?: string; key: string; expr: string }>; const variables: VarDeclaration[] = []; const tagModifiers: TagModifier[] = []; for (const row of records) { const tag = row.tag ?? ""; const key = row.key ?? ""; const expr = row.expr ?? ""; if (!key || !expr) { console.warn(`${source}: skipping row with empty key or expr`); continue; } if (tag) { // Tag modifier if (!tag.startsWith("#")) { throw new Error(`${source}: tag must start with #, got "${tag}"`); } if (!key.startsWith("$")) { throw new Error(`${source}: key must start with $, got "${key}"`); } const thresholdRaw = row.threshold?.trim() ?? ""; const threshold = thresholdRaw ? parseInt(thresholdRaw, 10) : 1; if (isNaN(threshold) || threshold < 1) { throw new Error( `${source}: threshold must be a positive integer, got "${row.threshold}"`, ); } tagModifiers.push({ tag, target: key, expression: expr, threshold }); } else { // Variable declaration if (!key.startsWith("$")) { throw new Error(`${source}: key must start with $, got "${key}"`); } variables.push({ key, expression: expr }); } } return { variables, tagModifiers }; }