ventojs 2.1.0 → 2.2.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/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## [2.2.0] - 2025-10-15
8
+ ### Added
9
+ - Support for destructuring in set [#158] [#154].
10
+
11
+ ### Fixed
12
+ - Possible variables naming collision [#157].
13
+ - `auto_trim` plugin edge cases [#159].
14
+ - `set`: allow `$` character in the variable name.
15
+
16
+ ## [2.1.1] - 2025-09-18
17
+ ### Fixed
18
+ - The tag `include` fails when it's inside a `slot`.
19
+
7
20
  ## [2.1.0] - 2025-09-17
8
21
  ### Added
9
22
  - New `strict` mode to fail when using an undefined variable. This mode has a different performance profile than normal mode; it's mostly intended for testing and debug purposes. [#101], [#142]
@@ -80,8 +93,14 @@ Vento 2.0 is now dependency-free and compatible with browsers without a build st
80
93
  [#148]: https://github.com/ventojs/vento/issues/148
81
94
  [#150]: https://github.com/ventojs/vento/issues/150
82
95
  [#151]: https://github.com/ventojs/vento/issues/151
96
+ [#154]: https://github.com/ventojs/vento/issues/154
83
97
  [#156]: https://github.com/ventojs/vento/issues/156
98
+ [#157]: https://github.com/ventojs/vento/issues/157
99
+ [#158]: https://github.com/ventojs/vento/issues/158
100
+ [#159]: https://github.com/ventojs/vento/issues/159
84
101
 
102
+ [2.2.0]: https://github.com/ventojs/vento/compare/v2.1.1...v2.2.0
103
+ [2.1.1]: https://github.com/ventojs/vento/compare/v2.1.0...v2.1.1
85
104
  [2.1.0]: https://github.com/ventojs/vento/compare/v2.0.2...v2.1.0
86
105
  [2.0.2]: https://github.com/ventojs/vento/compare/v2.0.1...v2.0.2
87
106
  [2.0.1]: https://github.com/ventojs/vento/compare/v2.0.0...v2.0.1
@@ -7,6 +7,7 @@ export class Environment {
7
7
  tags = [];
8
8
  tokenPreprocessors = [];
9
9
  filters = {};
10
+ #tempVariablesCreated = 0;
10
11
  utils = {
11
12
  callMethod,
12
13
  createError,
@@ -236,6 +237,12 @@ export class Environment {
236
237
  }
237
238
  return output;
238
239
  }
240
+ getTempVariable() {
241
+ const id = this.#tempVariablesCreated;
242
+ const variable = `__tmp${id}`;
243
+ this.#tempVariablesCreated++;
244
+ return variable;
245
+ }
239
246
  }
240
247
  function isGlobal(name) {
241
248
  if (name == "name")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ventojs",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "🌬 A minimal but powerful template engine",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,6 +1,5 @@
1
1
  export const defaultTags = [
2
2
  ">",
3
- "#",
4
3
  "set",
5
4
  "/set",
6
5
  "if",
@@ -15,6 +14,8 @@ export const defaultTags = [
15
14
  "/export",
16
15
  "import",
17
16
  ];
17
+ const LEADING_WHITESPACE = /(^|\n)[ \t]+$/;
18
+ const TRAILING_WHITESPACE = /^[ \t]*\r?\n/;
18
19
  export default function (options = { tags: defaultTags }) {
19
20
  return (env) => {
20
21
  env.tokenPreprocessors.push((_, tokens) => autoTrim(tokens, options));
@@ -22,21 +23,34 @@ export default function (options = { tags: defaultTags }) {
22
23
  }
23
24
  export function autoTrim(tokens, options) {
24
25
  for (let i = 0; i < tokens.length; i++) {
25
- const previous = tokens[i - 1];
26
26
  const token = tokens[i];
27
- const next = tokens[i + 1];
28
27
  const [type, code] = token;
29
- if (type === "tag" &&
30
- options.tags.find((tag) => code === tag || code.startsWith(tag + " "))) {
31
- // Remove leading horizontal space
32
- previous[1] = previous[1].replace(/(^|\n)[ \t]*$/, "$1");
33
- // Remove trailing horizontal space + newline
34
- if (next) {
35
- next[1] = next[1].replace(/^[ \t]*(?:\r\n|\n)/, "");
36
- }
28
+ let needsTrim = false;
29
+ if (type === "comment") {
30
+ needsTrim = true;
31
+ }
32
+ else if (type === "tag") {
33
+ needsTrim = options.tags.some((tag) => {
34
+ if (!code.startsWith(tag))
35
+ return false;
36
+ return /\s/.test(code[tag.length] ?? " ");
37
+ });
37
38
  }
38
- else if (type === "comment") {
39
- previous[1] = previous[1].replace(/(?:\r\n|\n)[ \t]*$/, "");
39
+ if (!needsTrim)
40
+ continue;
41
+ // Remove leading horizontal space
42
+ const previous = tokens[i - 1];
43
+ previous[1] = previous[1].replace(LEADING_WHITESPACE, "$1");
44
+ // Skip "filter" tokens to find the next "string" token
45
+ for (let j = i + 1; j < tokens.length; j++) {
46
+ if (tokens[j][0] === "filter")
47
+ continue;
48
+ if (tokens[j][0] !== "string")
49
+ break;
50
+ // Remove trailing horizontal space + newline
51
+ const next = tokens[j];
52
+ next[1] = next[1].replace(TRAILING_WHITESPACE, "");
53
+ break;
40
54
  }
41
55
  }
42
56
  }
package/plugins/echo.js CHANGED
@@ -14,14 +14,15 @@ function echoTag(env, [, code], output, tokens) {
14
14
  return `${output} += ${compiled};`;
15
15
  }
16
16
  // Captured echo, e.g. {{ echo |> toUpperCase }} foo {{ /echo }}
17
- const compiled = [`let __tmp = "";`];
18
- const filters = env.compileFilters(tokens, "__tmp");
19
- compiled.push(...env.compileTokens(tokens, "__tmp", "/echo"));
20
- if (filters != "__tmp") {
21
- compiled.push(`__tmp = ${filters}`);
17
+ const tmp = env.getTempVariable();
18
+ const compiled = [`let ${tmp} = "";`];
19
+ const filters = env.compileFilters(tokens, tmp);
20
+ compiled.push(...env.compileTokens(tokens, tmp, "/echo"));
21
+ if (filters != tmp) {
22
+ compiled.push(`${tmp} = ${filters}`);
22
23
  }
23
24
  return `{
24
25
  ${compiled.join("\n")}
25
- ${output} += __tmp;
26
+ ${output} += ${tmp};
26
27
  }`;
27
28
  }
@@ -16,13 +16,14 @@ function functionTag(env, token, _output, tokens) {
16
16
  const [_, exp, as, name, args] = match;
17
17
  const compiled = [];
18
18
  compiled.push(`${as || ""} function ${name} ${args || "()"} {`);
19
- compiled.push(`let __output = "";`);
20
- const result = env.compileFilters(tokens, "__output");
19
+ const tmp = env.getTempVariable();
20
+ compiled.push(`let ${tmp} = "";`);
21
+ const result = env.compileFilters(tokens, tmp);
21
22
  if (exp) {
22
- compiled.push(...env.compileTokens(tokens, "__output", "/export"));
23
+ compiled.push(...env.compileTokens(tokens, tmp, "/export"));
23
24
  }
24
25
  else {
25
- compiled.push(...env.compileTokens(tokens, "__output", "/function"));
26
+ compiled.push(...env.compileTokens(tokens, tmp, "/function"));
26
27
  }
27
28
  compiled.push(`return __env.utils.safeString(${result});`);
28
29
  compiled.push(`}`);
package/plugins/import.js CHANGED
@@ -21,10 +21,11 @@ function importTag(env, token) {
21
21
  const variables = [];
22
22
  const [, identifiers, specifier] = match;
23
23
  const defaultImport = identifiers.match(DEFAULT_IMPORT);
24
+ const tmp = env.getTempVariable();
24
25
  if (defaultImport) {
25
26
  const [name] = defaultImport;
26
27
  variables.push(name);
27
- compiled.push(`${name} = __tmp;`);
28
+ compiled.push(`${name} = ${tmp};`);
28
29
  }
29
30
  else {
30
31
  const namedImports = identifiers.match(NAMED_IMPORTS);
@@ -46,7 +47,7 @@ function importTag(env, token) {
46
47
  throw new SourceError("Invalid named import", position);
47
48
  }
48
49
  });
49
- compiled.push(`({${chunks.join(",")}} = __tmp);`);
50
+ compiled.push(`({${chunks.join(",")}} = ${tmp});`);
50
51
  }
51
52
  else {
52
53
  throw new SourceError("Invalid import tag", position);
@@ -54,7 +55,7 @@ function importTag(env, token) {
54
55
  }
55
56
  const { dataVarname } = env.options;
56
57
  return `let ${variables.join(",")}; {
57
- let __tmp = await __env.run(${specifier}, {...${dataVarname}}, __template.path, ${position});
58
+ let ${tmp} = await __env.run(${specifier}, {...${dataVarname}}, __template.path, ${position});
58
59
  ${compiled.join("\n")}
59
60
  }`;
60
61
  }
@@ -27,12 +27,13 @@ function includeTag(env, token, output, tokens) {
27
27
  data = tagCode.slice(bracketIndex).trim();
28
28
  }
29
29
  const { dataVarname } = env.options;
30
+ const tmp = env.getTempVariable();
30
31
  return `{
31
- const __tmp = await __env.run(${file},
32
+ const ${tmp} = await __env.run(${file},
32
33
  {...${dataVarname}${data ? `, ...${data}` : ""}},
33
34
  __template.path,
34
35
  ${position}
35
36
  );
36
- ${output} += ${env.compileFilters(tokens, "__tmp.content")};
37
+ ${output} += ${env.compileFilters(tokens, `${tmp}.content`)};
37
38
  }`;
38
39
  }
package/plugins/layout.js CHANGED
@@ -39,10 +39,11 @@ function slotTag(env, token, _output, tokens) {
39
39
  if (!SLOT_NAME.test(name)) {
40
40
  throw new SourceError(`Invalid slot name "${name}"`, position);
41
41
  }
42
- const compiledFilters = env.compileFilters(tokens, "__tmp");
42
+ const tmp = env.getTempVariable();
43
+ const compiledFilters = env.compileFilters(tokens, tmp);
43
44
  return `{
44
- let __tmp = '';
45
- ${env.compileTokens(tokens, "__tmp", "/slot").join("\n")}
45
+ let ${tmp} = '';
46
+ ${env.compileTokens(tokens, tmp, "/slot").join("\n")}
46
47
  __slots.${name} ??= '';
47
48
  __slots.${name} += ${compiledFilters};
48
49
  __slots.${name} = __env.utils.safeString(__slots.${name});
package/plugins/set.js CHANGED
@@ -4,6 +4,9 @@ export default function () {
4
4
  env.tags.push(setTag);
5
5
  };
6
6
  }
7
+ const VARNAME = /^[a-zA-Z_$][\w$]*$/;
8
+ const DETECTED_VARS = /([a-zA-Z_$][\w$]*)\b(?!\s*\:)/g;
9
+ const VALID_TAG = /^set\s+([\w{}[\]\s,:.$]+)\s*=\s*([\s\S]+)$/;
7
10
  function setTag(env, token, _output, tokens) {
8
11
  const [, code, position] = token;
9
12
  if (!code.startsWith("set ")) {
@@ -13,12 +16,25 @@ function setTag(env, token, _output, tokens) {
13
16
  const { dataVarname } = env.options;
14
17
  // Value is set (e.g. {{ set foo = "bar" }})
15
18
  if (expression.includes("=")) {
16
- const match = code.match(/^set\s+([\w]+)\s*=\s*([\s\S]+)$/);
19
+ const match = code.match(VALID_TAG);
17
20
  if (!match) {
18
21
  throw new SourceError("Invalid set tag", position);
19
22
  }
20
- const [, variable, value] = match;
23
+ const variable = match[1].trim();
24
+ const value = match[2].trim();
21
25
  const val = env.compileFilters(tokens, value);
26
+ if ((variable.startsWith("{") && variable.endsWith("}")) ||
27
+ (variable.startsWith("[") && variable.endsWith("]"))) {
28
+ const names = Array.from(variable.matchAll(DETECTED_VARS))
29
+ .map((n) => n[1]);
30
+ return `
31
+ var ${variable} = ${val};
32
+ Object.assign(${dataVarname}, { ${names.join(", ")} });
33
+ `;
34
+ }
35
+ if (!VARNAME.test(variable)) {
36
+ throw new SourceError("Invalid variable name", position);
37
+ }
22
38
  return `var ${variable} = ${dataVarname}["${variable}"] = ${val};`;
23
39
  }
24
40
  // Value is captured (eg: {{ set foo }}bar{{ /set }})
@@ -37,6 +37,7 @@ export interface Options {
37
37
  strict: boolean;
38
38
  }
39
39
  export declare class Environment {
40
+ #private;
40
41
  cache: Map<string, Template | Promise<Template>>;
41
42
  options: Options;
42
43
  tags: Tag[];
@@ -52,6 +53,7 @@ export declare class Environment {
52
53
  load(file: string, from?: string, position?: number): Promise<Template>;
53
54
  compileTokens(tokens: Token[], outputVar?: string, closeToken?: string, closeTokenOptional?: boolean): string[];
54
55
  compileFilters(tokens: Token[], output: string, autoescape?: boolean): string;
56
+ getTempVariable(): string;
55
57
  }
56
58
  export declare class SafeString extends String {
57
59
  }