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.
This commit is contained in:
parent
a2d9605f4b
commit
ae44191b28
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue