115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
/**
|
|
* Spark table scanner — CLI-side markdown table detection and conversion.
|
|
*
|
|
* Parses markdown tables, detects spark tables (first column header is a dice
|
|
* formula), and converts them to CSV for the directive scanner.
|
|
*
|
|
* Not imported at runtime — the frontend only needs CSV parsing + rolling.
|
|
*/
|
|
|
|
import Slugger from "github-slugger";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface MarkdownTable {
|
|
headers: string[];
|
|
rows: string[][];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Markdown table parser
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Parse all markdown tables from a markdown string.
|
|
* Handles both leading/trailing `|` styles and bare styles.
|
|
*/
|
|
export function parseMarkdownTables(markdown: string): MarkdownTable[] {
|
|
const tables: MarkdownTable[] = [];
|
|
const lines = markdown.split(/\r?\n/);
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const headerCells = splitTableRow(lines[i]);
|
|
if (!headerCells || headerCells.length < 2) continue;
|
|
|
|
// Peek at the next line — must be a separator row
|
|
if (i + 1 >= lines.length) continue;
|
|
const sepCells = splitTableRow(lines[i + 1]);
|
|
if (!sepCells || sepCells.length < headerCells.length) continue;
|
|
if (!sepCells.every((c) => /^:?-{3,}:?$/.test(c))) continue;
|
|
|
|
// Valid table header + separator — collect body rows
|
|
const rows: string[][] = [];
|
|
let j = i + 2;
|
|
while (j < lines.length) {
|
|
const rowCells = splitTableRow(lines[j]);
|
|
if (!rowCells) break;
|
|
// Allow rows with fewer cells (unfilled trailing columns)
|
|
rows.push(rowCells);
|
|
j++;
|
|
}
|
|
|
|
// Only include tables with at least one data row
|
|
if (rows.length > 0) {
|
|
tables.push({ headers: headerCells, rows });
|
|
}
|
|
i = j - 1;
|
|
}
|
|
|
|
return tables;
|
|
}
|
|
|
|
/** Split a pipe-delimited table row, stripping optional leading/trailing `|` */
|
|
function splitTableRow(line: string): string[] | null {
|
|
const trimmed = line.trim();
|
|
if (!trimmed.includes("|")) return null;
|
|
|
|
// Strip optional leading and trailing `|`
|
|
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());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Spark table detection & metadata
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const DICE_HEADER_RE = /^d\d+$/i;
|
|
|
|
/** Check whether a table is a spark table (first header is a dice formula) */
|
|
export function isSparkTable(table: MarkdownTable): boolean {
|
|
if (table.headers.length < 2) return false;
|
|
return DICE_HEADER_RE.test(table.headers[0]);
|
|
}
|
|
|
|
/**
|
|
* Generate the spark table slug by concatenating slugs of all data column
|
|
* headers (excluding the dice column).
|
|
*/
|
|
export function sparkTableSlug(table: MarkdownTable): string {
|
|
const slugger = new Slugger();
|
|
return table.headers
|
|
.slice(1)
|
|
.map((h) => slugger.slug(h.toLowerCase()))
|
|
.join("-");
|
|
}
|
|
|
|
/**
|
|
* Scan all spark tables in a markdown file and return their metadata
|
|
* (without rows — suitable for listing available tables).
|
|
*/
|
|
export function scanSparkTables(
|
|
markdown: string,
|
|
): { notation: string; slug: string; dataHeaders: string[] }[] {
|
|
const tables = parseMarkdownTables(markdown);
|
|
return tables.filter(isSparkTable).map((table) => ({
|
|
notation: table.headers[0],
|
|
slug: sparkTableSlug(table),
|
|
dataHeaders: table.headers.slice(1),
|
|
}));
|
|
}
|