-
Notifications
You must be signed in to change notification settings - Fork 0
/
mod.ts
129 lines (120 loc) · 3.9 KB
/
mod.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
* Type alias for data objects where keys are strings and values can be of any type.
*/
type Data = Record<string, unknown>;
/**
* Interface for constructor options, allowing customization of template delimiters and escape behavior.
*/
export interface ConstructorOptions {
/**
* The opening delimiter for template tags. Defaults to "{{".
*/
open?: string;
/**
* The closing delimiter for template tags. Defaults to "}}".
*/
close?: string;
/**
* Whether to escape output for HTML entities. Defaults to true.
*/
isEscape?: boolean;
}
/**
* Interface for the compiled function, which takes a data object and an escape function, and returns a rendered string.
*/
export interface CompiledFunction {
(data: Data, escape: (str: string | object) => string): string;
}
/**
* Class for parsing and rendering template strings.
*/
export default class Template {
private open: string;
private close: string;
private cache: Map<string, CompiledFunction> = new Map();
private decoder = new TextDecoder();
private isEscape: boolean;
private reg: RegExp;
/**
* Creates a new Template instance.
* @param options - Optional constructor options.
*/
constructor(options?: ConstructorOptions) {
this.open = options?.open ?? "{{";
this.close = options?.close ?? "}}";
this.isEscape = options?.isEscape ?? true;
this.reg = new RegExp(`${this.open}([\\s\\S]+?)${this.close}`, "g");
}
/**
* Renders a given template string.
* @param str - The template string to render.
* @param data - The data object used to populate the template.
* @returns The rendered string.
*/
render(str: string, data: Data): string {
return str.replace(this.reg, (match, key: string): string => {
let value: unknown = data;
key.replace(/ /g, "").split(".").forEach((k) => {
value = (value as Record<string, unknown>)[k];
});
if (value === undefined) return match;
return this.escape(value as string | Data);
});
}
/**
* Compiles a template string into an executable function.
* @param str - The template string to compile.
* @returns The compiled function.
*/
compile(str: string): CompiledFunction {
const result = str.replace(/\n/g, "\\n")
.replace(this.reg, (match, key: string): string => {
key = key.trim();
return `' + (obj.${key} ? escape(obj.${key}) : '${match}') + '`;
});
const tpl = `let tpl = '${result}'\n return tpl;`;
return new Function("obj", "escape", tpl) as CompiledFunction;
}
/**
* Renders data using a compiled function.
* @param compiled - The compiled function.
* @param data - The data object used to populate the template.
* @returns The rendered string.
*/
renderCompiled(compiled: CompiledFunction, data: Data): string {
return compiled(data, this.escape.bind(this));
}
/**
* Reads a template from a file and renders it.
* @param path - The path to the template file.
* @param data - The data object used to populate the template.
* @returns The rendered string.
*/
async renderFile(path: string, data: Data): Promise<string> {
if (this.cache.has(path)) {
return this.renderCompiled(this.cache.get(path)!, data);
}
const buf = await Deno.readFile(path);
const str = this.decoder.decode(buf);
const compiled = this.compile(str);
this.cache.set(path, compiled);
return compiled(data, this.escape.bind(this));
}
/**
* Escapes a string for HTML entities.
* @param str - The string or object to escape.
* @returns The escaped string.
*/
private escape(str: string | object): string {
if (typeof str === "object") {
str = JSON.stringify(str);
}
str = String(str);
if (this.isEscape === false) return str;
return str.replace(/&(?!\w+;)/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
}