refactor: move spark table parsing to directive scanner

Relocate spark table logic from the block processor to a dedicated
directive scanner. This allows spark tables to be treated as
`:md-table` directives and ensures they are processed in a second pass
after the content index is fully populated.
This commit is contained in:
2026-07-10 15:55:45 +08:00
parent 89b32ef15e
commit d4ebca5fd0
7 changed files with 419 additions and 100 deletions
+22 -19
View File
@@ -26,8 +26,12 @@ import type { StatSheet } from "../../cli/completions/types";
import {
FENCED_BLOCK_RE,
parseBlockAttrs,
parseSparkTableCsv,
} from "../../cli/completions/block-scanner";
import {
scanDirectives,
buildSparkTableCompletion,
inspectSparkTableCsv,
} from "../../cli/completions/directive-scanner";
export type { StatDef, StatTemplate, StatSheet };
@@ -105,26 +109,24 @@ async function scanClientSide(): Promise<JournalCompletions> {
const sparkTables: SparkTableCompletion[] = [];
const stats: StatDef[] = [];
const statTemplates: StatTemplate[] = [];
const tagRegex = /:md-dice\[([^[]+)\]/gi;
// Build a temporary index for resolving CSV paths
const tempIndex: Record<string, string> = {};
// First pass: load all .md content into temp index
for (const filePath of paths) {
const content = await getIndexedData(filePath);
if (content) tempIndex[filePath] = content;
}
for (const filePath of paths) {
const content = tempIndex[filePath];
if (!content) continue;
// Fresh slugger per file
const slugger = new Slugger();
// ---- Dice directives ----
let match: RegExpExecArray | null;
tagRegex.lastIndex = 0;
while ((match = tagRegex.exec(content)) !== null) {
const raw = match[1].trim();
if (!raw || raw.length > 80) continue;
if (!/^\d*d\d+/i.test(raw) && !/^[+-]/.test(raw)) continue;
dice.push({ label: raw, notation: raw, source: filePath });
}
// ---- Links (headings) ----
// ---- Links (headings) - from original content ----
const basePath = filePath.replace(/\.md$/, "");
const fileName = basePath.split("/").filter(Boolean).pop() || basePath;
links.push({ path: basePath, label: fileName, section: null });
@@ -136,7 +138,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
});
}
// ---- Unified block scanning ----
// ---- Unified block scanning (stats) ----
FENCED_BLOCK_RE.lastIndex = 0;
let blockMatch: RegExpExecArray | null;
while ((blockMatch = FENCED_BLOCK_RE.exec(content)) !== null) {
@@ -164,12 +166,13 @@ async function scanClientSide(): Promise<JournalCompletions> {
stats.push(...result.modifierDefs);
statTemplates.push(result.template);
}
if (attrs.role === "spark-table") {
const parsed = parseSparkTableCsv(body, filePath, slugger);
if (parsed) sparkTables.push(parsed);
}
}
// ---- Directive scanning (dice + spark tables) ----
const fileDir = filePath.split("/").slice(0, -1).join("/") || ".";
const directiveResult = scanDirectives(content, filePath, tempIndex, fileDir);
dice.push(...directiveResult.dice);
sparkTables.push(...directiveResult.sparkTables);
}
return { dice, links, sparkTables, stats, statTemplates, statSheets: [] };