feat: add CSV support for journal stat blocks

This commit is contained in:
2026-07-09 09:47:17 +08:00
parent 138c089514
commit 45cd9a01ac
2 changed files with 138 additions and 2 deletions
+99 -1
View File
@@ -99,6 +99,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
const stats: StatDef[] = [];
const tagRegex = /<md-dice[^>]*>\s*([\s\S]*?)\s*<\/md-dice>/gi;
const statBlockRegex = /```yaml\s+role=stat\s*\n([\s\S]*?)```/gi;
const statCsvRegex = /```csv\s+role=stat\s*\n([\s\S]*?)```/gi;
for (const filePath of paths) {
const content = await getIndexedData(filePath);
@@ -164,7 +165,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
i = j - 1;
}
// Stat block scan
// Stat block scan (YAML)
statBlockRegex.lastIndex = 0;
let statMatch: RegExpExecArray | null;
while ((statMatch = statBlockRegex.exec(content)) !== null) {
@@ -172,6 +173,14 @@ async function scanClientSide(): Promise<JournalCompletions> {
const parsed = parseStatYaml(yaml, filePath);
stats.push(...parsed);
}
// Stat block scan (CSV)
statCsvRegex.lastIndex = 0;
while ((statMatch = statCsvRegex.exec(content)) !== null) {
const csv = statMatch[1];
const parsed = parseStatCsv(csv, filePath);
stats.push(...parsed);
}
}
return { dice, links, sparkTables, stats };
@@ -284,6 +293,95 @@ function parseStatYaml(yaml: string, source: string): StatDef[] {
return defs;
}
// ---------------------------------------------------------------------------
// Stat CSV parser
// ---------------------------------------------------------------------------
/**
* Parse a ```csv role=stat block into StatDef entries.
*
* Columns: key, label, type, scope, default, roll, target, formula, options
* Only `key` is required. `type` defaults to "number", `scope` to "player".
* `options` column uses pipe-separated values: "a|b|c".
*/
function parseStatCsv(csv: string, source: string): StatDef[] {
const lines = csv.trim().split(/\r?\n/);
if (lines.length === 0) return [];
// Parse header
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
const idx = (name: string) => {
const i = headers.indexOf(name);
return i === -1 ? null : i;
};
const defs: StatDef[] = [];
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 === 0) continue;
const get = (name: string) => {
const colIdx = idx(name);
if (colIdx === null || colIdx >= cols.length) return undefined;
return cols[colIdx].trim() || undefined;
};
const key = get("key");
if (!key) continue;
const type = (get("type") || "number") as StatDef["type"];
const scope = (get("scope") || "player") as StatDef["scope"];
let options: string[] | undefined;
const optRaw = get("options");
if (optRaw) {
options = optRaw
.split("|")
.map((s) => s.trim())
.filter(Boolean);
}
defs.push({
key,
label: get("label") || key,
type,
scope,
default: get("default"),
roll: get("roll"),
target: get("target"),
formula: get("formula"),
options,
source,
});
}
return defs;
}
/** Split a CSV row respecting quoted fields. */
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;
}
// ------------------- Init (runs eagerly at import time) -------------------
// Using a top-level IIFE so the promise starts immediately