/** * 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 { parseStatYaml, parseStatCsv, parseTemplateCsv, parseStatModifiers, type StatDef, type StatTemplate, } from "./stat-parser.js"; import { FENCED_BLOCK_RE, parseBlockAttrs, resolveBlockAs, } from "./block-scanner.js"; // Re-export shared pieces for convenience export { FENCED_BLOCK_RE, parseBlockAttrs, resolveBlockAs, type BlockAttrs, } from "./block-scanner.js"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface ProcessedBlocks { stats: StatDef[]; statTemplates: StatTemplate[]; } 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 blocks for completions * - role=spark-table blocks are converted to :md-table directives * (spark table completions are collected later by the directive scanner) */ export function processBlocks( content: string, fileRelativePath: string, index: Record, ): BlockResult { const fileDir = posix.dirname(fileRelativePath); const blocks: ProcessedBlocks = { stats: [], statTemplates: [], }; 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 === "stat-modifiers") { const id = attrs.id || `_mod_${contentHash(body)}`; const result = parseStatModifiers(body, fileRelativePath, id, "player", attrs.extra["label"]); blocks.stats.push(result.statDef); blocks.stats.push(...result.modifierDefs); blocks.statTemplates.push(result.template); } 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 }; }