165 lines
4.7 KiB
TypeScript
165 lines
4.7 KiB
TypeScript
/**
|
|
* 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<string, string>,
|
|
): 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 };
|
|
}
|