refactor: use CSV path instead of markdown file for spark tables
Switch the spark table resolution logic to use the direct path to the backing CSV file rather than attempting to parse the containing markdown file. This simplifies the lookup process and improves reliability. Also refactor `SparkTableMeta` to store rows as objects keyed by header instead of raw string arrays.
This commit is contained in:
parent
b8cfb05e53
commit
719bb6ad8a
|
|
@ -135,6 +135,7 @@ export function inspectSparkTableCsv(csv: string): string[] | null {
|
||||||
export function buildSparkTableCompletion(
|
export function buildSparkTableCompletion(
|
||||||
csv: string,
|
csv: string,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
|
csvPath: string,
|
||||||
slugger: Slugger,
|
slugger: Slugger,
|
||||||
): SparkTableCompletion | null {
|
): SparkTableCompletion | null {
|
||||||
const dataHeaders = inspectSparkTableCsv(csv);
|
const dataHeaders = inspectSparkTableCsv(csv);
|
||||||
|
|
@ -156,6 +157,7 @@ export function buildSparkTableCompletion(
|
||||||
notation: headers[0],
|
notation: headers[0],
|
||||||
slug: combinedSlug,
|
slug: combinedSlug,
|
||||||
filePath: basePath,
|
filePath: basePath,
|
||||||
|
csvPath,
|
||||||
headers: dataHeaders,
|
headers: dataHeaders,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +248,7 @@ export function scanDirectives(
|
||||||
newIndexEntries[resolvedPath] = csv;
|
newIndexEntries[resolvedPath] = csv;
|
||||||
|
|
||||||
// Collect spark table completion
|
// Collect spark table completion
|
||||||
const st = buildSparkTableCompletion(csv, filePath, slugger);
|
const st = buildSparkTableCompletion(csv, filePath, resolvedPath, slugger);
|
||||||
if (st) {
|
if (st) {
|
||||||
sparkTables.push(st);
|
sparkTables.push(st);
|
||||||
}
|
}
|
||||||
|
|
@ -288,7 +290,7 @@ export function scanDirectives(
|
||||||
let csv = index[csvPath] ?? newIndexEntries[csvPath];
|
let csv = index[csvPath] ?? newIndexEntries[csvPath];
|
||||||
if (!csv) continue;
|
if (!csv) continue;
|
||||||
|
|
||||||
const st = buildSparkTableCompletion(csv, filePath, slugger);
|
const st = buildSparkTableCompletion(csv, filePath, csvPath, slugger);
|
||||||
if (!st) continue;
|
if (!st) continue;
|
||||||
|
|
||||||
// Check if data-spark is already set in extra attrs
|
// Check if data-spark is already set in extra attrs
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,10 @@ export interface SparkTableCompletion {
|
||||||
notation: string;
|
notation: string;
|
||||||
/** Concatenated slug of data column headers */
|
/** Concatenated slug of data column headers */
|
||||||
slug: string;
|
slug: string;
|
||||||
/** File path of the containing .md file */
|
/** File path of the containing .md file (without extension) */
|
||||||
filePath: string;
|
filePath: string;
|
||||||
|
/** Resolved path to the .csv file backing this spark table */
|
||||||
|
csvPath: string;
|
||||||
/** Data column headers for display */
|
/** Data column headers for display */
|
||||||
headers: string[];
|
headers: string[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,8 +127,8 @@ export const JournalInput: Component = () => {
|
||||||
try {
|
try {
|
||||||
const key = (parsed.payload as { key: string }).key;
|
const key = (parsed.payload as { key: string }).key;
|
||||||
const match = comp.data.sparkTables.find((s) => s.slug === key);
|
const match = comp.data.sparkTables.find((s) => s.slug === key);
|
||||||
const filePath = match?.filePath ?? "";
|
const csvPath = match?.csvPath ?? "";
|
||||||
const p = await resolveSparkPayload({ key, filePath });
|
const p = await resolveSparkPayload({ key, csvPath });
|
||||||
const result = sendMessage("spark", p);
|
const result = sendMessage("spark", p);
|
||||||
const r = unwrap(result);
|
const r = unwrap(result);
|
||||||
finish(r.ok, r.err);
|
finish(r.ok, r.err);
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,6 @@ import {
|
||||||
} from "../../cli/completions/block-scanner";
|
} from "../../cli/completions/block-scanner";
|
||||||
import {
|
import {
|
||||||
scanDirectives,
|
scanDirectives,
|
||||||
buildSparkTableCompletion,
|
|
||||||
inspectSparkTableCsv,
|
|
||||||
} from "../../cli/completions/directive-scanner";
|
} from "../../cli/completions/directive-scanner";
|
||||||
|
|
||||||
export type { StatDef, StatTemplate, StatSheet };
|
export type { StatDef, StatTemplate, StatSheet };
|
||||||
|
|
@ -54,6 +52,7 @@ export interface SparkTableCompletion {
|
||||||
notation: string;
|
notation: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
|
csvPath: string;
|
||||||
headers: string[];
|
headers: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,8 @@ import { For } from "solid-js";
|
||||||
import { registerMessageType } from "../registry";
|
import { registerMessageType } from "../registry";
|
||||||
import { rollFormula } from "../../md-commander/hooks";
|
import { rollFormula } from "../../md-commander/hooks";
|
||||||
import {
|
import {
|
||||||
findSparkTableByCombinedSlug,
|
parseSparkTableCsv,
|
||||||
rollSparkTable,
|
rollSparkTable,
|
||||||
scanSparkTables,
|
|
||||||
} from "../../utils/spark-table";
|
} from "../../utils/spark-table";
|
||||||
import { getIndexedData } from "../../../data-loader/file-index";
|
import { getIndexedData } from "../../../data-loader/file-index";
|
||||||
|
|
||||||
|
|
@ -70,33 +69,23 @@ export type SparkPayload = z.infer<typeof schema>;
|
||||||
* Resolve a spark table roll.
|
* Resolve a spark table roll.
|
||||||
*
|
*
|
||||||
* `key` is the combined slug (pageName-columnSlug).
|
* `key` is the combined slug (pageName-columnSlug).
|
||||||
* `filePath` is the .md file path (without .md extension) from completions.
|
* `csvPath` is the resolved .csv file path from completions.
|
||||||
*/
|
*/
|
||||||
export async function resolveSparkPayload(raw: {
|
export async function resolveSparkPayload(raw: {
|
||||||
key: string;
|
key: string;
|
||||||
filePath: string;
|
csvPath: string;
|
||||||
}): Promise<SparkPayload> {
|
}): Promise<SparkPayload> {
|
||||||
const mdPath = `/${raw.filePath.replace(/^\//, "")}.md`;
|
let csv: string;
|
||||||
const pageName =
|
|
||||||
raw.filePath.replace(/^\//, "").split("/").filter(Boolean).pop() ||
|
|
||||||
raw.filePath;
|
|
||||||
|
|
||||||
let content: string;
|
|
||||||
try {
|
try {
|
||||||
content = await getIndexedData(mdPath);
|
csv = await getIndexedData(raw.csvPath);
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(`Failed to load file: "${mdPath}"`);
|
throw new Error(`Failed to load CSV: "${raw.csvPath}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = findSparkTableByCombinedSlug(content, raw.key, pageName);
|
const meta = parseSparkTableCsv(csv);
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
const available = scanSparkTables(content);
|
|
||||||
const names =
|
|
||||||
available.length > 0
|
|
||||||
? available.map((t) => `${pageName}-${t.slug}`).join(", ")
|
|
||||||
: "(none)";
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Spark table "${raw.key}" not found in "${mdPath}". Available: ${names}`,
|
`Spark table "${raw.key}" not found in CSV "${raw.csvPath}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Slugger from "github-slugger";
|
import Slugger from "github-slugger";
|
||||||
|
import { parseCSVString } from "../utils/csv-loader";
|
||||||
import { rollFormula } from "../md-commander/hooks";
|
import { rollFormula } from "../md-commander/hooks";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -41,8 +42,8 @@ export interface SparkTableMeta {
|
||||||
slug: string;
|
slug: string;
|
||||||
/** Data column headers (excluding dice column) */
|
/** Data column headers (excluding dice column) */
|
||||||
dataHeaders: string[];
|
dataHeaders: string[];
|
||||||
/** Full list of rows */
|
/** Full list of rows as objects keyed by header */
|
||||||
rows: string[][];
|
rows: Record<string, string>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -141,7 +142,7 @@ export function findSparkTable(
|
||||||
notation: table.headers[0],
|
notation: table.headers[0],
|
||||||
slug,
|
slug,
|
||||||
dataHeaders: table.headers.slice(1),
|
dataHeaders: table.headers.slice(1),
|
||||||
rows: table.rows,
|
rows: rowsToObjects(table),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,13 +170,50 @@ export function findSparkTableByCombinedSlug(
|
||||||
notation: table.headers[0],
|
notation: table.headers[0],
|
||||||
slug: colSlug,
|
slug: colSlug,
|
||||||
dataHeaders: table.headers.slice(1),
|
dataHeaders: table.headers.slice(1),
|
||||||
rows: table.rows,
|
rows: rowsToObjects(table),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Convert a MarkdownTable's string[][] rows to Record<string, string>[] */
|
||||||
|
function rowsToObjects(table: MarkdownTable): Record<string, string>[] {
|
||||||
|
return table.rows.map((row) => {
|
||||||
|
const obj: Record<string, string> = {};
|
||||||
|
for (let i = 0; i < table.headers.length; i++) {
|
||||||
|
obj[table.headers[i]] = row[i] ?? "";
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a CSV string into a SparkTableMeta.
|
||||||
|
* The CSV must have a dice formula as its first column header.
|
||||||
|
* Returns null if the CSV is not a valid spark table.
|
||||||
|
*/
|
||||||
|
export function parseSparkTableCsv(csv: string): SparkTableMeta | null {
|
||||||
|
const parsed = parseCSVString(csv);
|
||||||
|
const headers = Object.keys(parsed[0] ?? {});
|
||||||
|
|
||||||
|
if (headers.length < 2) return null;
|
||||||
|
if (!DICE_HEADER_RE.test(headers[0])) return null;
|
||||||
|
|
||||||
|
const slugger = new Slugger();
|
||||||
|
const dataHeaders = headers.slice(1);
|
||||||
|
const slug = dataHeaders
|
||||||
|
.map((h) => slugger.slug(h.toLowerCase()))
|
||||||
|
.join("-");
|
||||||
|
|
||||||
|
return {
|
||||||
|
notation: headers[0],
|
||||||
|
slug,
|
||||||
|
dataHeaders,
|
||||||
|
rows: parsed as Record<string, string>[],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Scan all spark tables in a markdown file and return their metadata */
|
/** Scan all spark tables in a markdown file and return their metadata */
|
||||||
export function scanSparkTables(
|
export function scanSparkTables(
|
||||||
markdown: string,
|
markdown: string,
|
||||||
|
|
@ -237,8 +275,9 @@ export function rollSparkTable(meta: SparkTableMeta): SparkTableResult {
|
||||||
const slugger = new Slugger();
|
const slugger = new Slugger();
|
||||||
const columns: SparkTableColumn[] = [];
|
const columns: SparkTableColumn[] = [];
|
||||||
|
|
||||||
for (let colIdx = 0; colIdx < meta.dataHeaders.length; colIdx++) {
|
const diceHeader = Object.keys(meta.rows[0] ?? {})[0] ?? "";
|
||||||
const header = meta.dataHeaders[colIdx];
|
|
||||||
|
for (const header of meta.dataHeaders) {
|
||||||
const slug = slugger.slug(header.toLowerCase());
|
const slug = slugger.slug(header.toLowerCase());
|
||||||
|
|
||||||
const roll = rollFormula(meta.notation);
|
const roll = rollFormula(meta.notation);
|
||||||
|
|
@ -247,9 +286,9 @@ export function rollSparkTable(meta: SparkTableMeta): SparkTableResult {
|
||||||
// Find the row matching the rolled value
|
// Find the row matching the rolled value
|
||||||
let value = `(no row for ${rolledValue})`;
|
let value = `(no row for ${rolledValue})`;
|
||||||
for (const row of meta.rows) {
|
for (const row of meta.rows) {
|
||||||
const diceCell = row[0] ?? "";
|
const diceCell = row[diceHeader] ?? "";
|
||||||
if (matchesCell(diceCell, rolledValue)) {
|
if (matchesCell(diceCell, rolledValue)) {
|
||||||
value = row[colIdx + 1] ?? "";
|
value = row[header] ?? "";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue