Skip to content

Instantly share code, notes, and snippets.

@janispritzkau
Created April 25, 2021 21:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janispritzkau/ba9571ff973aae98a4ad972f1a52f686 to your computer and use it in GitHub Desktop.
Save janispritzkau/ba9571ff973aae98a4ad972f1a52f686 to your computer and use it in GitHub Desktop.
Deno EJS Template Engine
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 "&amp;";
if (c == "<") return "&lt;";
if (c == ">") return "&gt;";
if (c == '"') return "&quot;";
return "&#39;";
});
}
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