refactor: consolidate block processing logic
Introduce a centralized `block-processor` and `block-scanner` to handle markdown fenced code blocks. This replaces the previous `inline-blocks.ts` and individual completion sources with a unified approach that handles block stripping, directive replacement, and metadata extraction (stats, templates, and spark tables) in a single pass.
This commit is contained in:
@@ -21,6 +21,11 @@ import {
|
||||
parseTemplateCsv,
|
||||
} from "../../cli/completions/stat-parser";
|
||||
import type { StatDef, StatTemplate } from "../../cli/completions/stat-parser";
|
||||
import {
|
||||
FENCED_BLOCK_RE,
|
||||
parseBlockAttrs,
|
||||
parseSparkTableCsv,
|
||||
} from "../../cli/completions/block-scanner";
|
||||
|
||||
export type { StatDef, StatTemplate };
|
||||
|
||||
@@ -95,19 +100,17 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
||||
const links: LinkCompletion[] = [];
|
||||
const sparkTables: SparkTableCompletion[] = [];
|
||||
const stats: StatDef[] = [];
|
||||
const tagRegex = /<md-dice[^>]*>\s*([\s\S]*?)\s*<\/md-dice>/gi;
|
||||
const statBlockRegex = /```yaml\s+role=stat\s*\n([\s\S]*?)```/gi;
|
||||
const statCsvRegex = /```csv\s+role=stat\s*\n([\s\S]*?)```/gi;
|
||||
const statTemplates: StatTemplate[] = [];
|
||||
const tagRegex = /:md-dice\[([^[]+)\]/gi;
|
||||
|
||||
for (const filePath of paths) {
|
||||
const content = await getIndexedData(filePath);
|
||||
if (!content) continue;
|
||||
|
||||
// Fresh slugger per file so duplicate headers across files don't
|
||||
// pollute each other.
|
||||
// Fresh slugger per file
|
||||
const slugger = new Slugger();
|
||||
|
||||
// Dice scan
|
||||
// ---- Dice directives ----
|
||||
let match: RegExpExecArray | null;
|
||||
tagRegex.lastIndex = 0;
|
||||
while ((match = tagRegex.exec(content)) !== null) {
|
||||
@@ -117,7 +120,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
||||
dice.push({ label: raw, notation: raw, source: filePath });
|
||||
}
|
||||
|
||||
// Link scan (headings)
|
||||
// ---- Links (headings) ----
|
||||
const basePath = filePath.replace(/\.md$/, "");
|
||||
const fileName = basePath.split("/").filter(Boolean).pop() || basePath;
|
||||
links.push({ path: basePath, label: fileName, section: null });
|
||||
@@ -129,87 +132,37 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
||||
});
|
||||
}
|
||||
|
||||
// Spark table scan
|
||||
const sparkLines = content.split(/\r?\n/);
|
||||
for (let i = 0; i < sparkLines.length; i++) {
|
||||
const headerCells = splitTableRow(sparkLines[i]);
|
||||
if (!headerCells || headerCells.length < 2) continue;
|
||||
if (!/^d\d+$/i.test(headerCells[0])) continue;
|
||||
// ---- Unified block scanning ----
|
||||
FENCED_BLOCK_RE.lastIndex = 0;
|
||||
let blockMatch: RegExpExecArray | null;
|
||||
while ((blockMatch = FENCED_BLOCK_RE.exec(content)) !== null) {
|
||||
const [, lang, infoString, body] = blockMatch;
|
||||
const attrs = parseBlockAttrs(infoString);
|
||||
attrs.lang = attrs.lang || lang;
|
||||
|
||||
if (i + 1 >= sparkLines.length) continue;
|
||||
const sepCells = splitTableRow(sparkLines[i + 1]);
|
||||
if (!sepCells || !sepCells.every((c) => /^:?-{3,}:?$/.test(c))) continue;
|
||||
if (attrs.role === "stat") {
|
||||
if (attrs.lang === "yaml" || attrs.lang === "yml") {
|
||||
stats.push(...parseStatYaml(body, filePath));
|
||||
} else if (attrs.lang === "csv") {
|
||||
stats.push(...parseStatCsv(body, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
let j = i + 2;
|
||||
while (j < sparkLines.length && splitTableRow(sparkLines[j])) j++;
|
||||
if (j <= i + 2) continue;
|
||||
if (attrs.role === "stat-template") {
|
||||
const name = attrs.id || `_tpl_${filePath}_${stats.length}`;
|
||||
statTemplates.push(parseTemplateCsv(body, filePath, name));
|
||||
}
|
||||
|
||||
const dataHeaders = headerCells.slice(1);
|
||||
const stSlug = dataHeaders
|
||||
.map((h) => slugger.slug(h.toLowerCase()))
|
||||
.join("-");
|
||||
|
||||
// Combined key: pageName-columnSlug
|
||||
const combinedSlug = `${fileName}-${stSlug}`;
|
||||
|
||||
sparkTables.push({
|
||||
label: `${fileName} § ${stSlug}`,
|
||||
notation: headerCells[0],
|
||||
slug: combinedSlug,
|
||||
filePath: basePath,
|
||||
headers: dataHeaders,
|
||||
});
|
||||
|
||||
i = j - 1;
|
||||
}
|
||||
|
||||
// Stat block scan (YAML)
|
||||
statBlockRegex.lastIndex = 0;
|
||||
let statMatch: RegExpExecArray | null;
|
||||
while ((statMatch = statBlockRegex.exec(content)) !== null) {
|
||||
const yaml = statMatch[1];
|
||||
const parsed = parseStatYaml(yaml, filePath);
|
||||
stats.push(...parsed);
|
||||
}
|
||||
|
||||
// Stat block scan (CSV)
|
||||
statCsvRegex.lastIndex = 0;
|
||||
while ((statMatch = statCsvRegex.exec(content)) !== null) {
|
||||
const csv = statMatch[1];
|
||||
const parsed = parseStatCsv(csv, filePath);
|
||||
stats.push(...parsed);
|
||||
}
|
||||
}
|
||||
|
||||
// Stat template scan
|
||||
const statTemplates: StatTemplate[] = [];
|
||||
const templateBlockRegex =
|
||||
/```csv\s+role=stat-template\s+file=(\S+)\s*\n([\s\S]*?)```/gi;
|
||||
for (const filePath of paths) {
|
||||
const content = await getIndexedData(filePath);
|
||||
if (!content) continue;
|
||||
|
||||
templateBlockRegex.lastIndex = 0;
|
||||
let tMatch: RegExpExecArray | null;
|
||||
while ((tMatch = templateBlockRegex.exec(content)) !== null) {
|
||||
const name = tMatch[1];
|
||||
const csv = tMatch[2];
|
||||
statTemplates.push(parseTemplateCsv(csv, filePath, name));
|
||||
if (attrs.role === "spark-table") {
|
||||
const parsed = parseSparkTableCsv(body, filePath, slugger);
|
||||
if (parsed) sparkTables.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { dice, links, sparkTables, stats, statTemplates };
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
// ------------------- Init (runs eagerly at import time) -------------------
|
||||
|
||||
// Using a top-level IIFE so the promise starts immediately
|
||||
|
||||
Reference in New Issue
Block a user