import { copyFileSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs" import { dirname, isAbsolute, relative, resolve as resolvePath } from "node:path" import { homedir } from "node:os" import { TextDecoder } from "node:util" const IF_PATH_RE = /^<@if\((.+?)\)>(.*)$/ const VAR_RE = /<@var\(context\.(.+?)(?::(\w+))?\)>/g const DIRECTIVE_RE = /<@(if|elseif|else|endif)(?:\((.+?)\))?>/g const EQ_RE = /^eq\(context\.(.+?),\s*"(.*)"\)$/ const PATH_RE = /^context\.(.+)$/ function evalCondition(expr: string | undefined, context: Record): boolean { if (!expr) throw new Error("Missing condition expression") const eqMatch = expr.match(EQ_RE) if (eqMatch) { return resolveContext(context, eqMatch[1]!) === eqMatch[2] } const pathMatch = expr.match(PATH_RE) if (pathMatch) { return !!resolveContext(context, pathMatch[1]!) } throw new Error(`Invalid condition expression: ${expr}`) } function resolveContext(context: Record, path: string): unknown { const segments = path.split(".") let current: unknown = context for (const seg of segments) { if (current === null || current === undefined || typeof current !== "object") return undefined current = (current as Record)[seg] } return current } function isInsidePath(parent: string, child: string): boolean { const rel = relative(parent, child) return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)) } function assertSafeRenderTarget(srcDir: string, destDir: string) { const sourceRoot = resolvePath(srcDir) const destRoot = resolvePath(destDir) if (destRoot === resolvePath(destRoot, "..")) { throw new Error("Refusing to render into filesystem root") } if (destRoot === resolvePath(process.cwd())) { throw new Error("Refusing to render into the current working directory") } if (destRoot === resolvePath(homedir())) { throw new Error("Refusing to render into the home directory") } if (isInsidePath(sourceRoot, destRoot) || isInsidePath(destRoot, sourceRoot)) { throw new Error("Refusing to render when source and target directories overlap") } } function resolveOutputPath(destRoot: string, outputName: string): string { const outputPath = resolvePath(destRoot, outputName) if (!isInsidePath(destRoot, outputPath)) { throw new Error(`Refusing to write outside target directory: ${outputName}`) } return outputPath } function getOutputName(entry: string, context: Record): string | null { const ifMatch = entry.match(IF_PATH_RE) let outputName = entry if (ifMatch) { if (!evalCondition(ifMatch[1], context)) return null outputName = ifMatch[2]! } return outputName.replace(VAR_RE, (_match, path: string) => { return String(resolveContext(context, path) ?? "") }) } function isUtf8Text(buffer: Buffer): boolean { if (buffer.indexOf(0) !== -1) return false try { new TextDecoder("utf-8", { fatal: true }).decode(buffer) return true } catch { return false } } // Each stack frame tracks: did any branch match yet, is the current branch active type IfFrame = { matched: boolean; active: boolean; sawElse: boolean } function processIfBlocks(content: string, context: Record): string { let result = "" let pos = 0 const stack: IfFrame[] = [] const re = new RegExp(DIRECTIVE_RE.source, "g") let match: RegExpExecArray | null function isEmitting(): boolean { return stack.every(f => f.active) } while ((match = re.exec(content)) !== null) { const directive = match[1]! const condPath = match[2] if (directive === "if") { if (isEmitting()) result += content.slice(pos, match.index) const truthy = evalCondition(condPath, context) stack.push({ matched: truthy, active: truthy, sawElse: false }) pos = re.lastIndex } else if (directive === "elseif") { if (stack.length === 0) throw new Error("Unexpected <@elseif> without <@if>") const top = stack[stack.length - 1]! if (top.sawElse) throw new Error("Unexpected <@elseif> after <@else>") // Emit text before this directive if current branch was active if (isEmitting()) result += content.slice(pos, match.index) if (top.matched) { // A previous branch already matched — skip this one top.active = false } else { const truthy = evalCondition(condPath, context) top.matched = truthy top.active = truthy } pos = re.lastIndex } else if (directive === "else") { if (stack.length === 0) throw new Error("Unexpected <@else> without <@if>") const top = stack[stack.length - 1]! if (top.sawElse) throw new Error("Unexpected duplicate <@else>") if (isEmitting()) result += content.slice(pos, match.index) top.active = !top.matched top.matched = true top.sawElse = true pos = re.lastIndex } else if (directive === "endif") { if (stack.length === 0) throw new Error("Unexpected <@endif> without <@if>") if (isEmitting()) result += content.slice(pos, match.index) stack.pop() pos = re.lastIndex } } if (stack.length > 0) { throw new Error("Unmatched <@if> without <@endif>") } result += content.slice(pos) return result } function renderContent(content: string, context: Record): string { const processed = processIfBlocks(content, context) return processed.replace(VAR_RE, (_match, path: string) => { return String(resolveContext(context, path) ?? "") }) } function renderDir( srcDir: string, destDir: string, context: Record, ) { const destRoot = resolvePath(destDir) assertSafeRenderTarget(srcDir, destDir) validateOutputPaths(srcDir, destRoot, context) rmSync(destDir, { recursive: true, force: true }) renderDirInner(srcDir, destRoot, context) } function validateOutputPaths( srcDir: string, destDir: string, context: Record, ) { const entries = readdirSync(srcDir).sort() for (const entry of entries) { const srcPath = resolvePath(srcDir, entry) const stat = statSync(srcPath) const outputName = getOutputName(entry, context) if (outputName === null) continue const destPath = resolveOutputPath(destDir, outputName) if (stat.isDirectory()) { validateOutputPaths(srcPath, destPath, context) } } } function renderDirInner( srcDir: string, destDir: string, context: Record, ) { mkdirSync(destDir, { recursive: true }) const entries = readdirSync(srcDir).sort() for (const entry of entries) { const srcPath = resolvePath(srcDir, entry) const stat = statSync(srcPath) const outputName = getOutputName(entry, context) if (outputName === null) continue const destPath = resolveOutputPath(destDir, outputName) if (stat.isDirectory()) { renderDirInner(srcPath, destPath, context) } else { mkdirSync(dirname(destPath), { recursive: true }) const content = readFileSync(srcPath) if (isUtf8Text(content)) { writeFileSync(destPath, renderContent(content.toString("utf-8"), context)) } else { copyFileSync(srcPath, destPath) } } } } export { renderDir, renderContent }