From ae44191b28319e69a92241f965d1ae6ba78a187d Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 13 Jul 2026 09:38:09 +0800 Subject: [PATCH] refactor: use csv-parse for declare block parsing Replace manual CSV splitting with `csv-parse` to allow for order-agnostic columns and better handling of quoted values. The `tag` column is now optional. --- src/cli/completions/declare-parser.ts | 69 ++++++++++----------------- 1 file changed, 24 insertions(+), 45 deletions(-) diff --git a/src/cli/completions/declare-parser.ts b/src/cli/completions/declare-parser.ts index fc7de5b..c37a36a 100644 --- a/src/cli/completions/declare-parser.ts +++ b/src/cli/completions/declare-parser.ts @@ -3,7 +3,7 @@ * * Shared between CLI completions scanner and browser-side completions. * - * Format: + * Format (columns can be in any order; `tag` is optional): * tag,key,expr * ,$hp,$con*5+$mod_hp ← variable declaration * ,$ac,10+$dex ← variable declaration @@ -14,6 +14,8 @@ * When `tag` is present: when #tag is active, $key gets expr added to its base. */ +import { parse } from "csv-parse/browser/esm/sync"; + export interface VarDeclaration { key: string; // "$hp" (always starts with $) expression: string; // "$con*5+$mod_hp" @@ -34,33 +36,34 @@ export interface DeclareResult { * 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: [] }; + const trimmed = csv.trim(); + if (!trimmed) 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") { + // 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 headers "tag,key,expr". Got: ${headers.join(",")}`, + `${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; key: string; expr: string }>; + 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() ?? ""; + for (const row of records) { + const tag = row.tag ?? ""; + const key = row.key ?? ""; + const expr = row.expr ?? ""; if (!key || !expr) { - console.warn(`${source}:${i + 1}: skipping row with empty key or expr`); + console.warn(`${source}: skipping row with empty key or expr`); continue; } @@ -68,12 +71,12 @@ export function parseDeclareCsv(csv: string, source: string): DeclareResult { // Tag modifier if (!tag.startsWith("#")) { throw new Error( - `${source}:${i + 1}: tag must start with #, got "${tag}"`, + `${source}: tag must start with #, got "${tag}"`, ); } if (!key.startsWith("$")) { throw new Error( - `${source}:${i + 1}: key must start with $, got "${key}"`, + `${source}: key must start with $, got "${key}"`, ); } tagModifiers.push({ tag, target: key, expression: expr }); @@ -81,7 +84,7 @@ export function parseDeclareCsv(csv: string, source: string): DeclareResult { // Variable declaration if (!key.startsWith("$")) { throw new Error( - `${source}:${i + 1}: key must start with $, got "${key}"`, + `${source}: key must start with $, got "${key}"`, ); } variables.push({ key, expression: expr }); @@ -90,27 +93,3 @@ export function parseDeclareCsv(csv: string, source: string): DeclareResult { 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; -} \ No newline at end of file