Created
April 25, 2021 21:34
-
-
Save janispritzkau/ba9571ff973aae98a4ad972f1a52f686 to your computer and use it in GitHub Desktop.
Deno EJS Template Engine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { dirname, extname, resolve } from "https://deno.land/std/path/mod.ts"; | |
const MODE_EVAL = 0; | |
const MODE_ESCAPED = 1; | |
const MODE_RAW = 2; | |
interface Range { | |
source: [number, number]; | |
code: [number, number]; | |
} | |
function compile(source: string) { | |
const parts: string[] = []; | |
let code = `return(async()=>{let _out="";`; | |
let pos = 0; | |
let i = 0; | |
while (i < source.length - 1) { | |
if (source[i] == "<" && source[i + 1] == "%") { | |
if (pos != i) { | |
code += | |
`_range[0]=${pos};_range[1]=${i};_out+=_parts[${parts.length}];`; | |
parts.push(source.slice(pos, i)); | |
} | |
i += 2; | |
if (i == source.length) throw new SyntaxError("Unexpected end"); | |
const mode = source[i] != "-" | |
? source[i] != "=" ? MODE_EVAL : MODE_ESCAPED | |
: MODE_RAW; | |
if (mode != MODE_EVAL) i++; | |
pos = i; | |
while (true) { | |
if (i == source.length) throw new SyntaxError("Unexpected end"); | |
if (source[i] == "%" && source[i + 1] == ">") break; | |
i++; | |
} | |
const codeStart = code.length; | |
const text = source.slice(pos, i).trim(); | |
code += `_range[0]=${pos};_range[1]=${i};`; | |
if (mode == MODE_EVAL) { | |
code += `${text};`; | |
} else if (mode == MODE_ESCAPED) { | |
code += `_out+=_escape(String(${text}));`; | |
} else { | |
code += `_out+=String(${text});`; | |
} | |
pos = i += 2; | |
continue; | |
} | |
i++; | |
} | |
if (pos != i) { | |
code += `_range[0]=${pos};_range[1]=${i};_out+=_parts[${parts.length}];`; | |
parts.push(source.slice(pos, i)); | |
} | |
code += `return _out;})()`; | |
return { parts, code }; | |
} | |
function escape(text: string) { | |
return text.replace(/[&<>"']/g, (c) => { | |
if (c == "&") return "&"; | |
if (c == "<") return "<"; | |
if (c == ">") return ">"; | |
if (c == '"') return """; | |
return "'"; | |
}); | |
} | |
export interface RenderOptions { | |
root?: string; | |
path?: string; | |
customInclude?: ( | |
path: string, | |
params?: Record<string, any>, | |
) => Promise<string>; | |
} | |
async function include( | |
path: string, | |
params: Record<string, any>, | |
options?: RenderOptions, | |
) { | |
const source = await Deno.readTextFile(path); | |
const ret = await render(source, params, options); | |
return ret; | |
} | |
export async function render( | |
source: string, | |
params: Record<string, any>, | |
options?: RenderOptions, | |
) { | |
const { parts, code } = compile(source); | |
const root = resolve(options?.root ?? ""); | |
const errRange = [0, source.length]; | |
const args = new Map(Object.entries(params)); | |
args.set("_range", errRange); | |
args.set("_parts", parts); | |
args.set("_escape", escape); | |
args.set("include", async (path: string, params: Record<string, any>) => { | |
path = resolve(root, path); | |
if (extname(path) != ".ejs" && options?.customInclude) { | |
return await options.customInclude(path, params); | |
} | |
return await include(path, params ?? {}, { | |
...options, | |
path, | |
root: dirname(path), | |
}); | |
}); | |
let render: Function; | |
try { | |
render = new Function(...args.keys(), code); | |
} catch (e) { | |
throw new Error(`Syntax error in ${options?.path ?? "<source>"}`); | |
} | |
try { | |
return await render(...args.values()); | |
} catch (e) { | |
const { line, col } = findLineCol(source, errRange[0]); | |
throw new Error( | |
`${e.message}\nat ${options?.path ?? "<source>"}:${line}:${col}`, | |
); | |
} | |
} | |
export async function renderFile( | |
path: string, | |
params: Record<string, any>, | |
options?: RenderOptions, | |
) { | |
path = resolve(path); | |
return await include(path, params, { | |
...options, | |
path, | |
root: options?.root ?? dirname(path), | |
}); | |
} | |
function findLineCol(text: string, pos: number) { | |
let line = 1; | |
let col = 1; | |
for (let i = 0; i < pos; i++) { | |
if (text[i] == "\n") { | |
line++; | |
col = 0; | |
} | |
col++; | |
} | |
return { line, col }; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment