dry stuff up
This commit is contained in:
100
parser.ts
100
parser.ts
@@ -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
101
render.ts
@@ -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
99
scanner.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user