From 9ed1324e062de8edffa1cd3b9f00da901509ba84 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Sun, 24 May 2026 16:03:21 +0200 Subject: [PATCH] dry stuff up --- parser.ts | 100 +------------------------------------------ render.ts | 101 +------------------------------------------- scanner.ts | 99 +++++++++++++++++++++++++++++++++++++++++++ tsconfig.build.json | 2 +- 4 files changed, 102 insertions(+), 200 deletions(-) create mode 100644 scanner.ts diff --git a/parser.ts b/parser.ts index 61b46f7..87b0dc7 100644 --- a/parser.ts +++ b/parser.ts @@ -1,115 +1,17 @@ import { readdirSync, statSync, readFileSync } from "node:fs" import { join } from "node:path" import { TextDecoder } from "node:util" +import { getDirectiveTokens, splitArgs } from "./scanner" export type TemplateVariable = { path: string type: string } -const IF_RE = /<@(?:if|elseif)\((.+?)\)>/g const VAR_RE = /<@var\(context\.(.+?)(?::(\w+))?\)>/g const STRING_COMPARE_RE = /^(?:eq|neq)\(context\.(.+?),\s*"(.*)"\)$/ const PATH_RE = /^context\.(.+)$/ -type DirectiveToken = { - type: "if" | "elseif" | "else" | "endif" - condition?: string -} - -function readCondition(text: string, start: number): { condition: string; end: number } | null { - let depth = 0 - let inString = false - let escaped = false - - for (let i = start; i < text.length; i++) { - const char = text[i]! - if (escaped) { - escaped = false - continue - } - if (char === "\\") { - escaped = true - continue - } - if (char === "\"") { - inString = !inString - continue - } - if (inString) continue - if (char === "(") { - depth += 1 - continue - } - if (char === ")") { - depth -= 1 - if (depth === 0 && text[i + 1] === ">") { - return { condition: text.slice(start + 1, i), end: i + 2 } - } - } - } - - return null -} - -function getDirectiveTokens(text: string): DirectiveToken[] { - const tokens: DirectiveToken[] = [] - for (let i = 0; i < text.length; i++) { - if (text[i] !== "<" || text[i + 1] !== "@") continue - const rest = text.slice(i + 2) - const type = ["elseif", "endif", "else", "if"].find(name => rest.startsWith(name)) as DirectiveToken["type"] | undefined - if (!type) continue - const afterName = i + 2 + type.length - if ((type === "if" || type === "elseif") && text[afterName] === "(") { - const parsed = readCondition(text, afterName) - if (!parsed) continue - tokens.push({ type, condition: parsed.condition }) - i = parsed.end - 1 - } else if ((type === "else" || type === "endif") && text[afterName] === ">") { - tokens.push({ type }) - i = afterName - } - } - return tokens -} - -function splitArgs(args: string): string[] { - const result: string[] = [] - let current = "" - let depth = 0 - let inString = false - let escaped = false - - for (const char of args) { - if (escaped) { - current += char - escaped = false - continue - } - if (char === "\\") { - current += char - escaped = true - continue - } - if (char === "\"") { - current += char - inString = !inString - continue - } - if (!inString && char === "(") depth += 1 - if (!inString && char === ")") depth -= 1 - if (!inString && depth === 0 && char === ",") { - result.push(current.trim()) - current = "" - continue - } - current += char - } - - if (current.trim() !== "") result.push(current.trim()) - return result -} - function extractCondition(expr: string | undefined, vars: TemplateVariable[]) { if (!expr) throw new Error("Missing condition expression") for (const operator of ["and", "or"]) { diff --git a/render.ts b/render.ts index aff4061..8fafe46 100644 --- a/render.ts +++ b/render.ts @@ -10,6 +10,7 @@ import { import { dirname, isAbsolute, relative, resolve as resolvePath } from "node:path" import { homedir } from "node:os" import { TextDecoder } from "node:util" +import { getDirectiveTokens, splitArgs, type DirectiveToken } from "./scanner" const IF_PATH_RE = /^<@if\((.+?)\)>(.*)$/ const VAR_RE = /<@var\(context\.(.+?)(?::(\w+))?\)>/g @@ -65,43 +66,6 @@ type RenderState = { manifest?: ReverseMapManifest } -function splitArgs(args: string): string[] { - const result: string[] = [] - let current = "" - let depth = 0 - let inString = false - let escaped = false - - for (const char of args) { - if (escaped) { - current += char - escaped = false - continue - } - if (char === "\\") { - current += char - escaped = true - continue - } - if (char === "\"") { - current += char - inString = !inString - continue - } - if (!inString && char === "(") depth += 1 - if (!inString && char === ")") depth -= 1 - if (!inString && depth === 0 && char === ",") { - result.push(current.trim()) - current = "" - continue - } - current += char - } - - if (current.trim() !== "") result.push(current.trim()) - return result -} - function evalCondition(expr: string | undefined, context: Record): boolean { if (!expr) throw new Error("Missing condition expression") for (const operator of ["and", "or"] as const) { @@ -248,48 +212,6 @@ function isUtf8Text(buffer: Buffer): boolean { } } -type DirectiveToken = { - type: "if" | "elseif" | "else" | "endif" - condition?: string - index: number - end: number -} - -function readDirectiveCondition(text: string, start: number): { condition: string; end: number } | null { - let depth = 0 - let inString = false - let escaped = false - - for (let i = start; i < text.length; i++) { - const char = text[i]! - if (escaped) { - escaped = false - continue - } - if (char === "\\") { - escaped = true - continue - } - if (char === "\"") { - inString = !inString - continue - } - if (inString) continue - if (char === "(") { - depth += 1 - continue - } - if (char === ")") { - depth -= 1 - if (depth === 0 && text[i + 1] === ">") { - return { condition: text.slice(start + 1, i), end: i + 2 } - } - } - } - - return null -} - type TextNode = { type: "text" text: string @@ -326,27 +248,6 @@ type ConditionalRender = { } } -function getDirectiveTokens(content: string): DirectiveToken[] { - const tokens: DirectiveToken[] = [] - for (let i = 0; i < content.length; i++) { - if (content[i] !== "<" || content[i + 1] !== "@") continue - const rest = content.slice(i + 2) - const type = ["elseif", "endif", "else", "if"].find(name => rest.startsWith(name)) as DirectiveToken["type"] | undefined - if (!type) continue - const afterName = i + 2 + type.length - if ((type === "if" || type === "elseif") && content[afterName] === "(") { - const parsed = readDirectiveCondition(content, afterName) - if (!parsed) continue - tokens.push({ type, condition: parsed.condition, index: i, end: parsed.end }) - i = parsed.end - 1 - } else if ((type === "else" || type === "endif") && content[afterName] === ">") { - tokens.push({ type, index: i, end: afterName + 1 }) - i = afterName - } - } - return tokens -} - function parseNodes( content: string, tokens: DirectiveToken[], diff --git a/scanner.ts b/scanner.ts new file mode 100644 index 0000000..2e67647 --- /dev/null +++ b/scanner.ts @@ -0,0 +1,99 @@ +export type DirectiveToken = { + type: "if" | "elseif" | "else" | "endif" + condition?: string + index: number + end: number +} + +function readCondition(text: string, start: number): { condition: string; end: number } | null { + let depth = 0 + let inString = false + let escaped = false + + for (let i = start; i < text.length; i++) { + const char = text[i]! + if (escaped) { + escaped = false + continue + } + if (char === "\\") { + escaped = true + continue + } + if (char === "\"") { + inString = !inString + continue + } + if (inString) continue + if (char === "(") { + depth += 1 + continue + } + if (char === ")") { + depth -= 1 + if (depth === 0 && text[i + 1] === ">") { + return { condition: text.slice(start + 1, i), end: i + 2 } + } + } + } + + return null +} + +export function getDirectiveTokens(text: string): DirectiveToken[] { + const tokens: DirectiveToken[] = [] + for (let i = 0; i < text.length; i++) { + if (text[i] !== "<" || text[i + 1] !== "@") continue + const rest = text.slice(i + 2) + const type = ["elseif", "endif", "else", "if"].find(name => rest.startsWith(name)) as DirectiveToken["type"] | undefined + if (!type) continue + const afterName = i + 2 + type.length + if ((type === "if" || type === "elseif") && text[afterName] === "(") { + const parsed = readCondition(text, afterName) + if (!parsed) continue + tokens.push({ type, condition: parsed.condition, index: i, end: parsed.end }) + i = parsed.end - 1 + } else if ((type === "else" || type === "endif") && text[afterName] === ">") { + tokens.push({ type, index: i, end: afterName + 1 }) + i = afterName + } + } + return tokens +} + +export function splitArgs(args: string): string[] { + const result: string[] = [] + let current = "" + let depth = 0 + let inString = false + let escaped = false + + for (const char of args) { + if (escaped) { + current += char + escaped = false + continue + } + if (char === "\\") { + current += char + escaped = true + continue + } + if (char === "\"") { + current += char + inString = !inString + continue + } + if (!inString && char === "(") depth += 1 + if (!inString && char === ")") depth -= 1 + if (!inString && depth === 0 && char === ",") { + result.push(current.trim()) + current = "" + continue + } + current += char + } + + if (current.trim() !== "") result.push(current.trim()) + return result +} diff --git a/tsconfig.build.json b/tsconfig.build.json index e820b8c..e277a5f 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -6,5 +6,5 @@ "emitDeclarationOnly": true, "outDir": "./dist" }, - "include": ["index.ts", "parser.ts", "render.ts", "reverse.ts"] + "include": ["index.ts", "parser.ts", "render.ts", "reverse.ts", "scanner.ts"] }