116 lines
3.2 KiB
TypeScript
116 lines
3.2 KiB
TypeScript
/**
|
|
* 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;
|
|
} |