dry stuff up

This commit is contained in:
Gregor Lohaus
2026-05-24 16:03:21 +02:00
parent d8536d83de
commit 9ed1324e06
4 changed files with 102 additions and 200 deletions

100
parser.ts
View File

@@ -1,115 +1,17 @@
import { readdirSync, statSync, readFileSync } from "node:fs" import { readdirSync, statSync, readFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { TextDecoder } from "node:util" import { TextDecoder } from "node:util"
import { getDirectiveTokens, splitArgs } from "./scanner"
export type TemplateVariable = { export type TemplateVariable = {
path: string path: string
type: string type: string
} }
const IF_RE = /<@(?:if|elseif)\((.+?)\)>/g
const VAR_RE = /<@var\(context\.(.+?)(?::(\w+))?\)>/g const VAR_RE = /<@var\(context\.(.+?)(?::(\w+))?\)>/g
const STRING_COMPARE_RE = /^(?:eq|neq)\(context\.(.+?),\s*"(.*)"\)$/ const STRING_COMPARE_RE = /^(?:eq|neq)\(context\.(.+?),\s*"(.*)"\)$/
const PATH_RE = /^context\.(.+)$/ 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[]) { function extractCondition(expr: string | undefined, vars: TemplateVariable[]) {
if (!expr) throw new Error("Missing condition expression") if (!expr) throw new Error("Missing condition expression")
for (const operator of ["and", "or"]) { for (const operator of ["and", "or"]) {

101
render.ts
View File

@@ -10,6 +10,7 @@ import {
import { dirname, isAbsolute, relative, resolve as resolvePath } from "node:path" import { dirname, isAbsolute, relative, resolve as resolvePath } from "node:path"
import { homedir } from "node:os" import { homedir } from "node:os"
import { TextDecoder } from "node:util" import { TextDecoder } from "node:util"
import { getDirectiveTokens, splitArgs, type DirectiveToken } from "./scanner"
const IF_PATH_RE = /^<@if\((.+?)\)>(.*)$/ const IF_PATH_RE = /^<@if\((.+?)\)>(.*)$/
const VAR_RE = /<@var\(context\.(.+?)(?::(\w+))?\)>/g const VAR_RE = /<@var\(context\.(.+?)(?::(\w+))?\)>/g
@@ -65,43 +66,6 @@ type RenderState = {
manifest?: ReverseMapManifest 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<string, unknown>): boolean { function evalCondition(expr: string | undefined, context: Record<string, unknown>): boolean {
if (!expr) throw new Error("Missing condition expression") if (!expr) throw new Error("Missing condition expression")
for (const operator of ["and", "or"] as const) { 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 TextNode = {
type: "text" type: "text"
text: string 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( function parseNodes(
content: string, content: string,
tokens: DirectiveToken[], tokens: DirectiveToken[],

99
scanner.ts Normal file
View File

@@ -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
}

View File

@@ -6,5 +6,5 @@
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"outDir": "./dist" "outDir": "./dist"
}, },
"include": ["index.ts", "parser.ts", "render.ts", "reverse.ts"] "include": ["index.ts", "parser.ts", "render.ts", "reverse.ts", "scanner.ts"]
} }