ventojs 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/esm/_dnt.shims.d.ts +5 -0
- package/esm/_dnt.shims.js +62 -0
- package/esm/_dnt.test_polyfills.d.ts +11 -0
- package/esm/_dnt.test_shims.d.ts +5 -0
- package/esm/deps/deno.land/std@0.178.0/_util/asserts.d.ts +10 -0
- package/esm/deps/deno.land/std@0.178.0/_util/asserts.js +21 -0
- package/esm/deps/deno.land/std@0.178.0/_util/os.d.ts +4 -0
- package/esm/deps/deno.land/std@0.178.0/_util/os.js +18 -0
- package/esm/deps/deno.land/std@0.178.0/path/_constants.d.ts +39 -0
- package/esm/deps/deno.land/std@0.178.0/path/_constants.js +46 -0
- package/esm/deps/deno.land/std@0.178.0/path/_interface.d.ts +26 -0
- package/esm/deps/deno.land/std@0.178.0/path/_interface.js +3 -0
- package/esm/deps/deno.land/std@0.178.0/path/_util.d.ts +11 -0
- package/esm/deps/deno.land/std@0.178.0/path/_util.js +161 -0
- package/esm/deps/deno.land/std@0.178.0/path/common.d.ts +13 -0
- package/esm/deps/deno.land/std@0.178.0/path/common.js +36 -0
- package/esm/deps/deno.land/std@0.178.0/path/glob.d.ts +83 -0
- package/esm/deps/deno.land/std@0.178.0/path/glob.js +361 -0
- package/esm/deps/deno.land/std@0.178.0/path/mod.d.ts +9 -0
- package/esm/deps/deno.land/std@0.178.0/path/mod.js +32 -0
- package/esm/deps/deno.land/std@0.178.0/path/posix.d.ts +86 -0
- package/esm/deps/deno.land/std@0.178.0/path/posix.js +442 -0
- package/esm/deps/deno.land/std@0.178.0/path/separator.d.ts +2 -0
- package/esm/deps/deno.land/std@0.178.0/path/separator.js +5 -0
- package/esm/deps/deno.land/std@0.178.0/path/win32.d.ts +91 -0
- package/esm/deps/deno.land/std@0.178.0/path/win32.js +909 -0
- package/esm/deps/deno.land/std@0.190.0/_util/asserts.d.ts +10 -0
- package/esm/deps/deno.land/std@0.190.0/bytes/copy.d.ts +27 -0
- package/esm/deps/deno.land/std@0.190.0/fmt/colors.d.ts +270 -0
- package/esm/deps/deno.land/std@0.190.0/front_matter/mod.d.ts +78 -0
- package/esm/deps/deno.land/std@0.190.0/front_matter/yaml.d.ts +4 -0
- package/esm/deps/deno.land/std@0.190.0/io/buffer.d.ts +81 -0
- package/esm/deps/deno.land/std@0.190.0/testing/_diff.d.ts +26 -0
- package/esm/deps/deno.land/std@0.190.0/testing/_format.d.ts +1 -0
- package/esm/deps/deno.land/std@0.190.0/testing/asserts.d.ts +284 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_error.d.ts +6 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_loader/loader.d.ts +4 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_loader/loader_state.d.ts +43 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_mark.d.ts +10 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_state.d.ts +5 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/binary.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/bool.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/float.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/function.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/int.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/map.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/merge.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/mod.d.ts +16 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/nil.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/omap.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/pairs.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/regexp.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/seq.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/set.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/str.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/timestamp.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_type/undefined.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/_utils.d.ts +19 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/parse.d.ts +35 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/schema/core.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/schema/default.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/schema/extended.d.ts +30 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/schema/failsafe.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/schema/json.d.ts +2 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/schema/mod.d.ts +5 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/schema.d.ts +22 -0
- package/esm/deps/deno.land/std@0.190.0/yaml/type.d.ts +28 -0
- package/esm/deps.d.ts +1 -0
- package/esm/deps.js +1 -0
- package/esm/mod.d.ts +7 -0
- package/esm/mod.js +28 -0
- package/esm/package.json +3 -0
- package/esm/plugins/escape.d.ts +2 -0
- package/esm/plugins/escape.js +16 -0
- package/esm/plugins/for.d.ts +2 -0
- package/esm/plugins/for.js +48 -0
- package/esm/plugins/if.d.ts +2 -0
- package/esm/plugins/if.js +32 -0
- package/esm/plugins/include.d.ts +2 -0
- package/esm/plugins/include.js +16 -0
- package/esm/plugins/js.d.ts +2 -0
- package/esm/plugins/js.js +11 -0
- package/esm/plugins/layout.d.ts +2 -0
- package/esm/plugins/layout.js +29 -0
- package/esm/plugins/set.d.ts +2 -0
- package/esm/plugins/set.js +44 -0
- package/esm/src/environment.d.ts +29 -0
- package/esm/src/environment.js +136 -0
- package/esm/src/loader.d.ts +14 -0
- package/esm/src/loader.js +19 -0
- package/esm/src/tokenizer.d.ts +8 -0
- package/esm/src/tokenizer.js +197 -0
- package/esm/test/comment.test.d.ts +1 -0
- package/esm/test/escape.test.d.ts +1 -0
- package/esm/test/for.test.d.ts +1 -0
- package/esm/test/if.test.d.ts +1 -0
- package/esm/test/include.test.d.ts +1 -0
- package/esm/test/js.test.d.ts +1 -0
- package/esm/test/layout.test.d.ts +1 -0
- package/esm/test/print.test.d.ts +1 -0
- package/esm/test/raw.test.d.ts +1 -0
- package/esm/test/set.test.d.ts +1 -0
- package/esm/test/tokenizer.test.d.ts +1 -0
- package/esm/test/utils.d.ts +23 -0
- package/package.json +25 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default function () {
|
|
2
|
+
return (env) => {
|
|
3
|
+
env.filters.escape = escape;
|
|
4
|
+
};
|
|
5
|
+
}
|
|
6
|
+
const escapeMap = {
|
|
7
|
+
"&": "&",
|
|
8
|
+
"<": "<",
|
|
9
|
+
">": ">",
|
|
10
|
+
'"': """,
|
|
11
|
+
"'": "'",
|
|
12
|
+
"`": "`",
|
|
13
|
+
};
|
|
14
|
+
function escape(str) {
|
|
15
|
+
return str.replace(/[&<>"'`]/g, (match) => escapeMap[match]);
|
|
16
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export default function () {
|
|
2
|
+
return (env) => {
|
|
3
|
+
env.tags.push(forTag);
|
|
4
|
+
env.utils.toIterator = toIterator;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
function forTag(env, code, output, tokens) {
|
|
8
|
+
if (!code.startsWith("for ")) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const compiled = [];
|
|
12
|
+
const match = code.match(/^for\s+(await\s+)?(\w+)(?:,\s*(\w+))?\s+of\s+([\s|\S]+)$/);
|
|
13
|
+
if (!match) {
|
|
14
|
+
throw new Error(`Invalid for loop: ${code}`);
|
|
15
|
+
}
|
|
16
|
+
const [_, aw, var1, var2, collection] = match;
|
|
17
|
+
if (var2) {
|
|
18
|
+
compiled.push(`for ${aw || ""}(let [${var1}, ${var2}] of __env.utils.toIterator(${env.compileFilters(tokens, collection)}, true)) {`);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
compiled.push(`for ${aw || ""}(let ${var1} of __env.utils.toIterator(${env.compileFilters(tokens, collection)})) {`);
|
|
22
|
+
}
|
|
23
|
+
compiled.push(...env.compileTokens(tokens, output, ["/for"]));
|
|
24
|
+
tokens.shift();
|
|
25
|
+
compiled.push("}");
|
|
26
|
+
return compiled.join("\n");
|
|
27
|
+
}
|
|
28
|
+
function toIterator(item, withKeys = false) {
|
|
29
|
+
if (item === undefined || item === null) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(item)) {
|
|
33
|
+
return withKeys ? Object.entries(item) : item;
|
|
34
|
+
}
|
|
35
|
+
if (typeof item === "function") {
|
|
36
|
+
return toIterator(item(), withKeys);
|
|
37
|
+
}
|
|
38
|
+
if (typeof item === "object" && item !== null) {
|
|
39
|
+
return withKeys ? Object.entries(item) : Object.values(item);
|
|
40
|
+
}
|
|
41
|
+
if (typeof item === "string") {
|
|
42
|
+
return toIterator(item.split(""), withKeys);
|
|
43
|
+
}
|
|
44
|
+
if (typeof item === "number") {
|
|
45
|
+
return toIterator(new Array(item).fill(0).map((_, i) => i + 1), withKeys);
|
|
46
|
+
}
|
|
47
|
+
return toIterator([item], withKeys);
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export default function () {
|
|
2
|
+
return (env) => {
|
|
3
|
+
env.tags.push(ifTag);
|
|
4
|
+
env.tags.push(elseTag);
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
function ifTag(env, code, output, tokens) {
|
|
8
|
+
if (!code.startsWith("if ")) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const condition = code.replace(/^if\s+/, "").trim();
|
|
12
|
+
const compiled = [];
|
|
13
|
+
compiled.push(`if (${condition}) {`);
|
|
14
|
+
compiled.push(...env.compileTokens(tokens, output, ["/if"]));
|
|
15
|
+
tokens.shift();
|
|
16
|
+
compiled.push("}");
|
|
17
|
+
return compiled.join("\n");
|
|
18
|
+
}
|
|
19
|
+
function elseTag(_env, code) {
|
|
20
|
+
if (!code.startsWith("else ") && code !== "else") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const match = code.match(/^else(\s+if\s+(.*))?$/);
|
|
24
|
+
if (!match) {
|
|
25
|
+
throw new Error(`Invalid else: ${code}`);
|
|
26
|
+
}
|
|
27
|
+
const [_, ifTag, condition] = match;
|
|
28
|
+
if (ifTag) {
|
|
29
|
+
return `} else if (${condition}) {`;
|
|
30
|
+
}
|
|
31
|
+
return "} else {";
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default function () {
|
|
2
|
+
return (env) => {
|
|
3
|
+
env.tags.push(includeTag);
|
|
4
|
+
};
|
|
5
|
+
}
|
|
6
|
+
function includeTag(_env, code, output) {
|
|
7
|
+
if (!code.startsWith("include ")) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const match = code?.match(/^include\s+([^{]+|`[^`]+`)+(?:\{([\s|\S]*)\})?$/);
|
|
11
|
+
if (!match) {
|
|
12
|
+
throw new Error(`Invalid include: ${code}`);
|
|
13
|
+
}
|
|
14
|
+
const [_, file, data] = match;
|
|
15
|
+
return `${output} += await __env.run(${file}, {...__data${data ? `, ${data}` : ""}}, __file);`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export default function () {
|
|
2
|
+
return (env) => {
|
|
3
|
+
env.tags.push(layoutTag);
|
|
4
|
+
};
|
|
5
|
+
}
|
|
6
|
+
function layoutTag(env, code, output, tokens) {
|
|
7
|
+
if (!code.startsWith("layout ")) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const match = code?.match(/^layout\s+([^{]+|`[^`]+`)+(?:\{([\s|\S]*)\})?$/);
|
|
11
|
+
if (!match) {
|
|
12
|
+
throw new Error(`Invalid wrap: ${code}`);
|
|
13
|
+
}
|
|
14
|
+
const [_, file, data] = match;
|
|
15
|
+
const varname = "__content";
|
|
16
|
+
const compiled = [];
|
|
17
|
+
const compiledFilters = env.compileFilters(tokens, varname);
|
|
18
|
+
compiled.push("{");
|
|
19
|
+
compiled.push(`var ${varname} = "";`);
|
|
20
|
+
compiled.push(...env.compileTokens(tokens, varname, ["/layout"]));
|
|
21
|
+
if (tokens.length && (tokens[0][0] !== "tag" || tokens[0][1] !== "/layout")) {
|
|
22
|
+
throw new Error(`Missing closing tag for layout tag: ${code}`);
|
|
23
|
+
}
|
|
24
|
+
tokens.shift();
|
|
25
|
+
compiled.push(`${varname} = ${compiledFilters};`);
|
|
26
|
+
compiled.push(`${output} += await __env.run(${file}, {...__data${data ? `, ${data}` : ""}, content: ${varname}}, __file);`);
|
|
27
|
+
compiled.push("}");
|
|
28
|
+
return compiled.join("\n");
|
|
29
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export default function () {
|
|
2
|
+
return (env) => {
|
|
3
|
+
env.tags.push(setTag);
|
|
4
|
+
};
|
|
5
|
+
}
|
|
6
|
+
function setTag(env, code, _output, tokens) {
|
|
7
|
+
if (!code.startsWith("set ")) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const expression = code.replace(/^set\s+/, "");
|
|
11
|
+
// Value is set (e.g. {{ set foo = "bar" }})
|
|
12
|
+
if (expression.includes("=")) {
|
|
13
|
+
const match = code.match(/^set\s+([\w]+)\s*=\s*([\s\S]+)$/);
|
|
14
|
+
if (!match) {
|
|
15
|
+
throw new Error(`Invalid set tag: ${code}`);
|
|
16
|
+
}
|
|
17
|
+
const [, variable, value] = match;
|
|
18
|
+
const val = env.compileFilters(tokens, value);
|
|
19
|
+
return `if (__data.hasOwnProperty("${variable}")) {
|
|
20
|
+
${variable} = ${val};
|
|
21
|
+
} else {
|
|
22
|
+
var ${variable} = ${val};
|
|
23
|
+
}
|
|
24
|
+
__data["${variable}"] = ${variable};
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
// Value is captured (eg: {{ set foo }}bar{{ /set }})
|
|
28
|
+
const compiled = [];
|
|
29
|
+
const compiledFilters = env.compileFilters(tokens, expression);
|
|
30
|
+
compiled.push(`if (__data.hasOwnProperty("${expression}")) {
|
|
31
|
+
${expression} = "";
|
|
32
|
+
} else {
|
|
33
|
+
var ${expression} = "";
|
|
34
|
+
}
|
|
35
|
+
`);
|
|
36
|
+
compiled.push(...env.compileTokens(tokens, expression, ["/set"]));
|
|
37
|
+
if (tokens.length && (tokens[0][0] !== "tag" || tokens[0][1] !== "/set")) {
|
|
38
|
+
throw new Error(`Missing closing tag for set tag: ${code}`);
|
|
39
|
+
}
|
|
40
|
+
tokens.shift();
|
|
41
|
+
compiled.push(`${expression} = ${compiledFilters};`);
|
|
42
|
+
compiled.push(`__data["${expression.trim()}"] = ${expression};`);
|
|
43
|
+
return compiled.join("\n");
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Token } from "./tokenizer.js";
|
|
2
|
+
import type { Loader } from "./loader.js";
|
|
3
|
+
export interface Template {
|
|
4
|
+
(data?: Record<string, unknown>): Promise<string>;
|
|
5
|
+
code: string;
|
|
6
|
+
file?: string;
|
|
7
|
+
}
|
|
8
|
+
export type Tag = (env: Environment, code: string, output: string, tokens: Token[]) => string | undefined;
|
|
9
|
+
export type Filter = (...args: any[]) => any;
|
|
10
|
+
export type Plugin = (env: Environment) => void;
|
|
11
|
+
export interface Options {
|
|
12
|
+
loader: Loader;
|
|
13
|
+
dataVarname?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class Environment {
|
|
16
|
+
cache: Map<string, Template>;
|
|
17
|
+
options: Options;
|
|
18
|
+
tags: Tag[];
|
|
19
|
+
filters: Record<string, Filter>;
|
|
20
|
+
utils: Record<string, unknown>;
|
|
21
|
+
constructor(options: Options);
|
|
22
|
+
use(plugin: Plugin): void;
|
|
23
|
+
run(file: string, data: Record<string, unknown>, from?: string): Promise<string>;
|
|
24
|
+
runString(source: string, data?: Record<string, unknown>, file?: string): Promise<string>;
|
|
25
|
+
compile(source: string, path?: string, defaults?: Record<string, unknown>): Template;
|
|
26
|
+
load(file: string, from?: string): Promise<Template>;
|
|
27
|
+
compileTokens(tokens: Token[], outputVar?: string, stopAt?: string[]): string[];
|
|
28
|
+
compileFilters(tokens: Token[], output: string): string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
2
|
+
import tokenize from "./tokenizer.js";
|
|
3
|
+
export class Environment {
|
|
4
|
+
cache = new Map();
|
|
5
|
+
options;
|
|
6
|
+
tags = [];
|
|
7
|
+
filters = {};
|
|
8
|
+
utils = {};
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
use(plugin) {
|
|
13
|
+
plugin(this);
|
|
14
|
+
}
|
|
15
|
+
async run(file, data, from) {
|
|
16
|
+
const template = await this.load(file, from);
|
|
17
|
+
return await template(data);
|
|
18
|
+
}
|
|
19
|
+
async runString(source, data, file) {
|
|
20
|
+
if (file) {
|
|
21
|
+
const cached = this.cache.get(file);
|
|
22
|
+
if (cached) {
|
|
23
|
+
return await cached(data);
|
|
24
|
+
}
|
|
25
|
+
const template = this.compile(source, file);
|
|
26
|
+
this.cache.set(file, template);
|
|
27
|
+
return await template(data);
|
|
28
|
+
}
|
|
29
|
+
const template = this.compile(source, file);
|
|
30
|
+
return await template(data);
|
|
31
|
+
}
|
|
32
|
+
compile(source, path, defaults) {
|
|
33
|
+
try {
|
|
34
|
+
const tokens = tokenize(source);
|
|
35
|
+
const code = this.compileTokens(tokens).join("\n");
|
|
36
|
+
const constructor = new Function("__file", "__env", "__defaults", `return async function (__data) {
|
|
37
|
+
try {
|
|
38
|
+
__data = Object.assign({}, __defaults, __data);
|
|
39
|
+
const ${this.options.dataVarname} = __data;
|
|
40
|
+
let __output = "";
|
|
41
|
+
with (__data) {
|
|
42
|
+
${code}
|
|
43
|
+
}
|
|
44
|
+
return __output;
|
|
45
|
+
} catch (cause) {
|
|
46
|
+
throw new Error(\`Error rendering template: \${__file}\`, { cause });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`);
|
|
50
|
+
// console.log(code);
|
|
51
|
+
const template = constructor(path, this, defaults);
|
|
52
|
+
template.file = path;
|
|
53
|
+
template.code = code;
|
|
54
|
+
return template;
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
throw new Error(`Error compiling template: ${path || source}`, { cause });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async load(file, from) {
|
|
61
|
+
const path = from ? this.options.loader.resolve(from, file) : file;
|
|
62
|
+
if (!this.cache.has(path)) {
|
|
63
|
+
const { source, data } = await this.options.loader.load(path);
|
|
64
|
+
const template = this.compile(source, path, data);
|
|
65
|
+
this.cache.set(file, template);
|
|
66
|
+
}
|
|
67
|
+
return this.cache.get(file);
|
|
68
|
+
}
|
|
69
|
+
compileTokens(tokens, outputVar = "__output", stopAt) {
|
|
70
|
+
const compiled = [];
|
|
71
|
+
tokens: while (tokens.length > 0) {
|
|
72
|
+
if (stopAt && tokens[0][0] === "tag" && stopAt.includes(tokens[0][1])) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const [type, code] = tokens.shift();
|
|
76
|
+
if (type === "comment") {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (type === "string" || type === "raw") {
|
|
80
|
+
compiled.push(`${outputVar} += \`${code}\`;`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (type === "tag") {
|
|
84
|
+
for (const tag of this.tags) {
|
|
85
|
+
const compiledTag = tag(this, code, outputVar, tokens);
|
|
86
|
+
if (typeof compiledTag === "string") {
|
|
87
|
+
compiled.push(compiledTag);
|
|
88
|
+
continue tokens;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Unknown tag, just print it
|
|
92
|
+
const expression = this.compileFilters(tokens, code);
|
|
93
|
+
compiled.push(`${outputVar} += (${expression}) ?? "";`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Unknown token type "${type}"`);
|
|
97
|
+
}
|
|
98
|
+
return compiled;
|
|
99
|
+
}
|
|
100
|
+
compileFilters(tokens, output) {
|
|
101
|
+
while (tokens.length > 0 && tokens[0][0] === "filter") {
|
|
102
|
+
const [, code] = tokens.shift();
|
|
103
|
+
const match = code.match(/^(await\s+)?([\w.]+)(?:\((.*)\))?$/);
|
|
104
|
+
if (!match) {
|
|
105
|
+
throw new Error(`Invalid filter: ${code}`);
|
|
106
|
+
}
|
|
107
|
+
const [_, isAsync, name, args] = match;
|
|
108
|
+
if (!this.filters[name]) {
|
|
109
|
+
// If a global function
|
|
110
|
+
if (isGlobal(name)) {
|
|
111
|
+
output = `${isAsync ? "await " : ""}${name}(${output}${args ? `, ${args}` : ""})`;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// It's a prototype's method (e.g. `String.toUpperCase()`)
|
|
115
|
+
output = `${isAsync ? "await " : ""}(${output})?.${name}?.(${args ? args : ""})`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// It's a filter (e.g. filters.upper())
|
|
120
|
+
output = `${isAsync ? "await " : ""}__env.filters.${name}(${output}${args ? `, ${args}` : ""})`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return output;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function isGlobal(name) {
|
|
127
|
+
// @ts-ignore TS doesn't know about globalThis
|
|
128
|
+
if (dntShim.dntGlobalThis[name]) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (name.includes(".")) {
|
|
132
|
+
const [obj, prop] = name.split(".");
|
|
133
|
+
// @ts-ignore TS doesn't know about globalThis
|
|
134
|
+
return typeof dntShim.dntGlobalThis[obj]?.[prop] === "function";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TemplateSource {
|
|
2
|
+
source: string;
|
|
3
|
+
data?: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export interface Loader {
|
|
6
|
+
load(file: string): TemplateSource | Promise<TemplateSource>;
|
|
7
|
+
resolve(from: string, file: string): string;
|
|
8
|
+
}
|
|
9
|
+
export declare class FileLoader implements Loader {
|
|
10
|
+
#private;
|
|
11
|
+
constructor(root: string);
|
|
12
|
+
load(file: string): Promise<TemplateSource>;
|
|
13
|
+
resolve(from: string, file: string): string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
2
|
+
import { path } from "../deps.js";
|
|
3
|
+
export class FileLoader {
|
|
4
|
+
#root;
|
|
5
|
+
constructor(root) {
|
|
6
|
+
this.#root = root;
|
|
7
|
+
}
|
|
8
|
+
async load(file) {
|
|
9
|
+
return {
|
|
10
|
+
source: await dntShim.Deno.readTextFile(file),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
resolve(from, file) {
|
|
14
|
+
if (file.startsWith(".")) {
|
|
15
|
+
return path.join(path.dirname(from), file);
|
|
16
|
+
}
|
|
17
|
+
return path.join(this.#root, file);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type TokenType = "string" | "tag" | "filter" | "comment" | "raw";
|
|
2
|
+
export type Token = [TokenType, string];
|
|
3
|
+
export default function tokenize(source: string): Token[];
|
|
4
|
+
/**
|
|
5
|
+
* Parse a tag and return the indexes of the start and end brackets, and the filters between.
|
|
6
|
+
* For example: {{ tag |> filter1 |> filter2 }} => [2, 9, 20, 31]
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseTag(source: string): number[];
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
export default function tokenize(source) {
|
|
2
|
+
const tokens = [];
|
|
3
|
+
let type = "string";
|
|
4
|
+
let trimNext = false;
|
|
5
|
+
while (source.length > 0) {
|
|
6
|
+
if (type === "string") {
|
|
7
|
+
const index = source.indexOf("{{");
|
|
8
|
+
const code = index === -1 ? source : source.slice(0, index);
|
|
9
|
+
if (trimNext) {
|
|
10
|
+
tokens.push([type, code.trimStart()]);
|
|
11
|
+
trimNext = false;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
tokens.push([type, code]);
|
|
15
|
+
}
|
|
16
|
+
if (index === -1) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
source = source.slice(index);
|
|
20
|
+
// Check if it's a {{raw}} tag
|
|
21
|
+
const raw = parseRawTag(source);
|
|
22
|
+
if (raw) {
|
|
23
|
+
const rawCode = source.slice(raw[0], raw[1]);
|
|
24
|
+
tokens.push(["raw", rawCode]);
|
|
25
|
+
source = source.slice(raw[2]);
|
|
26
|
+
type = "string";
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
type = source.startsWith("{{#") ? "comment" : "tag";
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (type === "comment") {
|
|
33
|
+
source = source.slice(3);
|
|
34
|
+
const index = source.indexOf("#}}");
|
|
35
|
+
const comment = index === -1 ? source : source.slice(0, index);
|
|
36
|
+
tokens.push([type, comment]);
|
|
37
|
+
if (index === -1) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
source = source.slice(index + 3);
|
|
41
|
+
type = "string";
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (type === "tag") {
|
|
45
|
+
const indexes = parseTag(source);
|
|
46
|
+
const lastIndex = indexes.length - 1;
|
|
47
|
+
indexes.reduce((prev, curr, index) => {
|
|
48
|
+
let code = source.slice(prev, curr - 2);
|
|
49
|
+
// Tag
|
|
50
|
+
if (index === 1) {
|
|
51
|
+
// Left trim
|
|
52
|
+
if (code.startsWith("-")) {
|
|
53
|
+
code = code.slice(1);
|
|
54
|
+
const lastToken = tokens[tokens.length - 1];
|
|
55
|
+
lastToken[1] = lastToken[1].trimEnd();
|
|
56
|
+
}
|
|
57
|
+
// Right trim
|
|
58
|
+
if (code.endsWith("-") && index === lastIndex) {
|
|
59
|
+
code = code.slice(0, -1);
|
|
60
|
+
trimNext = true;
|
|
61
|
+
}
|
|
62
|
+
tokens.push([type, code.trim()]);
|
|
63
|
+
return curr;
|
|
64
|
+
}
|
|
65
|
+
// Right trim
|
|
66
|
+
if (index === lastIndex && code.endsWith("-")) {
|
|
67
|
+
code = code.slice(0, -1);
|
|
68
|
+
trimNext = true;
|
|
69
|
+
}
|
|
70
|
+
// Filters
|
|
71
|
+
tokens.push(["filter", code.trim()]);
|
|
72
|
+
return curr;
|
|
73
|
+
});
|
|
74
|
+
source = source.slice(indexes[indexes.length - 1]);
|
|
75
|
+
type = "string";
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return tokens;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse a tag and return the indexes of the start and end brackets, and the filters between.
|
|
83
|
+
* For example: {{ tag |> filter1 |> filter2 }} => [2, 9, 20, 31]
|
|
84
|
+
*/
|
|
85
|
+
export function parseTag(source) {
|
|
86
|
+
const length = source.length;
|
|
87
|
+
const statuses = [];
|
|
88
|
+
const indexes = [2];
|
|
89
|
+
let index = 0;
|
|
90
|
+
while (index < length) {
|
|
91
|
+
const char = source.charAt(index++);
|
|
92
|
+
switch (char) {
|
|
93
|
+
// Detect start brackets
|
|
94
|
+
case "{": {
|
|
95
|
+
const status = statuses[0];
|
|
96
|
+
if (status === "literal" && source.charAt(index - 2) === "$") {
|
|
97
|
+
statuses.unshift("bracket");
|
|
98
|
+
}
|
|
99
|
+
else if (status !== "comment" && status !== "single-quote" &&
|
|
100
|
+
status !== "double-quote" && status !== "literal") {
|
|
101
|
+
statuses.unshift("bracket");
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
// Detect end brackets
|
|
106
|
+
case "}": {
|
|
107
|
+
const status = statuses[0];
|
|
108
|
+
if (status === "bracket") {
|
|
109
|
+
statuses.shift();
|
|
110
|
+
if (statuses.length === 0) {
|
|
111
|
+
indexes.push(index);
|
|
112
|
+
return indexes;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
// Detect double quotes
|
|
118
|
+
case '"': {
|
|
119
|
+
const status = statuses[0];
|
|
120
|
+
if (status === "double-quote") {
|
|
121
|
+
statuses.shift();
|
|
122
|
+
}
|
|
123
|
+
else if (status !== "comment" &&
|
|
124
|
+
status !== "single-quote" &&
|
|
125
|
+
status !== "literal") {
|
|
126
|
+
statuses.unshift("double-quote");
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
// Detect single quotes
|
|
131
|
+
case "'": {
|
|
132
|
+
const status = statuses[0];
|
|
133
|
+
if (status === "single-quote") {
|
|
134
|
+
statuses.shift();
|
|
135
|
+
}
|
|
136
|
+
else if (status !== "comment" &&
|
|
137
|
+
status !== "double-quote" &&
|
|
138
|
+
status !== "literal") {
|
|
139
|
+
statuses.unshift("single-quote");
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
// Detect literals
|
|
144
|
+
case "`": {
|
|
145
|
+
const status = statuses[0];
|
|
146
|
+
if (status === "literal") {
|
|
147
|
+
statuses.shift();
|
|
148
|
+
}
|
|
149
|
+
else if (status !== "comment" &&
|
|
150
|
+
status !== "double-quote" &&
|
|
151
|
+
status !== "single-quote") {
|
|
152
|
+
statuses.unshift("literal");
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
// Detect comments
|
|
157
|
+
case "/": {
|
|
158
|
+
const status = statuses[0];
|
|
159
|
+
if (status !== "single-quote" && status !== "double-quote" &&
|
|
160
|
+
status !== "literal") {
|
|
161
|
+
if (source.charAt(index) === "*") {
|
|
162
|
+
statuses.unshift("comment");
|
|
163
|
+
}
|
|
164
|
+
else if (status === "comment" &&
|
|
165
|
+
source.charAt(index - 2) === "*") {
|
|
166
|
+
statuses.shift();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
// Detect filters
|
|
172
|
+
case "|": {
|
|
173
|
+
const status = statuses[0];
|
|
174
|
+
if (status === "bracket" && source.charAt(index) === ">") {
|
|
175
|
+
indexes.push(index + 1);
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
throw new Error("Unclosed tag");
|
|
182
|
+
}
|
|
183
|
+
function parseRawTag(source) {
|
|
184
|
+
const startResult = source.match(/^{{\s*raw\s*}}/);
|
|
185
|
+
if (!startResult) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const endResult = source.match(/{{\s*\/raw\s*}}/);
|
|
189
|
+
if (!endResult) {
|
|
190
|
+
throw new Error("Unclosed raw tag");
|
|
191
|
+
}
|
|
192
|
+
return [
|
|
193
|
+
startResult[0].length,
|
|
194
|
+
endResult.index,
|
|
195
|
+
endResult.index + endResult[0].length,
|
|
196
|
+
];
|
|
197
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../_dnt.test_polyfills.js";
|