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:
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* CLI block processor — wraps block-scanner with Node-specific index injection
|
||||
* and content stripping.
|
||||
*
|
||||
* Uses Node `crypto` and `path` — not safe for browser import.
|
||||
*/
|
||||
|
||||
import { posix } from "path";
|
||||
import { createHash } from "crypto";
|
||||
import Slugger from "github-slugger";
|
||||
import {
|
||||
parseStatYaml,
|
||||
parseStatCsv,
|
||||
parseTemplateCsv,
|
||||
type StatDef,
|
||||
type StatTemplate,
|
||||
} from "./stat-parser.js";
|
||||
import {
|
||||
FENCED_BLOCK_RE,
|
||||
parseBlockAttrs,
|
||||
resolveBlockAs,
|
||||
parseSparkTableCsv,
|
||||
} from "./block-scanner.js";
|
||||
import type { SparkTableCompletion } from "./types.js";
|
||||
|
||||
// Re-export shared pieces for convenience
|
||||
export {
|
||||
FENCED_BLOCK_RE,
|
||||
parseBlockAttrs,
|
||||
resolveBlockAs,
|
||||
parseSparkTableCsv,
|
||||
type BlockAttrs,
|
||||
} from "./block-scanner.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ProcessedBlocks {
|
||||
stats: StatDef[];
|
||||
statTemplates: StatTemplate[];
|
||||
sparkTables: SparkTableCompletion[];
|
||||
}
|
||||
|
||||
export interface BlockResult {
|
||||
/** Content with blocks processed (stripped or replaced with directives) */
|
||||
stripped: string;
|
||||
/** Parsed blocks for completions */
|
||||
blocks: ProcessedBlocks;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Content hash
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function contentHash(body: string): string {
|
||||
return createHash("md5").update(body).digest("hex").slice(0, 8);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main processor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Process all attributed fenced code blocks in a markdown file.
|
||||
*
|
||||
* - Strips/replaces blocks based on `as`
|
||||
* - Injects `role=file` bodies into the content index
|
||||
* - Collects stat/template/spark-table blocks for completions
|
||||
*/
|
||||
export function processBlocks(
|
||||
content: string,
|
||||
fileRelativePath: string,
|
||||
index: Record<string, string>,
|
||||
): BlockResult {
|
||||
const fileDir = posix.dirname(fileRelativePath);
|
||||
const slugger = new Slugger();
|
||||
|
||||
const blocks: ProcessedBlocks = {
|
||||
stats: [],
|
||||
statTemplates: [],
|
||||
sparkTables: [],
|
||||
};
|
||||
|
||||
const stripped = content.replace(
|
||||
FENCED_BLOCK_RE,
|
||||
(
|
||||
_match: string,
|
||||
lang: string,
|
||||
infoString: string,
|
||||
body: string,
|
||||
): string => {
|
||||
const attrs = parseBlockAttrs(infoString);
|
||||
attrs.lang = attrs.lang || lang;
|
||||
const effectiveAs = resolveBlockAs(attrs.role, attrs.as);
|
||||
|
||||
// ---- Dispatch by role ----
|
||||
|
||||
if (attrs.role === "stat") {
|
||||
if (attrs.lang === "yaml" || attrs.lang === "yml") {
|
||||
blocks.stats.push(...parseStatYaml(body, fileRelativePath));
|
||||
} else if (attrs.lang === "csv") {
|
||||
blocks.stats.push(...parseStatCsv(body, fileRelativePath));
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.role === "stat-template") {
|
||||
const name = attrs.id || `_tpl_${contentHash(body)}`;
|
||||
blocks.statTemplates.push(
|
||||
parseTemplateCsv(body, fileRelativePath, name),
|
||||
);
|
||||
}
|
||||
|
||||
if (attrs.role === "spark-table") {
|
||||
const parsed = parseSparkTableCsv(body, fileRelativePath, slugger);
|
||||
if (parsed) blocks.sparkTables.push(parsed);
|
||||
}
|
||||
|
||||
if (attrs.role === "file") {
|
||||
const filename = attrs.id
|
||||
? `${attrs.id}.${attrs.lang || "txt"}`
|
||||
: `_inline_${contentHash(body)}.${attrs.lang || "txt"}`;
|
||||
const resolvedPath = posix.join(fileDir, filename);
|
||||
index[resolvedPath] = body;
|
||||
}
|
||||
|
||||
// ---- Render by as ----
|
||||
|
||||
if (effectiveAs === "codeblock") {
|
||||
return _match; // keep as-is
|
||||
}
|
||||
|
||||
if (effectiveAs === "none") {
|
||||
return ""; // strip
|
||||
}
|
||||
|
||||
// Directive: :md-table[./file.csv], :md-card[./file.csv], :md-dice[./file.csv]
|
||||
if (effectiveAs.startsWith("md-")) {
|
||||
const filename = attrs.id
|
||||
? `${attrs.id}.${attrs.lang || "txt"}`
|
||||
: `_inline_${contentHash(body)}.${attrs.lang || "txt"}`;
|
||||
const resolvedPath = posix.join(fileDir, filename);
|
||||
|
||||
// Ensure body is in the index for directive rendering
|
||||
if (!index[resolvedPath]) {
|
||||
index[resolvedPath] = body;
|
||||
}
|
||||
|
||||
// Collect extra attrs for the directive
|
||||
const extra = { ...attrs.extra };
|
||||
const extraStr = Object.keys(extra).length
|
||||
? `{${Object.entries(extra)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(" ")}}`
|
||||
: "";
|
||||
return `:${effectiveAs}[./${filename}]${extraStr}`;
|
||||
}
|
||||
|
||||
// Unknown as → strip
|
||||
return "";
|
||||
},
|
||||
);
|
||||
|
||||
return { stripped, blocks };
|
||||
}
|
||||
Reference in New Issue
Block a user