2 Commits
v0.1.9 ... main

Author SHA1 Message Date
Gregor Lohaus
452d872954 meta, repo url in package.json
All checks were successful
Publish npm package / publish (push) Successful in 30s
2026-05-26 09:28:05 +02:00
Gregor Lohaus
e900729c8f better errors, and/or support
All checks were successful
Publish npm package / publish (push) Successful in 24s
2026-05-24 16:14:08 +02:00
3 changed files with 28 additions and 18 deletions

View File

@@ -451,7 +451,7 @@ test("file if",() => {
}) })
test("no endif should throw",() => { 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",() => { test("path vars cannot write outside target",() => {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@gregorlohaus/tdir", "name": "@gregorlohaus/tdir",
"version": "0.1.9", "version": "0.2.1",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
@@ -25,5 +25,9 @@
}, },
"peerDependencies": { "peerDependencies": {
"zod": "^4" "zod": "^4"
},
"repository": {
"type": "git",
"url": "https://gitea.gregorlohaus.com/gregor/tdir"
} }
} }

View File

@@ -1,5 +1,5 @@
import { readdirSync, statSync, readFileSync } from "node:fs" import { readdirSync, statSync, readFileSync } from "node:fs"
import { join } from "node:path" import { join, relative } from "node:path"
import { TextDecoder } from "node:util" import { TextDecoder } from "node:util"
import { getDirectiveTokens, splitArgs } from "./scanner" import { getDirectiveTokens, splitArgs } from "./scanner"
@@ -36,10 +36,15 @@ function extractCondition(expr: string | undefined, vars: TemplateVariable[]) {
throw new Error(`Invalid condition expression: ${expr}`) 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)) { for (const token of getDirectiveTokens(text)) {
if (token.type === "if" || token.type === "elseif") { 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)) { 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 }[] = [] const stack: { sawElse: boolean }[] = []
for (const token of getDirectiveTokens(content)) { for (const token of getDirectiveTokens(content)) {
@@ -59,22 +64,22 @@ function validateIfBlocks(content: string, vars: TemplateVariable[]) {
stack.push({ sawElse: false }) stack.push({ sawElse: false })
} else if (directive === "elseif") { } else if (directive === "elseif") {
const frame = stack[stack.length - 1] const frame = stack[stack.length - 1]
if (!frame) throw new Error("Unexpected <@elseif> without <@if>") if (!frame) throw new Error(`${source}: Unexpected <@elseif> without <@if>`)
if (frame.sawElse) throw new Error("Unexpected <@elseif> after <@else>") if (frame.sawElse) throw new Error(`${source}: Unexpected <@elseif> after <@else>`)
extractCondition(condition!, vars) extractCondition(condition!, vars)
} else if (directive === "else") { } else if (directive === "else") {
const frame = stack[stack.length - 1] const frame = stack[stack.length - 1]
if (!frame) throw new Error("Unexpected <@else> without <@if>") if (!frame) throw new Error(`${source}: Unexpected <@else> without <@if>`)
if (frame.sawElse) throw new Error("Unexpected duplicate <@else>") if (frame.sawElse) throw new Error(`${source}: Unexpected duplicate <@else>`)
frame.sawElse = true frame.sawElse = true
} else if (directive === "endif") { } 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() stack.pop()
} }
} }
if (stack.length > 0) { 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() const entries = readdirSync(dirPath).sort()
for (const entry of entries) { for (const entry of entries) {
const fullPath = join(dirPath, entry) const fullPath = join(dirPath, entry)
extractFromString(entry, vars) const relativePath = relative(rootPath, fullPath)
extractFromString(entry, vars, relativePath || entry)
const stat = statSync(fullPath) const stat = statSync(fullPath)
if (stat.isDirectory()) { if (stat.isDirectory()) {
walkDir(fullPath, vars) walkDir(fullPath, vars, rootPath)
} else if (stat.isFile()) { } else if (stat.isFile()) {
const content = readFileSync(fullPath) const content = readFileSync(fullPath)
if (isUtf8Text(content)) { if (isUtf8Text(content)) {
const text = content.toString("utf-8") const text = content.toString("utf-8")
extractFromString(text, vars) extractFromString(text, vars, relativePath || fullPath)
validateIfBlocks(text, vars) validateIfBlocks(text, vars, relativePath || fullPath)
} }
} }
} }
@@ -110,7 +116,7 @@ function walkDir(dirPath: string, vars: TemplateVariable[]) {
export function parse(dirPath: string): TemplateVariable[] { export function parse(dirPath: string): TemplateVariable[] {
const vars: TemplateVariable[] = [] const vars: TemplateVariable[] = []
walkDir(dirPath, vars) walkDir(dirPath, vars, dirPath)
const seen = new Set<string>() const seen = new Set<string>()
return vars.filter(v => { return vars.filter(v => {
const key = `${v.path}:${v.type}` const key = `${v.path}:${v.type}`