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 CHANGED
@@ -1,7 +1,7 @@
1
- # VENTO
1
+ # Vento
2
2
 
3
3
  This is a minimal template engine inspired by other great engines like Nunjucks,
4
- Liquid, Mustache or EJS.
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
- This is a library for Deno. ~~I'm planning to release an NPM version in the
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
- ## API
97
+ ## Editor support
155
98
 
156
- [Read the docs](https://oscarotero.github.io/vento/)
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"), "&lt;&gt;&#39;&amp;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("&lt;&gt;&apos;&amp;&#65;&#x41;"), "<>'&AA");
31
+ * assertEquals(unescape("&thorn;&eth;"), "&thorn;&eth;");
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("&thorn;&eth;", { 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
+ ["&", "&amp;"],
5
+ ["<", "&lt;"],
6
+ [">", "&gt;"],
7
+ ['"', "&quot;"],
8
+ ["'", "&#39;"],
9
+ ];
10
+ const defaultEntityList = Object.fromEntries([
11
+ ...rawToEntityEntries.map(([raw, entity]) => [entity, raw]),
12
+ ["&apos;", "'"],
13
+ ["&nbsp;", "\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"), "&lt;&gt;&#39;&amp;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("&lt;&gt;&apos;&amp;&#65;&#x41;"), "<>'&AA");
52
+ * assertEquals(unescape("&thorn;&eth;"), "&thorn;&eth;");
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("&thorn;&eth;", { 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
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Functions for HTML tasks such as escaping or unescaping HTML entities
3
+ *
4
+ * @module
5
+ */
6
+ export * from "./entities.js";
@@ -0,0 +1,8 @@
1
+ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
2
+ // This module is browser compatible.
3
+ /**
4
+ * Functions for HTML tasks such as escaping or unescaping HTML entities
5
+ *
6
+ * @module
7
+ */
8
+ export * from "./entities.js";
package/esm/deps.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * as path from "./deps/deno.land/std@0.201.0/path/mod.js";
2
+ export * as html from "./deps/deno.land/std@0.201.0/html/mod.js";
package/esm/deps.js CHANGED
@@ -1 +1,2 @@
1
1
  export * as path from "./deps/deno.land/std@0.201.0/path/mod.js";
2
+ export * as html from "./deps/deno.land/std@0.201.0/html/mod.js";
package/esm/mod.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Environment } from "./src/environment.js";
2
2
  import { Loader } from "./src/loader.js";
3
3
  export interface Options {
4
4
  includes?: string | Loader;
5
+ useWith?: boolean;
5
6
  dataVarname?: string;
6
7
  autoescape?: boolean;
7
8
  }
package/esm/mod.js CHANGED
@@ -21,6 +21,7 @@ export default function (options = {}) {
21
21
  loader,
22
22
  dataVarname: options.dataVarname || "it",
23
23
  autoescape: options.autoescape || false,
24
+ useWith: options.useWith || true,
24
25
  });
25
26
  // Register basic plugins
26
27
  env.use(ifTag());
@@ -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
- "&": "&amp;",
8
- "<": "&lt;",
9
- ">": "&gt;",
10
- '"': "&quot;",
11
- "'": "&#39;",
12
- "`": "&#x60;",
13
- };
14
- function escape(str) {
15
- return str.replace(/[&<>"'`]/g, (match) => escapeMap[match]);
16
- }
@@ -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 (__data.hasOwnProperty("${variable}")) {
20
+ return `if (${dataVarname}.hasOwnProperty("${variable}")) {
20
21
  ${variable} = ${val};
21
22
  } else {
22
23
  var ${variable} = ${val};
23
24
  }
24
- __data["${variable}"] = ${variable};
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 (__data.hasOwnProperty("${expression}")) {
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(`__data["${expression.trim()}"] = ${expression};`);
44
+ compiled.push(`${dataVarname}["${expression.trim()}"] = ${expression};`);
44
45
  compiled.push(`__exports["${expression.trim()}"] = ${expression};`);
45
46
  return compiled.join("\n");
46
47
  }
@@ -3,7 +3,7 @@ export default function () {
3
3
  env.tags.push(importTag);
4
4
  };
5
5
  }
6
- function importTag(_env, code) {
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 [_, vars, file] = match;
15
- const compiled = [];
16
- compiled.push(`__tmp = await __env.run(${file}, {...__data}, __file);`);
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
  }
@@ -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
- {...__data${data ? `, ${data}` : ""}},
17
+ const __tmp = await __env.run(${file},
18
+ {...${dataVarname}${data ? `, ${data}` : ""}},
18
19
  __file
19
20
  );
20
21
  ${output} += ${env.compileFilters(tokens, "__tmp.content")};
@@ -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(`var ${varname} = "";`);
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
- compiled.push(`__tmp = await __env.run(${file},
27
- {...__data${data ? `, ${data}` : ""}, content: ${env.compileFilters(tokens, varname)}},
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;`);
@@ -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 (__data.hasOwnProperty("${variable}")) {
20
+ return `if (${dataVarname}.hasOwnProperty("${variable}")) {
20
21
  ${variable} = ${val};
21
22
  } else {
22
23
  var ${variable} = ${val};
23
24
  }
24
- __data["${variable}"] = ${variable};
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 (__data.hasOwnProperty("${expression}")) {
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(`__data["${expression.trim()}"] = ${expression};`);
43
+ compiled.push(`${dataVarname}["${expression.trim()}"] = ${expression};`);
43
44
  return compiled.join("\n");
44
45
  }
@@ -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
- "&amp;": "&",
8
- "&lt;": "<",
9
- "&gt;": ">",
10
- "&quot;": '"',
11
- "&#39;": "'",
12
- "&#x60;": "`",
13
- };
14
- function unescape(str) {
15
- return str.replace(/(&amp;|&lt;|&gt;|&quot;|&#39;|&#x60;)/g, (match) => unescapeMap[match]);
16
- }
@@ -21,6 +21,7 @@ export interface Options {
21
21
  loader: Loader;
22
22
  dataVarname: string;
23
23
  autoescape: boolean;
24
+ useWith: boolean;
24
25
  }
25
26
  export declare class Environment {
26
27
  cache: Map<string, Template>;
@@ -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 constructor = new Function("__file", "__env", "__defaults", `return${sync ? "" : " async"} function (__data) {
40
+ const { dataVarname, useWith } = this.options;
41
+ const constructor = new Function("__file", "__env", "__defaults", `return${sync ? "" : " async"} function (${dataVarname}) {
41
42
  try {
42
- __data = Object.assign({}, __defaults, __data);
43
- const ${this.options.dataVarname} = __data;
44
- let __tmp;
43
+ ${dataVarname} = Object.assign({}, __defaults, ${dataVarname});
45
44
  const __exports = { content: "" };
46
- with (__data) {
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
- // If a global function
116
- if (isGlobal(name)) {
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
@@ -2,7 +2,7 @@
2
2
  "module": "./esm/mod.js",
3
3
  "main": "./script/mod.js",
4
4
  "name": "ventojs",
5
- "version": "0.8.1",
5
+ "version": "0.9.0",
6
6
  "description": "🌬 A minimal but powerful template engine",
7
7
  "license": "MIT",
8
8
  "repository": "github:oscarotero/vento",
@@ -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"), "&lt;&gt;&#39;&amp;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("&lt;&gt;&apos;&amp;&#65;&#x41;"), "<>'&AA");
31
+ * assertEquals(unescape("&thorn;&eth;"), "&thorn;&eth;");
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("&thorn;&eth;", { 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
+ ["&", "&amp;"],
8
+ ["<", "&lt;"],
9
+ [">", "&gt;"],
10
+ ['"', "&quot;"],
11
+ ["'", "&#39;"],
12
+ ];
13
+ const defaultEntityList = Object.fromEntries([
14
+ ...rawToEntityEntries.map(([raw, entity]) => [entity, raw]),
15
+ ["&apos;", "'"],
16
+ ["&nbsp;", "\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"), "&lt;&gt;&#39;&amp;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("&lt;&gt;&apos;&amp;&#65;&#x41;"), "<>'&AA");
56
+ * assertEquals(unescape("&thorn;&eth;"), "&thorn;&eth;");
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("&thorn;&eth;", { 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,6 @@
1
+ /**
2
+ * Functions for HTML tasks such as escaping or unescaping HTML entities
3
+ *
4
+ * @module
5
+ */
6
+ export * from "./entities.js";
@@ -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
@@ -1 +1,2 @@
1
1
  export * as path from "./deps/deno.land/std@0.201.0/path/mod.js";
2
+ export * as html from "./deps/deno.land/std@0.201.0/html/mod.js";
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
@@ -2,6 +2,7 @@ import { Environment } from "./src/environment.js";
2
2
  import { Loader } from "./src/loader.js";
3
3
  export interface Options {
4
4
  includes?: string | Loader;
5
+ useWith?: boolean;
5
6
  dataVarname?: string;
6
7
  autoescape?: boolean;
7
8
  }
package/script/mod.js CHANGED
@@ -49,6 +49,7 @@ function default_1(options = {}) {
49
49
  loader,
50
50
  dataVarname: options.dataVarname || "it",
51
51
  autoescape: options.autoescape || false,
52
+ useWith: options.useWith || true,
52
53
  });
53
54
  // Register basic plugins
54
55
  env.use((0, if_js_1.default)());
@@ -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
- "&": "&amp;",
11
- "<": "&lt;",
12
- ">": "&gt;",
13
- '"': "&quot;",
14
- "'": "&#39;",
15
- "`": "&#x60;",
16
- };
17
- function escape(str) {
18
- return str.replace(/[&<>"'`]/g, (match) => escapeMap[match]);
19
- }
@@ -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 (__data.hasOwnProperty("${variable}")) {
23
+ return `if (${dataVarname}.hasOwnProperty("${variable}")) {
23
24
  ${variable} = ${val};
24
25
  } else {
25
26
  var ${variable} = ${val};
26
27
  }
27
- __data["${variable}"] = ${variable};
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 (__data.hasOwnProperty("${expression}")) {
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(`__data["${expression.trim()}"] = ${expression};`);
47
+ compiled.push(`${dataVarname}["${expression.trim()}"] = ${expression};`);
47
48
  compiled.push(`__exports["${expression.trim()}"] = ${expression};`);
48
49
  return compiled.join("\n");
49
50
  }
@@ -6,7 +6,7 @@ function default_1() {
6
6
  };
7
7
  }
8
8
  exports.default = default_1;
9
- function importTag(_env, code) {
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 [_, vars, file] = match;
18
- const compiled = [];
19
- compiled.push(`__tmp = await __env.run(${file}, {...__data}, __file);`);
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
- {...__data${data ? `, ${data}` : ""}},
20
+ const __tmp = await __env.run(${file},
21
+ {...${dataVarname}${data ? `, ${data}` : ""}},
21
22
  __file
22
23
  );
23
24
  ${output} += ${env.compileFilters(tokens, "__tmp.content")};
@@ -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(`var ${varname} = "";`);
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
- compiled.push(`__tmp = await __env.run(${file},
30
- {...__data${data ? `, ${data}` : ""}, content: ${env.compileFilters(tokens, varname)}},
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;`);
@@ -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 (__data.hasOwnProperty("${variable}")) {
23
+ return `if (${dataVarname}.hasOwnProperty("${variable}")) {
23
24
  ${variable} = ${val};
24
25
  } else {
25
26
  var ${variable} = ${val};
26
27
  }
27
- __data["${variable}"] = ${variable};
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 (__data.hasOwnProperty("${expression}")) {
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(`__data["${expression.trim()}"] = ${expression};`);
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
- "&amp;": "&",
11
- "&lt;": "<",
12
- "&gt;": ">",
13
- "&quot;": '"',
14
- "&#39;": "'",
15
- "&#x60;": "`",
16
- };
17
- function unescape(str) {
18
- return str.replace(/(&amp;|&lt;|&gt;|&quot;|&#39;|&#x60;)/g, (match) => unescapeMap[match]);
19
- }
@@ -21,6 +21,7 @@ export interface Options {
21
21
  loader: Loader;
22
22
  dataVarname: string;
23
23
  autoescape: boolean;
24
+ useWith: boolean;
24
25
  }
25
26
  export declare class Environment {
26
27
  cache: Map<string, Template>;
@@ -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 constructor = new Function("__file", "__env", "__defaults", `return${sync ? "" : " async"} function (__data) {
69
+ const { dataVarname, useWith } = this.options;
70
+ const constructor = new Function("__file", "__env", "__defaults", `return${sync ? "" : " async"} function (${dataVarname}) {
70
71
  try {
71
- __data = Object.assign({}, __defaults, __data);
72
- const ${this.options.dataVarname} = __data;
73
- let __tmp;
72
+ ${dataVarname} = Object.assign({}, __defaults, ${dataVarname});
74
73
  const __exports = { content: "" };
75
- with (__data) {
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
- // If a global function
145
- if (isGlobal(name)) {
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
  }