ventojs 0.8.1 → 0.9.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/README.md +7 -63
- package/esm/deps/deno.land/std@0.201.0/html/entities.d.ts +38 -0
- package/esm/deps/deno.land/std@0.201.0/html/entities.js +76 -0
- package/esm/deps/deno.land/std@0.201.0/html/mod.d.ts +6 -0
- package/esm/deps/deno.land/std@0.201.0/html/mod.js +8 -0
- package/esm/deps.d.ts +1 -0
- package/esm/deps.js +1 -0
- package/esm/mod.d.ts +1 -0
- package/esm/mod.js +1 -0
- package/esm/plugins/escape.js +2 -12
- package/esm/plugins/export.js +5 -4
- package/esm/plugins/import.js +4 -11
- package/esm/plugins/include.js +3 -2
- package/esm/plugins/layout.js +4 -3
- package/esm/plugins/set.js +5 -4
- package/esm/plugins/unescape.js +2 -12
- package/esm/src/environment.d.ts +1 -0
- package/esm/src/environment.js +9 -12
- package/package.json +1 -1
- package/script/deps/deno.land/std@0.201.0/html/entities.d.ts +38 -0
- package/script/deps/deno.land/std@0.201.0/html/entities.js +81 -0
- package/script/deps/deno.land/std@0.201.0/html/mod.d.ts +6 -0
- package/script/deps/deno.land/std@0.201.0/html/mod.js +24 -0
- package/script/deps.d.ts +1 -0
- package/script/deps.js +2 -1
- package/script/mod.d.ts +1 -0
- package/script/mod.js +1 -0
- package/script/plugins/escape.js +2 -12
- package/script/plugins/export.js +5 -4
- package/script/plugins/import.js +4 -11
- package/script/plugins/include.js +3 -2
- package/script/plugins/layout.js +4 -3
- package/script/plugins/set.js +5 -4
- package/script/plugins/unescape.js +2 -12
- package/script/src/environment.d.ts +1 -0
- package/script/src/environment.js +9 -12
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Vento
|
|
2
2
|
|
|
3
3
|
This is a minimal template engine inspired by other great engines like Nunjucks,
|
|
4
|
-
Liquid, Mustache
|
|
4
|
+
Liquid, Mustache, and EJS.
|
|
5
5
|
|
|
6
6
|
## Why another template engine?
|
|
7
7
|
|
|
@@ -32,6 +32,7 @@ neither is). The issues I found in existing template engines:
|
|
|
32
32
|
### Liquid
|
|
33
33
|
|
|
34
34
|
- I like:
|
|
35
|
+
|
|
35
36
|
- The support for async evaluation is less hacky than Nunjucks.
|
|
36
37
|
- The variables are not escaped by default, there's an `escape` filter for
|
|
37
38
|
that.
|
|
@@ -91,66 +92,9 @@ First, let's take a look at this syntax example:
|
|
|
91
92
|
|
|
92
93
|
## Getting started
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
future.~~
|
|
96
|
-
[There's already an NPM version](https://www.npmjs.com/package/ventojs) that you
|
|
97
|
-
can install with `npm install ventojs`.
|
|
98
|
-
|
|
99
|
-
Import the library and create an instance:
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
import vento from "https://deno.land/x/vento/mod.ts";
|
|
103
|
-
|
|
104
|
-
const vto = vento({
|
|
105
|
-
// Resolve the non-relative includes paths
|
|
106
|
-
includes: "./path/to/includes",
|
|
107
|
-
});
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Or in Node:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
import vento from "ventojs";
|
|
114
|
-
|
|
115
|
-
const vto = vento({
|
|
116
|
-
// Resolve the non-relative includes paths
|
|
117
|
-
includes: "./path/to/includes",
|
|
118
|
-
});
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
There are different ways to load, compile and run a template. For example, you
|
|
122
|
-
can use `load` to load and compile a template file and return it.
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
// Load and return a template
|
|
126
|
-
const template = await vto.load("my-template.vto");
|
|
127
|
-
|
|
128
|
-
// Now you can use it passing the data
|
|
129
|
-
const result = await template({ title: "Hello world" });
|
|
130
|
-
console.log(result.content);
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Alternatively, you can load and run the template file in a single call:
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
const result = await vto.run("my-template.vto", { title: "Hello world" });
|
|
137
|
-
console.log(result.content);
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
If the template code is not a file, you can run it directly:
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
const result = await vto.runString("<h1>{{ title }}</h1>", {
|
|
144
|
-
title: "Hello world",
|
|
145
|
-
});
|
|
146
|
-
console.log(result.content);
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## Visual Studio Code Support
|
|
150
|
-
|
|
151
|
-
[The Vento extension for VS Code](https://marketplace.visualstudio.com/items?itemName=oscarotero.vento-syntax)
|
|
152
|
-
enables syntax highlight and provides some useful snippets.
|
|
95
|
+
See [Getting started](https://vento.js.org/getting-started/) on the docs.
|
|
153
96
|
|
|
154
|
-
##
|
|
97
|
+
## Editor support
|
|
155
98
|
|
|
156
|
-
[
|
|
99
|
+
See [Editor integrations](https://vento.js.org/editor-integrations/) on the
|
|
100
|
+
docs.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type EntityList = Record<string, string>;
|
|
2
|
+
/**
|
|
3
|
+
* Escapes text for safe interpolation into HTML text content and quoted attributes
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { escape } from "https://deno.land/std@$STD_VERSION/html/entities.ts";
|
|
8
|
+
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/assert/assert_equals.ts";
|
|
9
|
+
*
|
|
10
|
+
* assertEquals(escape("<>'&AA"), "<>'&AA");
|
|
11
|
+
*
|
|
12
|
+
* // characters that don't need to be escaped will be left alone,
|
|
13
|
+
* // even if named HTML entities exist for them
|
|
14
|
+
* assertEquals(escape("þð"), "þð");
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function escape(str: string): string;
|
|
18
|
+
export type UnescapeOptions = {
|
|
19
|
+
entityList: EntityList;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Unescapes HTML entities in text
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { unescape } from "https://deno.land/std@$STD_VERSION/html/entities.ts";
|
|
27
|
+
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/assert/assert_equals.ts";
|
|
28
|
+
*
|
|
29
|
+
* // default options (only handles &<>'" and numeric entities)
|
|
30
|
+
* assertEquals(unescape("<>'&AA"), "<>'&AA");
|
|
31
|
+
* assertEquals(unescape("þð"), "þð");
|
|
32
|
+
*
|
|
33
|
+
* // using the full named entity list from the HTML spec (~47K unminified)
|
|
34
|
+
* import entityList from "https://deno.land/std@$STD_VERSION/html/named_entity_list.json" assert { type: "json" };
|
|
35
|
+
* assertEquals(unescape("þð", { entityList }), "þð");
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function unescape(str: string, options?: Partial<UnescapeOptions>): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
2
|
+
// This module is browser compatible.
|
|
3
|
+
const rawToEntityEntries = [
|
|
4
|
+
["&", "&"],
|
|
5
|
+
["<", "<"],
|
|
6
|
+
[">", ">"],
|
|
7
|
+
['"', """],
|
|
8
|
+
["'", "'"],
|
|
9
|
+
];
|
|
10
|
+
const defaultEntityList = Object.fromEntries([
|
|
11
|
+
...rawToEntityEntries.map(([raw, entity]) => [entity, raw]),
|
|
12
|
+
["'", "'"],
|
|
13
|
+
[" ", "\xa0"],
|
|
14
|
+
]);
|
|
15
|
+
const rawToEntity = new Map(rawToEntityEntries);
|
|
16
|
+
const rawRe = new RegExp(`[${[...rawToEntity.keys()].join("")}]`, "g");
|
|
17
|
+
/**
|
|
18
|
+
* Escapes text for safe interpolation into HTML text content and quoted attributes
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { escape } from "https://deno.land/std@$STD_VERSION/html/entities.ts";
|
|
23
|
+
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/assert/assert_equals.ts";
|
|
24
|
+
*
|
|
25
|
+
* assertEquals(escape("<>'&AA"), "<>'&AA");
|
|
26
|
+
*
|
|
27
|
+
* // characters that don't need to be escaped will be left alone,
|
|
28
|
+
* // even if named HTML entities exist for them
|
|
29
|
+
* assertEquals(escape("þð"), "þð");
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function escape(str) {
|
|
33
|
+
return str.replaceAll(rawRe, (m) => rawToEntity.get(m));
|
|
34
|
+
}
|
|
35
|
+
const defaultUnescapeOptions = {
|
|
36
|
+
entityList: defaultEntityList,
|
|
37
|
+
};
|
|
38
|
+
const MAX_CODE_POINT = 0x10ffff;
|
|
39
|
+
const RX_DEC_ENTITY = /&#([0-9]+);/g;
|
|
40
|
+
const RX_HEX_ENTITY = /&#x(\p{AHex}+);/gu;
|
|
41
|
+
const entityListRegexCache = new WeakMap();
|
|
42
|
+
/**
|
|
43
|
+
* Unescapes HTML entities in text
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { unescape } from "https://deno.land/std@$STD_VERSION/html/entities.ts";
|
|
48
|
+
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/assert/assert_equals.ts";
|
|
49
|
+
*
|
|
50
|
+
* // default options (only handles &<>'" and numeric entities)
|
|
51
|
+
* assertEquals(unescape("<>'&AA"), "<>'&AA");
|
|
52
|
+
* assertEquals(unescape("þð"), "þð");
|
|
53
|
+
*
|
|
54
|
+
* // using the full named entity list from the HTML spec (~47K unminified)
|
|
55
|
+
* import entityList from "https://deno.land/std@$STD_VERSION/html/named_entity_list.json" assert { type: "json" };
|
|
56
|
+
* assertEquals(unescape("þð", { entityList }), "þð");
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function unescape(str, options = {}) {
|
|
60
|
+
const { entityList } = { ...defaultUnescapeOptions, ...options };
|
|
61
|
+
let entityRe = entityListRegexCache.get(entityList);
|
|
62
|
+
if (!entityRe) {
|
|
63
|
+
entityRe = new RegExp(`(${Object.keys(entityList)
|
|
64
|
+
.sort((a, b) => b.length - a.length)
|
|
65
|
+
.join("|")})`, "g");
|
|
66
|
+
entityListRegexCache.set(entityList, entityRe);
|
|
67
|
+
}
|
|
68
|
+
return str
|
|
69
|
+
.replaceAll(entityRe, (m) => entityList[m])
|
|
70
|
+
.replaceAll(RX_DEC_ENTITY, (_, dec) => codePointStrToChar(dec, 10))
|
|
71
|
+
.replaceAll(RX_HEX_ENTITY, (_, hex) => codePointStrToChar(hex, 16));
|
|
72
|
+
}
|
|
73
|
+
function codePointStrToChar(codePointStr, radix) {
|
|
74
|
+
const codePoint = parseInt(codePointStr, radix);
|
|
75
|
+
return codePoint > MAX_CODE_POINT ? "�" : String.fromCodePoint(codePoint);
|
|
76
|
+
}
|
package/esm/deps.d.ts
CHANGED
package/esm/deps.js
CHANGED
package/esm/mod.d.ts
CHANGED
package/esm/mod.js
CHANGED
package/esm/plugins/escape.js
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
|
+
import { html } from "../deps.js";
|
|
1
2
|
export default function () {
|
|
2
3
|
return (env) => {
|
|
3
|
-
env.filters.escape = escape;
|
|
4
|
+
env.filters.escape = html.escape;
|
|
4
5
|
};
|
|
5
6
|
}
|
|
6
|
-
const escapeMap = {
|
|
7
|
-
"&": "&",
|
|
8
|
-
"<": "<",
|
|
9
|
-
">": ">",
|
|
10
|
-
'"': """,
|
|
11
|
-
"'": "'",
|
|
12
|
-
"`": "`",
|
|
13
|
-
};
|
|
14
|
-
function escape(str) {
|
|
15
|
-
return str.replace(/[&<>"'`]/g, (match) => escapeMap[match]);
|
|
16
|
-
}
|
package/esm/plugins/export.js
CHANGED
|
@@ -8,6 +8,7 @@ function exportTag(env, code, _output, tokens) {
|
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
10
|
const expression = code.replace(/^export\s+/, "");
|
|
11
|
+
const { dataVarname } = env.options;
|
|
11
12
|
// Value is set (e.g. {{ export foo = "bar" }})
|
|
12
13
|
if (expression.includes("=")) {
|
|
13
14
|
const match = code.match(/^export\s+([\w]+)\s*=\s*([\s\S]+)$/);
|
|
@@ -16,19 +17,19 @@ function exportTag(env, code, _output, tokens) {
|
|
|
16
17
|
}
|
|
17
18
|
const [, variable, value] = match;
|
|
18
19
|
const val = env.compileFilters(tokens, value);
|
|
19
|
-
return `if (
|
|
20
|
+
return `if (${dataVarname}.hasOwnProperty("${variable}")) {
|
|
20
21
|
${variable} = ${val};
|
|
21
22
|
} else {
|
|
22
23
|
var ${variable} = ${val};
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
${dataVarname}["${variable}"] = ${variable};
|
|
25
26
|
__exports["${variable}"] = ${variable};
|
|
26
27
|
`;
|
|
27
28
|
}
|
|
28
29
|
// Value is captured (eg: {{ export foo }}bar{{ /export }})
|
|
29
30
|
const compiled = [];
|
|
30
31
|
const compiledFilters = env.compileFilters(tokens, expression);
|
|
31
|
-
compiled.push(`if (
|
|
32
|
+
compiled.push(`if (${dataVarname}.hasOwnProperty("${expression}")) {
|
|
32
33
|
${expression} = "";
|
|
33
34
|
} else {
|
|
34
35
|
var ${expression} = "";
|
|
@@ -40,7 +41,7 @@ function exportTag(env, code, _output, tokens) {
|
|
|
40
41
|
}
|
|
41
42
|
tokens.shift();
|
|
42
43
|
compiled.push(`${expression} = ${compiledFilters};`);
|
|
43
|
-
compiled.push(
|
|
44
|
+
compiled.push(`${dataVarname}["${expression.trim()}"] = ${expression};`);
|
|
44
45
|
compiled.push(`__exports["${expression.trim()}"] = ${expression};`);
|
|
45
46
|
return compiled.join("\n");
|
|
46
47
|
}
|
package/esm/plugins/import.js
CHANGED
|
@@ -3,7 +3,7 @@ export default function () {
|
|
|
3
3
|
env.tags.push(importTag);
|
|
4
4
|
};
|
|
5
5
|
}
|
|
6
|
-
function importTag(
|
|
6
|
+
function importTag(env, code) {
|
|
7
7
|
if (!code.startsWith("import ")) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
@@ -11,14 +11,7 @@ function importTag(_env, code) {
|
|
|
11
11
|
if (!match) {
|
|
12
12
|
throw new Error(`Invalid import: ${code}`);
|
|
13
13
|
}
|
|
14
|
-
const [
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
if (vars.startsWith("{")) {
|
|
18
|
-
compiled.push(`let ${vars} = __tmp;`);
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
compiled.push(`let ${vars} = __tmp;`);
|
|
22
|
-
}
|
|
23
|
-
return compiled.join("\n");
|
|
14
|
+
const [, vars, file] = match;
|
|
15
|
+
const { dataVarname } = env.options;
|
|
16
|
+
return `let ${vars} = await __env.run(${file}, {...${dataVarname}}, __file);`;
|
|
24
17
|
}
|
package/esm/plugins/include.js
CHANGED
|
@@ -12,9 +12,10 @@ function includeTag(env, code, output, tokens) {
|
|
|
12
12
|
throw new Error(`Invalid include: ${code}`);
|
|
13
13
|
}
|
|
14
14
|
const [_, file, data] = match;
|
|
15
|
+
const { dataVarname } = env.options;
|
|
15
16
|
return `{
|
|
16
|
-
__tmp = await __env.run(${file},
|
|
17
|
-
{
|
|
17
|
+
const __tmp = await __env.run(${file},
|
|
18
|
+
{...${dataVarname}${data ? `, ${data}` : ""}},
|
|
18
19
|
__file
|
|
19
20
|
);
|
|
20
21
|
${output} += ${env.compileFilters(tokens, "__tmp.content")};
|
package/esm/plugins/layout.js
CHANGED
|
@@ -16,15 +16,16 @@ function layoutTag(env, code, output, tokens) {
|
|
|
16
16
|
const compiled = [];
|
|
17
17
|
const compiledFilters = env.compileFilters(tokens, varname);
|
|
18
18
|
compiled.push("{");
|
|
19
|
-
compiled.push(`
|
|
19
|
+
compiled.push(`let ${varname} = "";`);
|
|
20
20
|
compiled.push(...env.compileTokens(tokens, varname, ["/layout"]));
|
|
21
21
|
if (tokens.length && (tokens[0][0] !== "tag" || tokens[0][1] !== "/layout")) {
|
|
22
22
|
throw new Error(`Missing closing tag for layout tag: ${code}`);
|
|
23
23
|
}
|
|
24
24
|
tokens.shift();
|
|
25
25
|
compiled.push(`${varname} = ${compiledFilters};`);
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const { dataVarname } = env.options;
|
|
27
|
+
compiled.push(`const __tmp = await __env.run(${file},
|
|
28
|
+
{...${dataVarname}${data ? `, ${data}` : ""}, content: ${env.compileFilters(tokens, varname)}},
|
|
28
29
|
__file
|
|
29
30
|
);
|
|
30
31
|
${output} += __tmp.content;`);
|
package/esm/plugins/set.js
CHANGED
|
@@ -8,6 +8,7 @@ function setTag(env, code, _output, tokens) {
|
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
10
|
const expression = code.replace(/^set\s+/, "");
|
|
11
|
+
const { dataVarname } = env.options;
|
|
11
12
|
// Value is set (e.g. {{ set foo = "bar" }})
|
|
12
13
|
if (expression.includes("=")) {
|
|
13
14
|
const match = code.match(/^set\s+([\w]+)\s*=\s*([\s\S]+)$/);
|
|
@@ -16,18 +17,18 @@ function setTag(env, code, _output, tokens) {
|
|
|
16
17
|
}
|
|
17
18
|
const [, variable, value] = match;
|
|
18
19
|
const val = env.compileFilters(tokens, value);
|
|
19
|
-
return `if (
|
|
20
|
+
return `if (${dataVarname}.hasOwnProperty("${variable}")) {
|
|
20
21
|
${variable} = ${val};
|
|
21
22
|
} else {
|
|
22
23
|
var ${variable} = ${val};
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
${dataVarname}["${variable}"] = ${variable};
|
|
25
26
|
`;
|
|
26
27
|
}
|
|
27
28
|
// Value is captured (eg: {{ set foo }}bar{{ /set }})
|
|
28
29
|
const compiled = [];
|
|
29
30
|
const compiledFilters = env.compileFilters(tokens, expression);
|
|
30
|
-
compiled.push(`if (
|
|
31
|
+
compiled.push(`if (${dataVarname}.hasOwnProperty("${expression}")) {
|
|
31
32
|
${expression} = "";
|
|
32
33
|
} else {
|
|
33
34
|
var ${expression} = "";
|
|
@@ -39,6 +40,6 @@ function setTag(env, code, _output, tokens) {
|
|
|
39
40
|
}
|
|
40
41
|
tokens.shift();
|
|
41
42
|
compiled.push(`${expression} = ${compiledFilters};`);
|
|
42
|
-
compiled.push(
|
|
43
|
+
compiled.push(`${dataVarname}["${expression.trim()}"] = ${expression};`);
|
|
43
44
|
return compiled.join("\n");
|
|
44
45
|
}
|
package/esm/plugins/unescape.js
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
|
+
import { html } from "../deps.js";
|
|
1
2
|
export default function () {
|
|
2
3
|
return (env) => {
|
|
3
|
-
env.filters.unescape = unescape;
|
|
4
|
+
env.filters.unescape = html.unescape;
|
|
4
5
|
};
|
|
5
6
|
}
|
|
6
|
-
const unescapeMap = {
|
|
7
|
-
"&": "&",
|
|
8
|
-
"<": "<",
|
|
9
|
-
">": ">",
|
|
10
|
-
""": '"',
|
|
11
|
-
"'": "'",
|
|
12
|
-
"`": "`",
|
|
13
|
-
};
|
|
14
|
-
function unescape(str) {
|
|
15
|
-
return str.replace(/(&|<|>|"|'|`)/g, (match) => unescapeMap[match]);
|
|
16
|
-
}
|
package/esm/src/environment.d.ts
CHANGED
package/esm/src/environment.js
CHANGED
|
@@ -37,15 +37,12 @@ export class Environment {
|
|
|
37
37
|
try {
|
|
38
38
|
const tokens = tokenize(source);
|
|
39
39
|
const code = this.compileTokens(tokens).join("\n");
|
|
40
|
-
const
|
|
40
|
+
const { dataVarname, useWith } = this.options;
|
|
41
|
+
const constructor = new Function("__file", "__env", "__defaults", `return${sync ? "" : " async"} function (${dataVarname}) {
|
|
41
42
|
try {
|
|
42
|
-
|
|
43
|
-
const ${this.options.dataVarname} = __data;
|
|
44
|
-
let __tmp;
|
|
43
|
+
${dataVarname} = Object.assign({}, __defaults, ${dataVarname});
|
|
45
44
|
const __exports = { content: "" };
|
|
46
|
-
with (
|
|
47
|
-
${code}
|
|
48
|
-
}
|
|
45
|
+
${useWith ? `with (${dataVarname}) {${code}}` : code}
|
|
49
46
|
return __exports;
|
|
50
47
|
} catch (cause) {
|
|
51
48
|
throw new Error(\`Error rendering template: \${__file}\`, { cause });
|
|
@@ -112,8 +109,11 @@ export class Environment {
|
|
|
112
109
|
}
|
|
113
110
|
const [_, isAsync, name, args] = match;
|
|
114
111
|
if (!this.filters[name]) {
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
if (name === "safe") {
|
|
113
|
+
unescaped = true;
|
|
114
|
+
}
|
|
115
|
+
else if (isGlobal(name)) {
|
|
116
|
+
// If a global function
|
|
117
117
|
output = `${isAsync ? "await " : ""}${name}(${output}${args ? `, ${args}` : ""})`;
|
|
118
118
|
}
|
|
119
119
|
else {
|
|
@@ -122,9 +122,6 @@ export class Environment {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
else {
|
|
125
|
-
if (name === "unescape") {
|
|
126
|
-
unescaped = true;
|
|
127
|
-
}
|
|
128
125
|
// It's a filter (e.g. filters.upper())
|
|
129
126
|
output = `${isAsync ? "await " : ""}__env.filters.${name}(${output}${args ? `, ${args}` : ""})`;
|
|
130
127
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type EntityList = Record<string, string>;
|
|
2
|
+
/**
|
|
3
|
+
* Escapes text for safe interpolation into HTML text content and quoted attributes
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { escape } from "https://deno.land/std@$STD_VERSION/html/entities.ts";
|
|
8
|
+
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/assert/assert_equals.ts";
|
|
9
|
+
*
|
|
10
|
+
* assertEquals(escape("<>'&AA"), "<>'&AA");
|
|
11
|
+
*
|
|
12
|
+
* // characters that don't need to be escaped will be left alone,
|
|
13
|
+
* // even if named HTML entities exist for them
|
|
14
|
+
* assertEquals(escape("þð"), "þð");
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function escape(str: string): string;
|
|
18
|
+
export type UnescapeOptions = {
|
|
19
|
+
entityList: EntityList;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Unescapes HTML entities in text
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { unescape } from "https://deno.land/std@$STD_VERSION/html/entities.ts";
|
|
27
|
+
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/assert/assert_equals.ts";
|
|
28
|
+
*
|
|
29
|
+
* // default options (only handles &<>'" and numeric entities)
|
|
30
|
+
* assertEquals(unescape("<>'&AA"), "<>'&AA");
|
|
31
|
+
* assertEquals(unescape("þð"), "þð");
|
|
32
|
+
*
|
|
33
|
+
* // using the full named entity list from the HTML spec (~47K unminified)
|
|
34
|
+
* import entityList from "https://deno.land/std@$STD_VERSION/html/named_entity_list.json" assert { type: "json" };
|
|
35
|
+
* assertEquals(unescape("þð", { entityList }), "þð");
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function unescape(str: string, options?: Partial<UnescapeOptions>): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
3
|
+
// This module is browser compatible.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.unescape = exports.escape = void 0;
|
|
6
|
+
const rawToEntityEntries = [
|
|
7
|
+
["&", "&"],
|
|
8
|
+
["<", "<"],
|
|
9
|
+
[">", ">"],
|
|
10
|
+
['"', """],
|
|
11
|
+
["'", "'"],
|
|
12
|
+
];
|
|
13
|
+
const defaultEntityList = Object.fromEntries([
|
|
14
|
+
...rawToEntityEntries.map(([raw, entity]) => [entity, raw]),
|
|
15
|
+
["'", "'"],
|
|
16
|
+
[" ", "\xa0"],
|
|
17
|
+
]);
|
|
18
|
+
const rawToEntity = new Map(rawToEntityEntries);
|
|
19
|
+
const rawRe = new RegExp(`[${[...rawToEntity.keys()].join("")}]`, "g");
|
|
20
|
+
/**
|
|
21
|
+
* Escapes text for safe interpolation into HTML text content and quoted attributes
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { escape } from "https://deno.land/std@$STD_VERSION/html/entities.ts";
|
|
26
|
+
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/assert/assert_equals.ts";
|
|
27
|
+
*
|
|
28
|
+
* assertEquals(escape("<>'&AA"), "<>'&AA");
|
|
29
|
+
*
|
|
30
|
+
* // characters that don't need to be escaped will be left alone,
|
|
31
|
+
* // even if named HTML entities exist for them
|
|
32
|
+
* assertEquals(escape("þð"), "þð");
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
function escape(str) {
|
|
36
|
+
return str.replaceAll(rawRe, (m) => rawToEntity.get(m));
|
|
37
|
+
}
|
|
38
|
+
exports.escape = escape;
|
|
39
|
+
const defaultUnescapeOptions = {
|
|
40
|
+
entityList: defaultEntityList,
|
|
41
|
+
};
|
|
42
|
+
const MAX_CODE_POINT = 0x10ffff;
|
|
43
|
+
const RX_DEC_ENTITY = /&#([0-9]+);/g;
|
|
44
|
+
const RX_HEX_ENTITY = /&#x(\p{AHex}+);/gu;
|
|
45
|
+
const entityListRegexCache = new WeakMap();
|
|
46
|
+
/**
|
|
47
|
+
* Unescapes HTML entities in text
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { unescape } from "https://deno.land/std@$STD_VERSION/html/entities.ts";
|
|
52
|
+
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/assert/assert_equals.ts";
|
|
53
|
+
*
|
|
54
|
+
* // default options (only handles &<>'" and numeric entities)
|
|
55
|
+
* assertEquals(unescape("<>'&AA"), "<>'&AA");
|
|
56
|
+
* assertEquals(unescape("þð"), "þð");
|
|
57
|
+
*
|
|
58
|
+
* // using the full named entity list from the HTML spec (~47K unminified)
|
|
59
|
+
* import entityList from "https://deno.land/std@$STD_VERSION/html/named_entity_list.json" assert { type: "json" };
|
|
60
|
+
* assertEquals(unescape("þð", { entityList }), "þð");
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
function unescape(str, options = {}) {
|
|
64
|
+
const { entityList } = { ...defaultUnescapeOptions, ...options };
|
|
65
|
+
let entityRe = entityListRegexCache.get(entityList);
|
|
66
|
+
if (!entityRe) {
|
|
67
|
+
entityRe = new RegExp(`(${Object.keys(entityList)
|
|
68
|
+
.sort((a, b) => b.length - a.length)
|
|
69
|
+
.join("|")})`, "g");
|
|
70
|
+
entityListRegexCache.set(entityList, entityRe);
|
|
71
|
+
}
|
|
72
|
+
return str
|
|
73
|
+
.replaceAll(entityRe, (m) => entityList[m])
|
|
74
|
+
.replaceAll(RX_DEC_ENTITY, (_, dec) => codePointStrToChar(dec, 10))
|
|
75
|
+
.replaceAll(RX_HEX_ENTITY, (_, hex) => codePointStrToChar(hex, 16));
|
|
76
|
+
}
|
|
77
|
+
exports.unescape = unescape;
|
|
78
|
+
function codePointStrToChar(codePointStr, radix) {
|
|
79
|
+
const codePoint = parseInt(codePointStr, radix);
|
|
80
|
+
return codePoint > MAX_CODE_POINT ? "�" : String.fromCodePoint(codePoint);
|
|
81
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
3
|
+
// This module is browser compatible.
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
16
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
/**
|
|
20
|
+
* Functions for HTML tasks such as escaping or unescaping HTML entities
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
__exportStar(require("./entities.js"), exports);
|
package/script/deps.d.ts
CHANGED
package/script/deps.js
CHANGED
|
@@ -23,5 +23,6 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.path = void 0;
|
|
26
|
+
exports.html = exports.path = void 0;
|
|
27
27
|
exports.path = __importStar(require("./deps/deno.land/std@0.201.0/path/mod.js"));
|
|
28
|
+
exports.html = __importStar(require("./deps/deno.land/std@0.201.0/html/mod.js"));
|
package/script/mod.d.ts
CHANGED
package/script/mod.js
CHANGED
package/script/plugins/escape.js
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const deps_js_1 = require("../deps.js");
|
|
3
4
|
function default_1() {
|
|
4
5
|
return (env) => {
|
|
5
|
-
env.filters.escape = escape;
|
|
6
|
+
env.filters.escape = deps_js_1.html.escape;
|
|
6
7
|
};
|
|
7
8
|
}
|
|
8
9
|
exports.default = default_1;
|
|
9
|
-
const escapeMap = {
|
|
10
|
-
"&": "&",
|
|
11
|
-
"<": "<",
|
|
12
|
-
">": ">",
|
|
13
|
-
'"': """,
|
|
14
|
-
"'": "'",
|
|
15
|
-
"`": "`",
|
|
16
|
-
};
|
|
17
|
-
function escape(str) {
|
|
18
|
-
return str.replace(/[&<>"'`]/g, (match) => escapeMap[match]);
|
|
19
|
-
}
|
package/script/plugins/export.js
CHANGED
|
@@ -11,6 +11,7 @@ function exportTag(env, code, _output, tokens) {
|
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
13
|
const expression = code.replace(/^export\s+/, "");
|
|
14
|
+
const { dataVarname } = env.options;
|
|
14
15
|
// Value is set (e.g. {{ export foo = "bar" }})
|
|
15
16
|
if (expression.includes("=")) {
|
|
16
17
|
const match = code.match(/^export\s+([\w]+)\s*=\s*([\s\S]+)$/);
|
|
@@ -19,19 +20,19 @@ function exportTag(env, code, _output, tokens) {
|
|
|
19
20
|
}
|
|
20
21
|
const [, variable, value] = match;
|
|
21
22
|
const val = env.compileFilters(tokens, value);
|
|
22
|
-
return `if (
|
|
23
|
+
return `if (${dataVarname}.hasOwnProperty("${variable}")) {
|
|
23
24
|
${variable} = ${val};
|
|
24
25
|
} else {
|
|
25
26
|
var ${variable} = ${val};
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
${dataVarname}["${variable}"] = ${variable};
|
|
28
29
|
__exports["${variable}"] = ${variable};
|
|
29
30
|
`;
|
|
30
31
|
}
|
|
31
32
|
// Value is captured (eg: {{ export foo }}bar{{ /export }})
|
|
32
33
|
const compiled = [];
|
|
33
34
|
const compiledFilters = env.compileFilters(tokens, expression);
|
|
34
|
-
compiled.push(`if (
|
|
35
|
+
compiled.push(`if (${dataVarname}.hasOwnProperty("${expression}")) {
|
|
35
36
|
${expression} = "";
|
|
36
37
|
} else {
|
|
37
38
|
var ${expression} = "";
|
|
@@ -43,7 +44,7 @@ function exportTag(env, code, _output, tokens) {
|
|
|
43
44
|
}
|
|
44
45
|
tokens.shift();
|
|
45
46
|
compiled.push(`${expression} = ${compiledFilters};`);
|
|
46
|
-
compiled.push(
|
|
47
|
+
compiled.push(`${dataVarname}["${expression.trim()}"] = ${expression};`);
|
|
47
48
|
compiled.push(`__exports["${expression.trim()}"] = ${expression};`);
|
|
48
49
|
return compiled.join("\n");
|
|
49
50
|
}
|
package/script/plugins/import.js
CHANGED
|
@@ -6,7 +6,7 @@ function default_1() {
|
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
8
|
exports.default = default_1;
|
|
9
|
-
function importTag(
|
|
9
|
+
function importTag(env, code) {
|
|
10
10
|
if (!code.startsWith("import ")) {
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
@@ -14,14 +14,7 @@ function importTag(_env, code) {
|
|
|
14
14
|
if (!match) {
|
|
15
15
|
throw new Error(`Invalid import: ${code}`);
|
|
16
16
|
}
|
|
17
|
-
const [
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
if (vars.startsWith("{")) {
|
|
21
|
-
compiled.push(`let ${vars} = __tmp;`);
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
compiled.push(`let ${vars} = __tmp;`);
|
|
25
|
-
}
|
|
26
|
-
return compiled.join("\n");
|
|
17
|
+
const [, vars, file] = match;
|
|
18
|
+
const { dataVarname } = env.options;
|
|
19
|
+
return `let ${vars} = await __env.run(${file}, {...${dataVarname}}, __file);`;
|
|
27
20
|
}
|
|
@@ -15,9 +15,10 @@ function includeTag(env, code, output, tokens) {
|
|
|
15
15
|
throw new Error(`Invalid include: ${code}`);
|
|
16
16
|
}
|
|
17
17
|
const [_, file, data] = match;
|
|
18
|
+
const { dataVarname } = env.options;
|
|
18
19
|
return `{
|
|
19
|
-
__tmp = await __env.run(${file},
|
|
20
|
-
{
|
|
20
|
+
const __tmp = await __env.run(${file},
|
|
21
|
+
{...${dataVarname}${data ? `, ${data}` : ""}},
|
|
21
22
|
__file
|
|
22
23
|
);
|
|
23
24
|
${output} += ${env.compileFilters(tokens, "__tmp.content")};
|
package/script/plugins/layout.js
CHANGED
|
@@ -19,15 +19,16 @@ function layoutTag(env, code, output, tokens) {
|
|
|
19
19
|
const compiled = [];
|
|
20
20
|
const compiledFilters = env.compileFilters(tokens, varname);
|
|
21
21
|
compiled.push("{");
|
|
22
|
-
compiled.push(`
|
|
22
|
+
compiled.push(`let ${varname} = "";`);
|
|
23
23
|
compiled.push(...env.compileTokens(tokens, varname, ["/layout"]));
|
|
24
24
|
if (tokens.length && (tokens[0][0] !== "tag" || tokens[0][1] !== "/layout")) {
|
|
25
25
|
throw new Error(`Missing closing tag for layout tag: ${code}`);
|
|
26
26
|
}
|
|
27
27
|
tokens.shift();
|
|
28
28
|
compiled.push(`${varname} = ${compiledFilters};`);
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const { dataVarname } = env.options;
|
|
30
|
+
compiled.push(`const __tmp = await __env.run(${file},
|
|
31
|
+
{...${dataVarname}${data ? `, ${data}` : ""}, content: ${env.compileFilters(tokens, varname)}},
|
|
31
32
|
__file
|
|
32
33
|
);
|
|
33
34
|
${output} += __tmp.content;`);
|
package/script/plugins/set.js
CHANGED
|
@@ -11,6 +11,7 @@ function setTag(env, code, _output, tokens) {
|
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
13
|
const expression = code.replace(/^set\s+/, "");
|
|
14
|
+
const { dataVarname } = env.options;
|
|
14
15
|
// Value is set (e.g. {{ set foo = "bar" }})
|
|
15
16
|
if (expression.includes("=")) {
|
|
16
17
|
const match = code.match(/^set\s+([\w]+)\s*=\s*([\s\S]+)$/);
|
|
@@ -19,18 +20,18 @@ function setTag(env, code, _output, tokens) {
|
|
|
19
20
|
}
|
|
20
21
|
const [, variable, value] = match;
|
|
21
22
|
const val = env.compileFilters(tokens, value);
|
|
22
|
-
return `if (
|
|
23
|
+
return `if (${dataVarname}.hasOwnProperty("${variable}")) {
|
|
23
24
|
${variable} = ${val};
|
|
24
25
|
} else {
|
|
25
26
|
var ${variable} = ${val};
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
${dataVarname}["${variable}"] = ${variable};
|
|
28
29
|
`;
|
|
29
30
|
}
|
|
30
31
|
// Value is captured (eg: {{ set foo }}bar{{ /set }})
|
|
31
32
|
const compiled = [];
|
|
32
33
|
const compiledFilters = env.compileFilters(tokens, expression);
|
|
33
|
-
compiled.push(`if (
|
|
34
|
+
compiled.push(`if (${dataVarname}.hasOwnProperty("${expression}")) {
|
|
34
35
|
${expression} = "";
|
|
35
36
|
} else {
|
|
36
37
|
var ${expression} = "";
|
|
@@ -42,6 +43,6 @@ function setTag(env, code, _output, tokens) {
|
|
|
42
43
|
}
|
|
43
44
|
tokens.shift();
|
|
44
45
|
compiled.push(`${expression} = ${compiledFilters};`);
|
|
45
|
-
compiled.push(
|
|
46
|
+
compiled.push(`${dataVarname}["${expression.trim()}"] = ${expression};`);
|
|
46
47
|
return compiled.join("\n");
|
|
47
48
|
}
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const deps_js_1 = require("../deps.js");
|
|
3
4
|
function default_1() {
|
|
4
5
|
return (env) => {
|
|
5
|
-
env.filters.unescape = unescape;
|
|
6
|
+
env.filters.unescape = deps_js_1.html.unescape;
|
|
6
7
|
};
|
|
7
8
|
}
|
|
8
9
|
exports.default = default_1;
|
|
9
|
-
const unescapeMap = {
|
|
10
|
-
"&": "&",
|
|
11
|
-
"<": "<",
|
|
12
|
-
">": ">",
|
|
13
|
-
""": '"',
|
|
14
|
-
"'": "'",
|
|
15
|
-
"`": "`",
|
|
16
|
-
};
|
|
17
|
-
function unescape(str) {
|
|
18
|
-
return str.replace(/(&|<|>|"|'|`)/g, (match) => unescapeMap[match]);
|
|
19
|
-
}
|
|
@@ -66,15 +66,12 @@ class Environment {
|
|
|
66
66
|
try {
|
|
67
67
|
const tokens = (0, tokenizer_js_1.default)(source);
|
|
68
68
|
const code = this.compileTokens(tokens).join("\n");
|
|
69
|
-
const
|
|
69
|
+
const { dataVarname, useWith } = this.options;
|
|
70
|
+
const constructor = new Function("__file", "__env", "__defaults", `return${sync ? "" : " async"} function (${dataVarname}) {
|
|
70
71
|
try {
|
|
71
|
-
|
|
72
|
-
const ${this.options.dataVarname} = __data;
|
|
73
|
-
let __tmp;
|
|
72
|
+
${dataVarname} = Object.assign({}, __defaults, ${dataVarname});
|
|
74
73
|
const __exports = { content: "" };
|
|
75
|
-
with (
|
|
76
|
-
${code}
|
|
77
|
-
}
|
|
74
|
+
${useWith ? `with (${dataVarname}) {${code}}` : code}
|
|
78
75
|
return __exports;
|
|
79
76
|
} catch (cause) {
|
|
80
77
|
throw new Error(\`Error rendering template: \${__file}\`, { cause });
|
|
@@ -141,8 +138,11 @@ class Environment {
|
|
|
141
138
|
}
|
|
142
139
|
const [_, isAsync, name, args] = match;
|
|
143
140
|
if (!this.filters[name]) {
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
if (name === "safe") {
|
|
142
|
+
unescaped = true;
|
|
143
|
+
}
|
|
144
|
+
else if (isGlobal(name)) {
|
|
145
|
+
// If a global function
|
|
146
146
|
output = `${isAsync ? "await " : ""}${name}(${output}${args ? `, ${args}` : ""})`;
|
|
147
147
|
}
|
|
148
148
|
else {
|
|
@@ -151,9 +151,6 @@ class Environment {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
else {
|
|
154
|
-
if (name === "unescape") {
|
|
155
|
-
unescaped = true;
|
|
156
|
-
}
|
|
157
154
|
// It's a filter (e.g. filters.upper())
|
|
158
155
|
output = `${isAsync ? "await " : ""}__env.filters.${name}(${output}${args ? `, ${args}` : ""})`;
|
|
159
156
|
}
|