diff --git a/index.test.ts b/index.test.ts index 6372009..c031f5d 100644 --- a/index.test.ts +++ b/index.test.ts @@ -451,7 +451,7 @@ test("file if",() => { }) test("no endif should throw",() => { - expect(() => initRenderer("./testdata/no_end_if")).toThrow() + expect(() => initRenderer("./testdata/no_end_if")).toThrow("test.txt") }) test("path vars cannot write outside target",() => { diff --git a/package.json b/package.json index b1a861c..32aba3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gregorlohaus/tdir", - "version": "0.1.9", + "version": "0.2.0", "license": "MIT", "type": "module", "main": "./dist/index.js", diff --git a/parser.ts b/parser.ts index 87b0dc7..8e75e84 100644 --- a/parser.ts +++ b/parser.ts @@ -1,5 +1,5 @@ import { readdirSync, statSync, readFileSync } from "node:fs" -import { join } from "node:path" +import { join, relative } from "node:path" import { TextDecoder } from "node:util" import { getDirectiveTokens, splitArgs } from "./scanner" @@ -36,10 +36,15 @@ function extractCondition(expr: string | undefined, vars: TemplateVariable[]) { throw new Error(`Invalid condition expression: ${expr}`) } -function extractFromString(text: string, vars: TemplateVariable[]) { +function extractFromString(text: string, vars: TemplateVariable[], source = "template") { for (const token of getDirectiveTokens(text)) { if (token.type === "if" || token.type === "elseif") { - extractCondition(token.condition, vars) + try { + extractCondition(token.condition, vars) + } catch (error) { + if (error instanceof Error) throw new Error(`${source}: ${error.message}`) + throw error + } } } for (const match of text.matchAll(VAR_RE)) { @@ -47,7 +52,7 @@ function extractFromString(text: string, vars: TemplateVariable[]) { } } -function validateIfBlocks(content: string, vars: TemplateVariable[]) { +function validateIfBlocks(content: string, vars: TemplateVariable[], source: string) { const stack: { sawElse: boolean }[] = [] for (const token of getDirectiveTokens(content)) { @@ -59,22 +64,22 @@ function validateIfBlocks(content: string, vars: TemplateVariable[]) { stack.push({ sawElse: false }) } else if (directive === "elseif") { const frame = stack[stack.length - 1] - if (!frame) throw new Error("Unexpected <@elseif> without <@if>") - if (frame.sawElse) throw new Error("Unexpected <@elseif> after <@else>") + if (!frame) throw new Error(`${source}: Unexpected <@elseif> without <@if>`) + if (frame.sawElse) throw new Error(`${source}: Unexpected <@elseif> after <@else>`) extractCondition(condition!, vars) } else if (directive === "else") { const frame = stack[stack.length - 1] - if (!frame) throw new Error("Unexpected <@else> without <@if>") - if (frame.sawElse) throw new Error("Unexpected duplicate <@else>") + if (!frame) throw new Error(`${source}: Unexpected <@else> without <@if>`) + if (frame.sawElse) throw new Error(`${source}: Unexpected duplicate <@else>`) frame.sawElse = true } else if (directive === "endif") { - if (stack.length === 0) throw new Error("Unexpected <@endif> without <@if>") + if (stack.length === 0) throw new Error(`${source}: Unexpected <@endif> without <@if>`) stack.pop() } } if (stack.length > 0) { - throw new Error("Unmatched <@if> without <@endif>") + throw new Error(`${source}: Unmatched <@if> without <@endif>`) } } @@ -88,21 +93,22 @@ function isUtf8Text(buffer: Buffer): boolean { } } -function walkDir(dirPath: string, vars: TemplateVariable[]) { +function walkDir(dirPath: string, vars: TemplateVariable[], rootPath: string) { const entries = readdirSync(dirPath).sort() for (const entry of entries) { const fullPath = join(dirPath, entry) - extractFromString(entry, vars) + const relativePath = relative(rootPath, fullPath) + extractFromString(entry, vars, relativePath || entry) const stat = statSync(fullPath) if (stat.isDirectory()) { - walkDir(fullPath, vars) + walkDir(fullPath, vars, rootPath) } else if (stat.isFile()) { const content = readFileSync(fullPath) if (isUtf8Text(content)) { const text = content.toString("utf-8") - extractFromString(text, vars) - validateIfBlocks(text, vars) + extractFromString(text, vars, relativePath || fullPath) + validateIfBlocks(text, vars, relativePath || fullPath) } } } @@ -110,7 +116,7 @@ function walkDir(dirPath: string, vars: TemplateVariable[]) { export function parse(dirPath: string): TemplateVariable[] { const vars: TemplateVariable[] = [] - walkDir(dirPath, vars) + walkDir(dirPath, vars, dirPath) const seen = new Set() return vars.filter(v => { const key = `${v.path}:${v.type}`