init
This commit is contained in:
123
render.ts
Normal file
123
render.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { readdirSync, statSync, readFileSync, mkdirSync, writeFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
const IF_PATH_RE = /^<@if\(context\.(.+?)\)>(.*)$/
|
||||
const VAR_RE = /<@var\(context\.(.+?)(?::(\w+))?\)>/g
|
||||
const DIRECTIVE_RE = /<@(if|elseif|else|endif)(?:\(context\.(.+?)\))?>/g
|
||||
|
||||
function resolve(context: Record<string, unknown>, 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<string, unknown>)[seg]
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
// Each stack frame tracks: did any branch match yet, is the current branch active
|
||||
type IfFrame = { matched: boolean; active: boolean }
|
||||
|
||||
function processIfBlocks(content: string, context: Record<string, unknown>): 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 = !!resolve(context, condPath!)
|
||||
stack.push({ matched: truthy, active: truthy })
|
||||
pos = re.lastIndex
|
||||
} else if (directive === "elseif") {
|
||||
if (stack.length === 0) throw new Error("Unexpected <@elseif> without <@if>")
|
||||
const top = stack[stack.length - 1]!
|
||||
// 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 = !!resolve(context, condPath!)
|
||||
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 (isEmitting()) result += content.slice(pos, match.index)
|
||||
top.active = !top.matched
|
||||
top.matched = 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, unknown>): string {
|
||||
const processed = processIfBlocks(content, context)
|
||||
return processed.replace(VAR_RE, (_match, path: string) => {
|
||||
return String(resolve(context, path) ?? "")
|
||||
})
|
||||
}
|
||||
|
||||
function renderDir(
|
||||
srcDir: string,
|
||||
destDir: string,
|
||||
context: Record<string, unknown>,
|
||||
) {
|
||||
mkdirSync(destDir, { recursive: true })
|
||||
const entries = readdirSync(srcDir).sort()
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = join(srcDir, entry)
|
||||
const stat = statSync(srcPath)
|
||||
|
||||
// Check if path segment has an @if directive
|
||||
const ifMatch = entry.match(IF_PATH_RE)
|
||||
let outputName = entry
|
||||
if (ifMatch) {
|
||||
const conditionPath = ifMatch[1]!
|
||||
if (!resolve(context, conditionPath)) continue
|
||||
outputName = ifMatch[2]!
|
||||
}
|
||||
|
||||
// Process @var in the output name
|
||||
outputName = outputName.replace(VAR_RE, (_match, path: string) => {
|
||||
return String(resolve(context, path) ?? "")
|
||||
})
|
||||
|
||||
const destPath = join(destDir, outputName)
|
||||
if (stat.isDirectory()) {
|
||||
renderDir(srcPath, destPath, context)
|
||||
} else {
|
||||
mkdirSync(destDir, { recursive: true })
|
||||
const content = readFileSync(srcPath, "utf-8")
|
||||
writeFileSync(destPath, renderContent(content, context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { renderDir, renderContent }
|
||||
Reference in New Issue
Block a user