feat: add spark table completion and rolling support
Implement "spark tables" functionality, which allows users to roll dice against markdown tables to retrieve specific values. - Add `sparkTablesSource` to scan markdown files for tables starting with a dice notation (e.g., d6, d20). - Implement `/spark` command in the journal to resolve and roll spark tables. - Add a new message type `spark` with a dedicated UI component to render the results. - Update completions API to include spark table metadata.
This commit is contained in:
@@ -227,13 +227,17 @@ export function createContentServer(
|
||||
host: string = "0.0.0.0",
|
||||
): ContentServer {
|
||||
let contentIndex: ContentIndex = {};
|
||||
let completionsIndex: CompletionsPayload = { dice: [], links: [] };
|
||||
let completionsIndex: CompletionsPayload = {
|
||||
dice: [],
|
||||
links: [],
|
||||
sparkTables: [],
|
||||
};
|
||||
|
||||
/** Re-scan completions from current content index (cached) */
|
||||
function recomputeCompletions(): void {
|
||||
completionsIndex = scanCompletions(contentIndex);
|
||||
console.log(
|
||||
`[completions] dice=${completionsIndex.dice.length} links=${completionsIndex.links.length}`,
|
||||
`[completions] dice=${completionsIndex.dice.length} links=${completionsIndex.links.length} sparkTables=${completionsIndex.sparkTables.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,22 @@
|
||||
|
||||
import { diceSource } from "./sources/dice.js";
|
||||
import { linksSource } from "./sources/links.js";
|
||||
import { sparkTablesSource } from "./sources/spark-tables.js";
|
||||
import type { CompletionSource, CompletionsPayload } from "./types.js";
|
||||
|
||||
export type {
|
||||
CompletionsPayload,
|
||||
DiceCompletion,
|
||||
LinkCompletion,
|
||||
SparkTableCompletion,
|
||||
} from "./types.js";
|
||||
|
||||
/** Registered sources — open for extension */
|
||||
const sources: CompletionSource[] = [diceSource, linksSource];
|
||||
const sources: CompletionSource[] = [
|
||||
diceSource,
|
||||
linksSource,
|
||||
sparkTablesSource,
|
||||
];
|
||||
|
||||
/**
|
||||
* Scan the full content index and return structured completion data.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Spark table completion source — extracts spark tables (markdown tables
|
||||
* whose first column header is a dice formula like d6, d20, etc.) from all
|
||||
* .md files.
|
||||
*/
|
||||
|
||||
import Slugger from "github-slugger";
|
||||
import type { CompletionSource, SparkTableCompletion } from "../types.js";
|
||||
|
||||
/** Regex: matches a pipe-delimited markdown table row */
|
||||
function splitTableRow(line: string): string[] | null {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.includes("|")) return null;
|
||||
let inner = trimmed;
|
||||
if (inner.startsWith("|")) inner = inner.slice(1);
|
||||
if (inner.endsWith("|")) inner = inner.slice(0, -1);
|
||||
return inner.split("|").map((c) => c.trim());
|
||||
}
|
||||
|
||||
const SEP_RE = /^:?-{3,}:?$/;
|
||||
const DICE_RE = /^d\d+$/i;
|
||||
|
||||
export const sparkTablesSource: CompletionSource = {
|
||||
key: "sparkTables",
|
||||
|
||||
scan(index) {
|
||||
const items: SparkTableCompletion[] = [];
|
||||
const slugger = new Slugger();
|
||||
|
||||
for (const [filePath, content] of Object.entries(index)) {
|
||||
if (!filePath.endsWith(".md")) continue;
|
||||
|
||||
const lines = content.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const headerCells = splitTableRow(lines[i]);
|
||||
if (!headerCells || headerCells.length < 2) continue;
|
||||
if (!DICE_RE.test(headerCells[0])) continue;
|
||||
|
||||
// Check separator row
|
||||
if (i + 1 >= lines.length) continue;
|
||||
const sepCells = splitTableRow(lines[i + 1]);
|
||||
if (!sepCells || !sepCells.every((c) => SEP_RE.test(c))) continue;
|
||||
|
||||
// Collect body rows
|
||||
let j = i + 2;
|
||||
while (j < lines.length) {
|
||||
const rowCells = splitTableRow(lines[j]);
|
||||
if (!rowCells) break;
|
||||
j++;
|
||||
}
|
||||
if (j <= i + 2) continue; // No body rows
|
||||
|
||||
// Build slug from data columns
|
||||
const dataHeaders = headerCells.slice(1);
|
||||
const slug = dataHeaders
|
||||
.map((h: string) => slugger.slug(h.toLowerCase()))
|
||||
.join("-");
|
||||
|
||||
const basePath = filePath.replace(/\.md$/, "");
|
||||
const fileName = basePath.split("/").filter(Boolean).pop() || basePath;
|
||||
|
||||
// Combined key: pageName-columnSlug (what user types after /spark)
|
||||
const combinedSlug = `${fileName}-${slug}`;
|
||||
|
||||
items.push({
|
||||
label: `${fileName} § ${slug}`,
|
||||
notation: headerCells[0],
|
||||
slug: combinedSlug,
|
||||
filePath: basePath,
|
||||
headers: dataHeaders,
|
||||
});
|
||||
|
||||
i = j - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
};
|
||||
@@ -22,10 +22,25 @@ export interface LinkCompletion {
|
||||
section: string | null;
|
||||
}
|
||||
|
||||
/** A spark table found in a markdown file */
|
||||
export interface SparkTableCompletion {
|
||||
/** Display label: "file § slug" */
|
||||
label: string;
|
||||
/** Dice notation (e.g. "d6", "d20") parsed from the first column header */
|
||||
notation: string;
|
||||
/** Concatenated slug of data column headers */
|
||||
slug: string;
|
||||
/** File path of the containing .md file */
|
||||
filePath: string;
|
||||
/** Data column headers for display */
|
||||
headers: string[];
|
||||
}
|
||||
|
||||
/** Top-level payload served at /__COMPLETIONS.json */
|
||||
export interface CompletionsPayload {
|
||||
dice: DiceCompletion[];
|
||||
links: LinkCompletion[];
|
||||
sparkTables: SparkTableCompletion[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user