/** * Declare parser — parses ```csv role=declare code blocks from markdown. * * Shared between CLI completions scanner and browser-side completions. * * Format: * tag,key,expr * ,$hp,$con*5+$mod_hp ← variable declaration * ,$ac,10+$dex ← variable declaration * #warrior,$mod_hp,20 ← tag modifier * #warrior,$mod_str,1 ← tag modifier * * When `tag` is empty: $key is a reactively computed variable. * When `tag` is present: when #tag is active, $key gets expr added to its base. */ 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" } export interface DeclareResult { variables: VarDeclaration[]; tagModifiers: TagModifier[]; } /** * Parse a ```csv role=declare block. */ export function parseDeclareCsv(csv: string, source: string): DeclareResult { const lines = csv.trim().split(/\r?\n/); if (lines.length === 0) return { variables: [], tagModifiers: [] }; // First line is header; validate it const headers = lines[0].split(",").map((h) => h.trim().toLowerCase()); if (headers[0] !== "tag" || headers[1] !== "key" || headers[2] !== "expr") { throw new Error( `${source}: role=declare blocks must have headers "tag,key,expr". Got: ${headers.join(",")}`, ); } const variables: VarDeclaration[] = []; const tagModifiers: TagModifier[] = []; for (let i = 1; i < lines.length; i++) { const row = lines[i].trim(); if (!row || row.startsWith("#")) continue; const cols = splitCsvRow(row); if (cols.length < 3) continue; const tag = cols[0]?.trim() ?? ""; const key = cols[1]?.trim() ?? ""; const expr = cols[2]?.trim() ?? ""; if (!key || !expr) { console.warn(`${source}:${i + 1}: skipping row with empty key or expr`); continue; } if (tag) { // Tag modifier if (!tag.startsWith("#")) { throw new Error( `${source}:${i + 1}: tag must start with #, got "${tag}"`, ); } if (!key.startsWith("$")) { throw new Error( `${source}:${i + 1}: key must start with $, got "${key}"`, ); } tagModifiers.push({ tag, target: key, expression: expr }); } else { // Variable declaration if (!key.startsWith("$")) { throw new Error( `${source}:${i + 1}: key must start with $, got "${key}"`, ); } variables.push({ key, expression: expr }); } } return { variables, tagModifiers }; } // --------------------------------------------------------------------------- // CSV row splitter // --------------------------------------------------------------------------- function splitCsvRow(row: string): string[] { const cols: string[] = []; let current = ""; let inQuote = false; for (let i = 0; i < row.length; i++) { const ch = row[i]; if (ch === '"') { inQuote = !inQuote; } else if (ch === "," && !inQuote) { cols.push(current); current = ""; } else { current += ch; } } cols.push(current); return cols; }