map rendered strings to template token source
This commit is contained in:
157
render.ts
157
render.ts
@@ -17,6 +17,41 @@ const DIRECTIVE_RE = /<@(if|elseif|else|endif)(?:\((.+?)\))?>/g
|
||||
const EQ_RE = /^eq\(context\.(.+?),\s*"(.*)"\)$/
|
||||
const PATH_RE = /^context\.(.+)$/
|
||||
|
||||
export type ReverseMapToken = {
|
||||
kind: "path" | "content"
|
||||
result: string
|
||||
token: string
|
||||
contextPath?: string
|
||||
outputPath: string
|
||||
templatePath: string
|
||||
range?: {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
}
|
||||
|
||||
export type ReverseMapFile = {
|
||||
outputPath: string
|
||||
templatePath: string
|
||||
tokens: ReverseMapToken[]
|
||||
}
|
||||
|
||||
export type ReverseMapManifest = {
|
||||
version: 1
|
||||
files: ReverseMapFile[]
|
||||
tokens: Record<string, string[]>
|
||||
}
|
||||
|
||||
export type RenderOptions = {
|
||||
reverseMap?: boolean | string
|
||||
}
|
||||
|
||||
type RenderState = {
|
||||
sourceRoot: string
|
||||
destRoot: string
|
||||
manifest?: ReverseMapManifest
|
||||
}
|
||||
|
||||
function evalCondition(expr: string | undefined, context: Record<string, unknown>): boolean {
|
||||
if (!expr) throw new Error("Missing condition expression")
|
||||
const eqMatch = expr.match(EQ_RE)
|
||||
@@ -74,16 +109,60 @@ function resolveOutputPath(destRoot: string, outputName: string): string {
|
||||
return outputPath
|
||||
}
|
||||
|
||||
function getOutputName(entry: string, context: Record<string, unknown>): string | null {
|
||||
function createReverseMapManifest(): ReverseMapManifest {
|
||||
return { version: 1, files: [], tokens: {} }
|
||||
}
|
||||
|
||||
function addReverseMapToken(
|
||||
state: RenderState | undefined,
|
||||
file: ReverseMapFile | undefined,
|
||||
token: ReverseMapToken,
|
||||
) {
|
||||
if (!state?.manifest || !file) return
|
||||
file.tokens.push(token)
|
||||
const tokens = state.manifest.tokens[token.result] ?? []
|
||||
if (!tokens.includes(token.token)) tokens.push(token.token)
|
||||
state.manifest.tokens[token.result] = tokens
|
||||
}
|
||||
|
||||
function getReverseMapPath(destRoot: string, reverseMap: boolean | string): string {
|
||||
if (reverseMap === true) return resolveOutputPath(destRoot, ".tdir-map.json")
|
||||
return resolveOutputPath(destRoot, reverseMap)
|
||||
}
|
||||
|
||||
function getOutputName(
|
||||
entry: string,
|
||||
context: Record<string, unknown>,
|
||||
state?: RenderState,
|
||||
file?: ReverseMapFile,
|
||||
): string | null {
|
||||
const ifMatch = entry.match(IF_PATH_RE)
|
||||
let outputName = entry
|
||||
if (ifMatch) {
|
||||
if (!evalCondition(ifMatch[1], context)) return null
|
||||
outputName = ifMatch[2]!
|
||||
if (ifMatch[0] !== outputName) {
|
||||
addReverseMapToken(state, file, {
|
||||
kind: "path",
|
||||
result: outputName,
|
||||
token: ifMatch[0],
|
||||
outputPath: file?.outputPath ?? "",
|
||||
templatePath: file?.templatePath ?? "",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return outputName.replace(VAR_RE, (_match, path: string) => {
|
||||
return String(resolveContext(context, path) ?? "")
|
||||
const result = String(resolveContext(context, path) ?? "")
|
||||
addReverseMapToken(state, file, {
|
||||
kind: "path",
|
||||
result,
|
||||
token: _match,
|
||||
contextPath: path,
|
||||
outputPath: file?.outputPath ?? "",
|
||||
templatePath: file?.templatePath ?? "",
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -162,22 +241,64 @@ function processIfBlocks(content: string, context: Record<string, unknown>): str
|
||||
}
|
||||
|
||||
function renderContent(content: string, context: Record<string, unknown>): string {
|
||||
return renderContentWithMap(content, context)
|
||||
}
|
||||
|
||||
function renderContentWithMap(
|
||||
content: string,
|
||||
context: Record<string, unknown>,
|
||||
state?: RenderState,
|
||||
file?: ReverseMapFile,
|
||||
): string {
|
||||
const processed = processIfBlocks(content, context)
|
||||
return processed.replace(VAR_RE, (_match, path: string) => {
|
||||
return String(resolveContext(context, path) ?? "")
|
||||
})
|
||||
let result = ""
|
||||
let pos = 0
|
||||
|
||||
for (const match of processed.matchAll(VAR_RE)) {
|
||||
const token = match[0]
|
||||
const path = match[1]!
|
||||
const rendered = String(resolveContext(context, path) ?? "")
|
||||
result += processed.slice(pos, match.index)
|
||||
const start = result.length
|
||||
result += rendered
|
||||
addReverseMapToken(state, file, {
|
||||
kind: "content",
|
||||
result: rendered,
|
||||
token,
|
||||
contextPath: path,
|
||||
outputPath: file?.outputPath ?? "",
|
||||
templatePath: file?.templatePath ?? "",
|
||||
range: { start, end: start + rendered.length },
|
||||
})
|
||||
pos = match.index! + token.length
|
||||
}
|
||||
|
||||
result += processed.slice(pos)
|
||||
return result
|
||||
}
|
||||
|
||||
function renderDir(
|
||||
srcDir: string,
|
||||
destDir: string,
|
||||
context: Record<string, unknown>,
|
||||
options: RenderOptions = {},
|
||||
) {
|
||||
const destRoot = resolvePath(destDir)
|
||||
const sourceRoot = resolvePath(srcDir)
|
||||
const state: RenderState = {
|
||||
sourceRoot,
|
||||
destRoot,
|
||||
manifest: options.reverseMap ? createReverseMapManifest() : undefined,
|
||||
}
|
||||
const mapPath = options.reverseMap ? getReverseMapPath(destRoot, options.reverseMap) : undefined
|
||||
assertSafeRenderTarget(srcDir, destDir)
|
||||
validateOutputPaths(srcDir, destRoot, context)
|
||||
rmSync(destDir, { recursive: true, force: true })
|
||||
renderDirInner(srcDir, destRoot, context)
|
||||
renderDirInner(srcDir, destRoot, context, state)
|
||||
if (state.manifest && mapPath) {
|
||||
mkdirSync(dirname(mapPath), { recursive: true })
|
||||
writeFileSync(mapPath, `${JSON.stringify(state.manifest, null, 2)}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
function validateOutputPaths(
|
||||
@@ -204,6 +325,7 @@ function renderDirInner(
|
||||
srcDir: string,
|
||||
destDir: string,
|
||||
context: Record<string, unknown>,
|
||||
state: RenderState,
|
||||
) {
|
||||
mkdirSync(destDir, { recursive: true })
|
||||
const entries = readdirSync(srcDir).sort()
|
||||
@@ -211,21 +333,36 @@ function renderDirInner(
|
||||
for (const entry of entries) {
|
||||
const srcPath = resolvePath(srcDir, entry)
|
||||
const stat = statSync(srcPath)
|
||||
|
||||
const outputName = getOutputName(entry, context)
|
||||
const templatePath = relative(state.sourceRoot, srcPath)
|
||||
const tempFile: ReverseMapFile = {
|
||||
outputPath: "",
|
||||
templatePath,
|
||||
tokens: [],
|
||||
}
|
||||
const outputName = getOutputName(entry, context, state, tempFile)
|
||||
if (outputName === null) continue
|
||||
|
||||
const destPath = resolveOutputPath(destDir, outputName)
|
||||
const outputPath = relative(state.destRoot, destPath)
|
||||
tempFile.outputPath = outputPath
|
||||
for (const token of tempFile.tokens) {
|
||||
token.outputPath = outputPath
|
||||
token.templatePath = templatePath
|
||||
}
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
renderDirInner(srcPath, destPath, context)
|
||||
if (tempFile.tokens.length > 0) state.manifest?.files.push(tempFile)
|
||||
renderDirInner(srcPath, destPath, context, state)
|
||||
} else {
|
||||
mkdirSync(dirname(destPath), { recursive: true })
|
||||
const content = readFileSync(srcPath)
|
||||
if (isUtf8Text(content)) {
|
||||
writeFileSync(destPath, renderContent(content.toString("utf-8"), context))
|
||||
const rendered = renderContentWithMap(content.toString("utf-8"), context, state, tempFile)
|
||||
writeFileSync(destPath, rendered)
|
||||
} else {
|
||||
copyFileSync(srcPath, destPath)
|
||||
}
|
||||
if (tempFile.tokens.length > 0) state.manifest?.files.push(tempFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user