/** * 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 { parseDeclareCsv, type VarDeclaration, type TagModifier } from "./declare-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 { declarations: VarDeclaration[]; tagModifiers: TagModifier[]; } 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 declare 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 = { declarations: [], tagModifiers: [], }; 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 === "declare") { try { const result = parseDeclareCsv(body, fileRelativePath); blocks.declarations.push(...result.variables); blocks.tagModifiers.push(...result.tagModifiers); } catch (e) { console.warn(`[block-processor] ${fileRelativePath}: ${e}`); } } 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 }; }