From e801ac9b5f2c50e7e971eb2e703e2bac63a2fbf1 Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 7 Jul 2026 19:13:11 +0800 Subject: [PATCH] refactor: improve spark table lookup and error reporting Replace the heuristic-based slug parsing with a more robust combined-slug lookup to correctly handle page names containing hyphens. Added `scanSparkTables` to provide better error messages when a table is not found. --- src/components/journal/types/spark.tsx | 36 +++++++++-------- src/components/utils/spark-table.ts | 54 ++++++++++++++++++++------ 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/components/journal/types/spark.tsx b/src/components/journal/types/spark.tsx index 20542ea..47c65e8 100644 --- a/src/components/journal/types/spark.tsx +++ b/src/components/journal/types/spark.tsx @@ -16,7 +16,11 @@ import { z } from "zod"; import { For } from "solid-js"; import { registerMessageType } from "../registry"; import { rollFormula } from "../../md-commander/hooks"; -import { findSparkTable, rollSparkTable } from "../../utils/spark-table"; +import { + findSparkTableByCombinedSlug, + rollSparkTable, + scanSparkTables, +} from "../../utils/spark-table"; import { getIndexedData } from "../../../data-loader/file-index"; // --------------------------------------------------------------------------- @@ -72,28 +76,28 @@ export async function resolveSparkPayload(raw: { key: string; filePath: string; }): Promise { - // key is "pageName-columnSlug". The page name is the last segment of - // filePath (e.g. "01-dry-dock" from "/ayi-games/.../01-dry-dock"). - // Strip it from the key to recover the column slug. - const pathParts = raw.filePath.replace(/^\//, "").split("/"); - const pageName = pathParts[pathParts.length - 1] || raw.filePath; - const columnSlug = - raw.key.length > pageName.length + 1 && raw.key.startsWith(pageName + "-") - ? raw.key.slice(pageName.length + 1) - : raw.key; - - const filePath = `/${raw.filePath.replace(/^\//, "")}`; + const mdPath = `/${raw.filePath.replace(/^\//, "")}.md`; + const pageName = + raw.filePath.replace(/^\//, "").split("/").filter(Boolean).pop() || + raw.filePath; let content: string; try { - content = await getIndexedData(filePath); + content = await getIndexedData(mdPath); } catch { - throw new Error(`Failed to load file: "${filePath}"`); + throw new Error(`Failed to load file: "${mdPath}"`); } - const meta = findSparkTable(content, columnSlug); + const meta = findSparkTableByCombinedSlug(content, raw.key, pageName); if (!meta) { - throw new Error(`Spark table "${columnSlug}" not found in "${filePath}"`); + const available = scanSparkTables(content); + const names = + available.length > 0 + ? available.map((t) => `${pageName}-${t.slug}`).join(", ") + : "(none)"; + throw new Error( + `Spark table "${raw.key}" not found in "${mdPath}". Available: ${names}`, + ); } const sparkResult = rollSparkTable(meta); diff --git a/src/components/utils/spark-table.ts b/src/components/utils/spark-table.ts index 2205ef6..9dd75f6 100644 --- a/src/components/utils/spark-table.ts +++ b/src/components/utils/spark-table.ts @@ -148,18 +148,44 @@ export function findSparkTable( return null; } +/** + * Find a spark table by its combined slug (`pageName-columnSlug`). + * Iterates every spark table in the content, constructs the combined slug, + * and returns the first match. Avoids the need to guess where the page + * name ends and the column slug begins (page names may contain `-`). + */ +export function findSparkTableByCombinedSlug( + markdown: string, + combinedSlug: string, + pageName: string, +): SparkTableMeta | null { + const tables = parseMarkdownTables(markdown); + for (const table of tables) { + if (!isSparkTable(table)) continue; + const colSlug = sparkTableSlug(table); + const candidate = `${pageName}-${colSlug}`; + if (candidate === combinedSlug) { + return { + notation: table.headers[0], + slug: colSlug, + dataHeaders: table.headers.slice(1), + rows: table.rows, + }; + } + } + return null; +} + /** Scan all spark tables in a markdown file and return their metadata */ export function scanSparkTables( markdown: string, ): Omit[] { const tables = parseMarkdownTables(markdown); - return tables - .filter(isSparkTable) - .map((table) => ({ - notation: table.headers[0], - slug: sparkTableSlug(table), - dataHeaders: table.headers.slice(1), - })); + return tables.filter(isSparkTable).map((table) => ({ + notation: table.headers[0], + slug: sparkTableSlug(table), + dataHeaders: table.headers.slice(1), + })); } // --------------------------------------------------------------------------- @@ -170,9 +196,7 @@ export function scanSparkTables( * Roll a spark table: for each data column, roll the dice formula and look * up the corresponding row value. */ -export function rollSparkTable( - meta: SparkTableMeta, -): SparkTableResult { +export function rollSparkTable(meta: SparkTableMeta): SparkTableResult { const slugger = new Slugger(); const columns: SparkTableColumn[] = []; @@ -183,11 +207,17 @@ export function rollSparkTable( const roll = rollFormula(meta.notation); const rolledValue = roll.result.total; - // Find the row matching the rolled value (1-based dice result) + // Find the row matching the rolled value — compare as integers, + // falling back to string comparison. let value = `(no row for ${rolledValue})`; for (const row of meta.rows) { const diceCell = row[0] ?? ""; - if (String(rolledValue) === diceCell.trim()) { + const cellNum = parseInt(diceCell.trim(), 10); + const match = + !isNaN(cellNum) && rolledValue === cellNum + ? true + : String(rolledValue) === diceCell.trim(); + if (match) { value = row[colIdx + 1] ?? ""; break; }