ventojs 1.15.2 → 2.0.0-canary.1

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.
Files changed (192) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -0
  3. package/{esm/src → core}/environment.js +100 -52
  4. package/core/errors.js +182 -0
  5. package/{esm/src → core}/js.js +23 -15
  6. package/core/reserved.js +64 -0
  7. package/{esm/src → core}/tokenizer.js +11 -6
  8. package/highlightjs-vento.js +24 -0
  9. package/{esm/src/loader.js → loaders/file.js} +4 -1
  10. package/loaders/filesystem.js +36 -0
  11. package/loaders/memory.js +24 -0
  12. package/loaders/module.js +50 -0
  13. package/{esm/src/url_loader.js → loaders/url.js} +8 -5
  14. package/loaders/utils.js +17 -0
  15. package/{esm/bare.js → mod.js} +7 -2
  16. package/package.json +161 -56
  17. package/{esm/plugins → plugins}/auto_trim.js +0 -1
  18. package/{esm/plugins → plugins}/echo.js +2 -7
  19. package/plugins/escape.js +40 -0
  20. package/{esm/plugins → plugins}/export.js +5 -8
  21. package/{esm/plugins → plugins}/for.js +8 -8
  22. package/{esm/plugins → plugins}/function.js +7 -12
  23. package/{esm/plugins → plugins}/if.js +6 -6
  24. package/{esm/plugins → plugins}/import.js +7 -6
  25. package/{esm/plugins → plugins}/include.js +6 -5
  26. package/{esm/plugins → plugins}/js.js +1 -2
  27. package/{esm/plugins → plugins}/layout.js +6 -9
  28. package/plugins/mod.js +30 -0
  29. package/{esm/plugins → plugins}/set.js +9 -11
  30. package/{esm/plugins → plugins}/trim.js +0 -1
  31. package/{esm/plugins → plugins}/unescape.js +0 -1
  32. package/prism-vento.js +26 -0
  33. package/{esm/src → types/core}/environment.d.ts +15 -18
  34. package/types/core/errors.d.ts +38 -0
  35. package/types/core/js.d.ts +11 -0
  36. package/types/core/reserved.d.ts +2 -0
  37. package/{esm/src → types/core}/tokenizer.d.ts +1 -3
  38. package/{esm/src/loader.d.ts → types/loaders/file.d.ts} +5 -4
  39. package/types/loaders/filesystem.d.ts +12 -0
  40. package/types/loaders/memory.d.ts +11 -0
  41. package/types/loaders/module.d.ts +19 -0
  42. package/{esm/src/url_loader.d.ts → types/loaders/url.d.ts} +5 -4
  43. package/types/loaders/utils.d.ts +1 -0
  44. package/{esm/bare.d.ts → types/mod.d.ts} +1 -3
  45. package/{esm → types}/plugins/auto_trim.d.ts +2 -4
  46. package/types/plugins/echo.d.ts +2 -0
  47. package/types/plugins/escape.d.ts +2 -0
  48. package/types/plugins/export.d.ts +2 -0
  49. package/types/plugins/for.d.ts +2 -0
  50. package/types/plugins/function.d.ts +2 -0
  51. package/types/plugins/if.d.ts +2 -0
  52. package/types/plugins/import.d.ts +2 -0
  53. package/types/plugins/include.d.ts +2 -0
  54. package/types/plugins/js.d.ts +2 -0
  55. package/types/plugins/layout.d.ts +2 -0
  56. package/types/plugins/mod.d.ts +2 -0
  57. package/types/plugins/set.d.ts +2 -0
  58. package/types/plugins/trim.d.ts +4 -0
  59. package/types/plugins/unescape.d.ts +2 -0
  60. package/types/web.d.ts +8 -0
  61. package/web.js +19 -0
  62. package/esm/_dnt.polyfills.d.ts +0 -12
  63. package/esm/_dnt.polyfills.d.ts.map +0 -1
  64. package/esm/_dnt.polyfills.js +0 -15
  65. package/esm/_dnt.shims.d.ts +0 -2
  66. package/esm/_dnt.shims.d.ts.map +0 -1
  67. package/esm/_dnt.shims.js +0 -57
  68. package/esm/_dnt.test_polyfills.d.ts.map +0 -1
  69. package/esm/_dnt.test_shims.d.ts.map +0 -1
  70. package/esm/bare.d.ts.map +0 -1
  71. package/esm/deps/deno.land/std@0.224.0/assert/_constants.d.ts.map +0 -1
  72. package/esm/deps/deno.land/std@0.224.0/assert/assert_equals.d.ts.map +0 -1
  73. package/esm/deps/deno.land/std@0.224.0/assert/assert_is_error.d.ts.map +0 -1
  74. package/esm/deps/deno.land/std@0.224.0/assert/assert_throws.d.ts.map +0 -1
  75. package/esm/deps/deno.land/std@0.224.0/assert/assertion_error.d.ts.map +0 -1
  76. package/esm/deps/deno.land/std@0.224.0/assert/equal.d.ts.map +0 -1
  77. package/esm/deps/deno.land/std@0.224.0/collections/_utils.d.ts.map +0 -1
  78. package/esm/deps/deno.land/std@0.224.0/collections/deep_merge.d.ts.map +0 -1
  79. package/esm/deps/deno.land/std@0.224.0/fmt/colors.d.ts.map +0 -1
  80. package/esm/deps/deno.land/std@0.224.0/front_matter/_formats.d.ts.map +0 -1
  81. package/esm/deps/deno.land/std@0.224.0/front_matter/create_extractor.d.ts.map +0 -1
  82. package/esm/deps/deno.land/std@0.224.0/front_matter/json.d.ts.map +0 -1
  83. package/esm/deps/deno.land/std@0.224.0/front_matter/mod.d.ts.map +0 -1
  84. package/esm/deps/deno.land/std@0.224.0/front_matter/test.d.ts.map +0 -1
  85. package/esm/deps/deno.land/std@0.224.0/front_matter/toml.d.ts.map +0 -1
  86. package/esm/deps/deno.land/std@0.224.0/front_matter/yaml.d.ts.map +0 -1
  87. package/esm/deps/deno.land/std@0.224.0/internal/diff.d.ts.map +0 -1
  88. package/esm/deps/deno.land/std@0.224.0/internal/format.d.ts.map +0 -1
  89. package/esm/deps/deno.land/std@0.224.0/internal/mod.d.ts.map +0 -1
  90. package/esm/deps/deno.land/std@0.224.0/toml/_parser.d.ts.map +0 -1
  91. package/esm/deps/deno.land/std@0.224.0/toml/parse.d.ts.map +0 -1
  92. package/esm/deps/deno.land/std@0.224.0/yaml/_error.d.ts.map +0 -1
  93. package/esm/deps/deno.land/std@0.224.0/yaml/_loader/loader.d.ts.map +0 -1
  94. package/esm/deps/deno.land/std@0.224.0/yaml/_loader/loader_state.d.ts.map +0 -1
  95. package/esm/deps/deno.land/std@0.224.0/yaml/_mark.d.ts.map +0 -1
  96. package/esm/deps/deno.land/std@0.224.0/yaml/_state.d.ts.map +0 -1
  97. package/esm/deps/deno.land/std@0.224.0/yaml/_type/binary.d.ts.map +0 -1
  98. package/esm/deps/deno.land/std@0.224.0/yaml/_type/bool.d.ts.map +0 -1
  99. package/esm/deps/deno.land/std@0.224.0/yaml/_type/float.d.ts.map +0 -1
  100. package/esm/deps/deno.land/std@0.224.0/yaml/_type/function.d.ts.map +0 -1
  101. package/esm/deps/deno.land/std@0.224.0/yaml/_type/int.d.ts.map +0 -1
  102. package/esm/deps/deno.land/std@0.224.0/yaml/_type/map.d.ts.map +0 -1
  103. package/esm/deps/deno.land/std@0.224.0/yaml/_type/merge.d.ts.map +0 -1
  104. package/esm/deps/deno.land/std@0.224.0/yaml/_type/mod.d.ts.map +0 -1
  105. package/esm/deps/deno.land/std@0.224.0/yaml/_type/nil.d.ts.map +0 -1
  106. package/esm/deps/deno.land/std@0.224.0/yaml/_type/omap.d.ts.map +0 -1
  107. package/esm/deps/deno.land/std@0.224.0/yaml/_type/pairs.d.ts.map +0 -1
  108. package/esm/deps/deno.land/std@0.224.0/yaml/_type/regexp.d.ts.map +0 -1
  109. package/esm/deps/deno.land/std@0.224.0/yaml/_type/seq.d.ts.map +0 -1
  110. package/esm/deps/deno.land/std@0.224.0/yaml/_type/set.d.ts.map +0 -1
  111. package/esm/deps/deno.land/std@0.224.0/yaml/_type/str.d.ts.map +0 -1
  112. package/esm/deps/deno.land/std@0.224.0/yaml/_type/timestamp.d.ts.map +0 -1
  113. package/esm/deps/deno.land/std@0.224.0/yaml/_type/undefined.d.ts.map +0 -1
  114. package/esm/deps/deno.land/std@0.224.0/yaml/_utils.d.ts.map +0 -1
  115. package/esm/deps/deno.land/std@0.224.0/yaml/parse.d.ts.map +0 -1
  116. package/esm/deps/deno.land/std@0.224.0/yaml/schema/core.d.ts.map +0 -1
  117. package/esm/deps/deno.land/std@0.224.0/yaml/schema/default.d.ts.map +0 -1
  118. package/esm/deps/deno.land/std@0.224.0/yaml/schema/extended.d.ts.map +0 -1
  119. package/esm/deps/deno.land/std@0.224.0/yaml/schema/failsafe.d.ts.map +0 -1
  120. package/esm/deps/deno.land/std@0.224.0/yaml/schema/json.d.ts.map +0 -1
  121. package/esm/deps/deno.land/std@0.224.0/yaml/schema/mod.d.ts.map +0 -1
  122. package/esm/deps/deno.land/std@0.224.0/yaml/schema.d.ts.map +0 -1
  123. package/esm/deps/deno.land/std@0.224.0/yaml/type.d.ts.map +0 -1
  124. package/esm/deps/jsr.io/@davidbonnet/astring/1.8.6/src/astring.d.ts +0 -110
  125. package/esm/deps/jsr.io/@davidbonnet/astring/1.8.6/src/astring.d.ts.map +0 -1
  126. package/esm/deps/jsr.io/@davidbonnet/astring/1.8.6/src/astring.js +0 -1159
  127. package/esm/deps.d.ts +0 -5
  128. package/esm/deps.d.ts.map +0 -1
  129. package/esm/deps.js +0 -3
  130. package/esm/mod.d.ts +0 -9
  131. package/esm/mod.d.ts.map +0 -1
  132. package/esm/mod.js +0 -36
  133. package/esm/package.json +0 -3
  134. package/esm/plugins/auto_trim.d.ts.map +0 -1
  135. package/esm/plugins/echo.d.ts +0 -4
  136. package/esm/plugins/echo.d.ts.map +0 -1
  137. package/esm/plugins/escape.d.ts +0 -4
  138. package/esm/plugins/escape.d.ts.map +0 -1
  139. package/esm/plugins/escape.js +0 -18
  140. package/esm/plugins/export.d.ts +0 -4
  141. package/esm/plugins/export.d.ts.map +0 -1
  142. package/esm/plugins/for.d.ts +0 -4
  143. package/esm/plugins/for.d.ts.map +0 -1
  144. package/esm/plugins/function.d.ts +0 -4
  145. package/esm/plugins/function.d.ts.map +0 -1
  146. package/esm/plugins/if.d.ts +0 -4
  147. package/esm/plugins/if.d.ts.map +0 -1
  148. package/esm/plugins/import.d.ts +0 -4
  149. package/esm/plugins/import.d.ts.map +0 -1
  150. package/esm/plugins/include.d.ts +0 -4
  151. package/esm/plugins/include.d.ts.map +0 -1
  152. package/esm/plugins/js.d.ts +0 -4
  153. package/esm/plugins/js.d.ts.map +0 -1
  154. package/esm/plugins/layout.d.ts +0 -4
  155. package/esm/plugins/layout.d.ts.map +0 -1
  156. package/esm/plugins/set.d.ts +0 -4
  157. package/esm/plugins/set.d.ts.map +0 -1
  158. package/esm/plugins/trim.d.ts +0 -6
  159. package/esm/plugins/trim.d.ts.map +0 -1
  160. package/esm/plugins/unescape.d.ts +0 -4
  161. package/esm/plugins/unescape.d.ts.map +0 -1
  162. package/esm/src/environment.d.ts.map +0 -1
  163. package/esm/src/errors.d.ts +0 -22
  164. package/esm/src/errors.d.ts.map +0 -1
  165. package/esm/src/errors.js +0 -42
  166. package/esm/src/js.d.ts +0 -12
  167. package/esm/src/js.d.ts.map +0 -1
  168. package/esm/src/loader.d.ts.map +0 -1
  169. package/esm/src/tokenizer.d.ts.map +0 -1
  170. package/esm/src/transformer.d.ts +0 -3
  171. package/esm/src/transformer.d.ts.map +0 -1
  172. package/esm/src/transformer.js +0 -219
  173. package/esm/src/url_loader.d.ts.map +0 -1
  174. package/esm/test/auto_trim.test.d.ts.map +0 -1
  175. package/esm/test/comment.test.d.ts.map +0 -1
  176. package/esm/test/compile.test.d.ts.map +0 -1
  177. package/esm/test/echo.test.d.ts.map +0 -1
  178. package/esm/test/escape.test.d.ts.map +0 -1
  179. package/esm/test/for.test.d.ts.map +0 -1
  180. package/esm/test/function.test.d.ts.map +0 -1
  181. package/esm/test/if.test.d.ts.map +0 -1
  182. package/esm/test/import.test.d.ts.map +0 -1
  183. package/esm/test/include.test.d.ts.map +0 -1
  184. package/esm/test/js.test.d.ts.map +0 -1
  185. package/esm/test/layout.test.d.ts.map +0 -1
  186. package/esm/test/print.test.d.ts.map +0 -1
  187. package/esm/test/safe.test.d.ts.map +0 -1
  188. package/esm/test/set.test.d.ts.map +0 -1
  189. package/esm/test/tokenizer.test.d.ts.map +0 -1
  190. package/esm/test/unescape.test.d.ts.map +0 -1
  191. package/esm/test/utils.d.ts.map +0 -1
  192. package/esm/test/with.test.d.ts.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](http://keepachangelog.com/)
5
+ and this project adheres to [Semantic Versioning](http://semver.org/).
6
+
7
+ ## 2.0.0 - Unreleased
8
+ Vento 2.0 is now dependency-free and compatible with browsers without a build step.
9
+
10
+ ### Added
11
+ - Build-less browser support.
12
+ - `plugins/mod.ts` module to register all default plugins easily.
13
+ - Support for precompiled templates.
14
+ - New filesystem loader to use File System API.
15
+ - Better errors reporting [#131], [#137]
16
+
17
+ ### Changed
18
+ - Renamed `src` directory to `core`.
19
+ - Moved all loaders to the `loaders` root directory.
20
+ - Implemented a different approach to resolve the variables without using `meriyah` to analyze the code [#128].
21
+ - The signature of `tag` plugins has changed:
22
+ ```diff
23
+ -- (env: Environment, code: string, output: string, tokens: Tokens[])
24
+ ++ (env: Environment, token: Token, output: string, tokens: Tokens[])
25
+ ```
26
+ - The `compileTokens` function has changed. The third argument is a string with the closing tag and now it throws an error if its not found:
27
+ ```diff
28
+ -- env.compileTokens(tokens, tmpOutput, ["/code"]);
29
+ -- if (tokens.length && (tokens[0][0] !== "tag" || tokens[0][1] !== "/code")) {
30
+ -- throw new Error("missing closing tag");
31
+ -- }
32
+ ++ env.compileTokens(tokens, tmpOutput, "/code");
33
+ ```
34
+
35
+ ### Removed
36
+ - `runStringSync` function.
37
+ - Deprecated option `useWith`.
38
+ - All extenal dependencies (`meriyah`, `estree`, etc).
39
+ - `bare.ts` file since now it's useless.
40
+
41
+ ### Fixed
42
+ - Functions output when the autoescape is enabled [#95]
43
+ - Improved escape filter performance [#134]
44
+
45
+ [#95]: https://github.com/ventojs/vento/issues/95
46
+ [#128]: https://github.com/ventojs/vento/issues/128
47
+ [#131]: https://github.com/ventojs/vento/issues/131
48
+ [#134]: https://github.com/ventojs/vento/issues/134
49
+ [#137]: https://github.com/ventojs/vento/issues/137
package/README.md CHANGED
@@ -20,6 +20,8 @@ Nunjucks, Liquid, Mustache, and EJS.
20
20
  ## Features
21
21
 
22
22
  - Minimal, fast runtime. 🔥
23
+ - No dependencies.
24
+ - Compatible with browsers and JS runtimes (Deno, Node, Bun, etc).
23
25
  - Ergonomic by design. All tags and outputs are written with `{{` and `}}`.
24
26
  - Write JavaScript anywhere. `{{ await user.getName() }}` is real JS executed at
25
27
  runtime.
@@ -1,8 +1,6 @@
1
- import "../_dnt.polyfills.js";
2
- import * as dntShim from "../_dnt.shims.js";
1
+ import iterateTopLevel from "./js.js";
3
2
  import tokenize from "./tokenizer.js";
4
- import { transformTemplateCode } from "./transformer.js";
5
- import { TemplateError, TransformError } from "./errors.js";
3
+ import { createError, TokenError } from "./errors.js";
6
4
  export class Environment {
7
5
  cache = new Map();
8
6
  options;
@@ -11,6 +9,10 @@ export class Environment {
11
9
  filters = {};
12
10
  utils = {
13
11
  callMethod,
12
+ createError,
13
+ safeString(str) {
14
+ return new SafeString(str);
15
+ },
14
16
  };
15
17
  constructor(options) {
16
18
  this.options = options;
@@ -35,46 +37,73 @@ export class Environment {
35
37
  const template = this.compile(source, file);
36
38
  return await template(data);
37
39
  }
38
- runStringSync(source, data) {
39
- const template = this.compile(source, "", {}, true);
40
- return template(data);
41
- }
42
- compile(source, path, defaults, sync = false) {
40
+ compile(source, path, defaults) {
43
41
  if (typeof source !== "string") {
44
- throw new Error(`The source code of "${path}" must be a string. Got ${typeof source}`);
42
+ throw new TypeError(`The source code of "${path}" must be a string. Got ${typeof source}`);
43
+ }
44
+ const allTokens = this.tokenize(source, path);
45
+ const tokens = [...allTokens];
46
+ const lastToken = tokens.at(-1);
47
+ if (lastToken[0] != "string") {
48
+ throw new TokenError("Unclosed tag", lastToken, source, path);
49
+ }
50
+ let code = "";
51
+ try {
52
+ code = this.compileTokens(tokens).join("\n");
53
+ }
54
+ catch (error) {
55
+ if (!(error instanceof Error))
56
+ throw error;
57
+ throw createError(error, {
58
+ source,
59
+ code,
60
+ tokens: allTokens,
61
+ path,
62
+ });
45
63
  }
46
- const tokens = this.tokenize(source, path);
47
- let code = this.compileTokens(tokens).join("\n");
48
64
  const { dataVarname, autoDataVarname } = this.options;
49
65
  if (autoDataVarname) {
50
- try {
51
- code = transformTemplateCode(code, dataVarname);
52
- }
53
- catch (cause) {
54
- if (cause instanceof TransformError) {
55
- throw new TemplateError(path, source, cause.position, cause);
56
- }
57
- throw new Error(`Unknown error while transforming ${path}`, { cause });
66
+ const generator = iterateTopLevel(code);
67
+ const [, , variables] = generator.next().value;
68
+ while (!generator.next().done)
69
+ ;
70
+ variables.delete(dataVarname);
71
+ if (variables.size > 0) {
72
+ code = `
73
+ var {${[...variables].join(",")}} = ${dataVarname};
74
+ {\n${code}\n}
75
+ `;
58
76
  }
59
77
  }
60
- const constructor = new Function("__file", "__env", "__defaults", "__err", `return${sync ? "" : " async"} function (${dataVarname}) {
61
- let __pos = 0;
62
78
  try {
63
- ${dataVarname} = Object.assign({}, __defaults, ${dataVarname});
64
- const __exports = { content: "" };
65
- ${code}
66
- return __exports;
67
- } catch (cause) {
68
- const template = ${sync ? "" : "await"} __env.cache.get(__file);
69
- throw new __err(__file, template?.source, __pos, cause);
79
+ const constructor = new Function("__env", `return async function __template(${dataVarname}) {
80
+ try {
81
+ ${dataVarname} = Object.assign({}, __template.defaults, ${dataVarname});
82
+ const __exports = { content: "" };
83
+ ${code}
84
+ return __exports;
85
+ } catch (error) {
86
+ throw __env.utils.createError(error, __template);
87
+ }
88
+ }`);
89
+ const template = constructor(this);
90
+ template.path = path;
91
+ template.code = constructor.toString();
92
+ template.source = source;
93
+ template.tokens = allTokens;
94
+ template.defaults = defaults || {};
95
+ return template;
96
+ }
97
+ catch (error) {
98
+ if (!(error instanceof Error))
99
+ throw error;
100
+ throw createError(error, {
101
+ source,
102
+ code,
103
+ tokens: allTokens,
104
+ path,
105
+ });
70
106
  }
71
- }
72
- `);
73
- const template = constructor(path, this, defaults, TemplateError);
74
- template.file = path;
75
- template.code = code;
76
- template.source = source;
77
- return template;
78
107
  }
79
108
  tokenize(source, path) {
80
109
  let tokens = tokenize(source);
@@ -97,17 +126,27 @@ export class Environment {
97
126
  .split("?")[0]
98
127
  .split("#")[0];
99
128
  cached = this.options.loader.load(cleanPath)
100
- .then(({ source, data }) => this.compile(source, path, data));
129
+ .then((result) => {
130
+ if (typeof result === "function") {
131
+ return result(this);
132
+ }
133
+ const { source, data } = result;
134
+ return this.compile(source, path, data);
135
+ });
101
136
  this.cache.set(path, cached);
102
137
  return await cached;
103
138
  }
104
- compileTokens(tokens, outputVar = "__exports.content", stopAt) {
139
+ compileTokens(tokens, outputVar = "__exports.content", closeToken) {
105
140
  const compiled = [];
141
+ let openToken;
106
142
  tokens: while (tokens.length > 0) {
107
- if (stopAt && tokens[0][0] === "tag" && stopAt.includes(tokens[0][1])) {
108
- break;
143
+ const token = tokens.shift();
144
+ const [type, code, pos] = token;
145
+ openToken ??= token;
146
+ // We found the closing tag, so we stop compiling
147
+ if (closeToken && type === "tag" && closeToken === code) {
148
+ return compiled;
109
149
  }
110
- const [type, code, pos] = tokens.shift();
111
150
  if (type === "comment") {
112
151
  continue;
113
152
  }
@@ -118,9 +157,9 @@ export class Environment {
118
157
  continue;
119
158
  }
120
159
  if (type === "tag") {
121
- compiled.push(`__pos = ${pos};`);
160
+ compiled.push(`/*__pos:${pos}*/`);
122
161
  for (const tag of this.tags) {
123
- const compiledTag = tag(this, code, outputVar, tokens);
162
+ const compiledTag = tag(this, token, outputVar, tokens);
124
163
  if (typeof compiledTag === "string") {
125
164
  compiled.push(compiledTag);
126
165
  continue tokens;
@@ -131,17 +170,22 @@ export class Environment {
131
170
  compiled.push(`${outputVar} += (${expression}) ?? "";`);
132
171
  continue;
133
172
  }
134
- throw new Error(`Unknown token type "${type}"`);
173
+ throw new TokenError(`Unknown token type "${type}"`, token);
174
+ }
175
+ // If we reach here, it means we have an open token that wasn't closed
176
+ if (closeToken) {
177
+ throw new TokenError(`Missing closing tag ("${closeToken}" tag is expected)`, openToken);
135
178
  }
136
179
  return compiled;
137
180
  }
138
181
  compileFilters(tokens, output, autoescape = false) {
139
182
  let unescaped = false;
140
183
  while (tokens.length > 0 && tokens[0][0] === "filter") {
141
- const [, code] = tokens.shift();
184
+ const token = tokens.shift();
185
+ const [, code, position] = token;
142
186
  const match = code.match(/^(await\s+)?([\w.]+)(?:\((.*)\))?$/);
143
187
  if (!match) {
144
- throw new Error(`Invalid filter: ${code}`);
188
+ throw new TokenError(`Invalid filter: ${code}`, token);
145
189
  }
146
190
  const [_, isAsync, name, args] = match;
147
191
  if (!Object.hasOwn(this.filters, name)) {
@@ -154,7 +198,7 @@ export class Environment {
154
198
  }
155
199
  else {
156
200
  // It's a prototype's method (e.g. `String.toUpperCase()`)
157
- output = `${isAsync ? "await " : ""}__env.utils.callMethod(${output}, "${name}", ${args ? args : ""})`;
201
+ output = `${isAsync ? "await " : ""}__env.utils.callMethod(${position}, ${output}, "${name}", ${args ? args : ""})`;
158
202
  }
159
203
  }
160
204
  else {
@@ -171,26 +215,30 @@ export class Environment {
171
215
  }
172
216
  }
173
217
  function isGlobal(name) {
174
- // @ts-ignore TS doesn't know about globalThis
175
- if (Object.hasOwn(dntShim.dntGlobalThis, name)) {
218
+ if (name == "name")
219
+ return false;
220
+ if (Object.hasOwn(globalThis, name)) {
176
221
  return true;
177
222
  }
178
223
  if (name.includes(".")) {
179
224
  const [obj, prop] = name.split(".");
180
225
  // @ts-ignore TS doesn't know about globalThis
181
- return Object.hasOwn(dntShim.dntGlobalThis[obj], prop);
226
+ return Object.hasOwn(globalThis[obj], prop);
182
227
  }
183
228
  }
229
+ function callMethod(position,
184
230
  // deno-lint-ignore no-explicit-any
185
- function callMethod(thisObject, method, ...args) {
231
+ thisObject, method, ...args) {
186
232
  if (thisObject === null || thisObject === undefined) {
187
233
  return thisObject;
188
234
  }
189
235
  if (typeof thisObject[method] === "function") {
190
236
  return thisObject[method](...args);
191
237
  }
192
- throw new Error(`"${method}" is not a valid filter, global object or a method of a ${typeof thisObject} variable`);
238
+ throw new TokenError(`Method "${method}" is not a function of ${typeof thisObject} variable`, position);
193
239
  }
194
240
  function checkAsync(fn) {
195
241
  return fn.constructor?.name === "AsyncFunction";
196
242
  }
243
+ export class SafeString extends String {
244
+ }
package/core/errors.js ADDED
@@ -0,0 +1,182 @@
1
+ export class VentoError extends Error {
2
+ }
3
+ export class TokenError extends VentoError {
4
+ token;
5
+ source;
6
+ file;
7
+ constructor(message, token, source, file) {
8
+ super(message);
9
+ this.name = "TokenError";
10
+ this.token = token;
11
+ this.source = source;
12
+ this.file = file;
13
+ }
14
+ getContext() {
15
+ if (!this.source || this.token === undefined) {
16
+ return;
17
+ }
18
+ return {
19
+ type: this.name,
20
+ message: this.message,
21
+ source: this.source,
22
+ position: typeof this.token === "number" ? this.token : this.token[2],
23
+ file: this.file,
24
+ };
25
+ }
26
+ }
27
+ export class RuntimeError extends VentoError {
28
+ context;
29
+ constructor(error, context) {
30
+ super(error.message);
31
+ this.name = error.name || "JavaScriptError";
32
+ this.context = context;
33
+ this.cause = error;
34
+ }
35
+ getContext() {
36
+ if (this.cause instanceof SyntaxError) {
37
+ return parseSyntaxError(this.cause, this.context);
38
+ }
39
+ if (this.cause instanceof Error) {
40
+ return parseError(this.cause, this.context);
41
+ }
42
+ }
43
+ }
44
+ export function createError(error, context) {
45
+ if (error instanceof RuntimeError)
46
+ return error;
47
+ // If the error is a TokenError, we can enhance it with the context information
48
+ if (error instanceof TokenError) {
49
+ error.file ??= context.path;
50
+ error.source ??= context.source;
51
+ return error;
52
+ }
53
+ // JavaScript syntax errors can be parsed to get accurate position
54
+ return new RuntimeError(error, context);
55
+ }
56
+ export async function printError(error) {
57
+ if (error instanceof VentoError) {
58
+ const context = await error.getContext();
59
+ if (context) {
60
+ console.error(stringifyContext(context));
61
+ return;
62
+ }
63
+ }
64
+ console.error(error);
65
+ }
66
+ function parseError(error, context) {
67
+ const stackMatch = error.stack?.match(/<anonymous>:(\d+):(\d+)/);
68
+ if (!stackMatch)
69
+ return;
70
+ const row = Number(stackMatch[1]) - 1;
71
+ const col = Number(stackMatch[2]);
72
+ const position = getAccurateErrorPosition(row, col, context);
73
+ if (position == -1)
74
+ return;
75
+ return {
76
+ type: error.name || "JavaScriptError",
77
+ message: error.message,
78
+ source: context.source,
79
+ position,
80
+ file: context.path,
81
+ };
82
+ }
83
+ async function parseSyntaxError(error, context) {
84
+ const code = `()=>{${context.code}}`;
85
+ const dataUrl = "data:application/javascript;base64," + btoa(code);
86
+ const stack = await import(dataUrl).catch(({ stack }) => stack);
87
+ if (!stack)
88
+ return;
89
+ const stackMatch = stack?.match(/:(\d+):(\d+)$/m);
90
+ if (!stackMatch)
91
+ return;
92
+ const row = Number(stackMatch[1]) - 1;
93
+ const col = Number(stackMatch[2]);
94
+ const position = getAccurateErrorPosition(row, col, context);
95
+ if (position == -1)
96
+ return;
97
+ return {
98
+ type: "SyntaxError",
99
+ message: error.message,
100
+ source: context.source,
101
+ position,
102
+ file: context.path,
103
+ };
104
+ }
105
+ function getAccurateErrorPosition(row, col, context) {
106
+ const { code, tokens, source } = context;
107
+ if (!tokens)
108
+ return -1;
109
+ const linesAndDelims = code.split(/(\r\n?|[\n\u2028\u2029])/);
110
+ const linesAndDelimsUntilIssue = linesAndDelims.slice(0, row * 2);
111
+ const issueIndex = linesAndDelimsUntilIssue.join("").length + col;
112
+ const posLine = linesAndDelimsUntilIssue.findLast((line) => {
113
+ return /^\/\*__pos:(\d+)\*\/$/.test(line);
114
+ });
115
+ if (!posLine)
116
+ return -1;
117
+ const position = Number(posLine.slice(8, -2));
118
+ const token = tokens.findLast((token) => {
119
+ if (token[2] == undefined)
120
+ return false;
121
+ return token[2] <= position;
122
+ });
123
+ if (!token)
124
+ return -1;
125
+ const isJS = token[1].startsWith(">");
126
+ const tag = isJS ? token[1].slice(1).trimStart() : token[1];
127
+ const issueStartIndex = code.lastIndexOf(tag, issueIndex);
128
+ if (issueStartIndex == -1)
129
+ return -1;
130
+ const sourceIssueStartIndex = source.indexOf(tag, position);
131
+ return sourceIssueStartIndex + issueIndex - issueStartIndex - 1;
132
+ }
133
+ const terminal = {
134
+ number: (n) => `\x1b[33m${n}\x1b[39m`,
135
+ dim: (line) => `\x1b[2m${line}\x1b[22m`,
136
+ error: (msg) => `\x1b[31m${msg}\x1b[39m`,
137
+ };
138
+ const LINE_TERMINATOR = /\r\n?|[\n\u2028\u2029]/;
139
+ export function stringifyContext(context, format = terminal) {
140
+ const { type, message, source, position, file } = context;
141
+ const sourceAfterIssue = source.slice(position);
142
+ const newlineMatch = sourceAfterIssue.match(LINE_TERMINATOR);
143
+ const endIndex = position + (newlineMatch?.index ?? sourceAfterIssue.length);
144
+ const lines = source.slice(0, endIndex).split(LINE_TERMINATOR);
145
+ const displayedLineEntries = [...lines.entries()].slice(-3);
146
+ const endLineIndex = lines.at(-1).length + position - endIndex;
147
+ const numberLength = (displayedLineEntries.at(-1)[0] + 1).toString().length;
148
+ const displayedCode = displayedLineEntries.map(([index, line]) => {
149
+ const number = `${index + 1}`.padStart(numberLength);
150
+ const sidebar = ` ${format.number(number)} ${format.dim("|")} `;
151
+ return sidebar + line;
152
+ }).join("\n");
153
+ const sidebarWidth = numberLength + 4;
154
+ const tooltipIndex = sidebarWidth + endLineIndex;
155
+ const tooltipIndent = " ".repeat(tooltipIndex);
156
+ const tooltip = tooltipIndent + format.error(`^ ${message}`);
157
+ const output = [];
158
+ output.push(`${format.error(type)}: ${message}`);
159
+ if (file) {
160
+ output.push(format.dim(getLocation(file, source, position)), "");
161
+ }
162
+ output.push(displayedCode, tooltip);
163
+ return output.join("\n");
164
+ }
165
+ function getLocation(file, source, position) {
166
+ let line = 1;
167
+ let column = 1;
168
+ for (let index = 0; index < position; index++) {
169
+ if (source[index] === "\n" ||
170
+ (source[index] === "\r" && source[index + 1] === "\n")) {
171
+ line++;
172
+ column = 1;
173
+ if (source[index] === "\r") {
174
+ index++;
175
+ }
176
+ }
177
+ else {
178
+ column++;
179
+ }
180
+ }
181
+ return `${file}:${line}:${column}`;
182
+ }
@@ -1,17 +1,19 @@
1
- import "../_dnt.polyfills.js";
1
+ import reserved from "./reserved.js";
2
2
  const TEMPLATE_PART = /[`}](?:\\?[^])*?(?:`|\${)/y;
3
3
  const REGEX_LITERAL_START = /(?<=[(=:,?&!]\s*)\//y;
4
- const STOPPING_POINT = /['"`{}[\]/|]/g;
4
+ const STOPPING_POINT = /['"`{}[\]/|]|((?<!\.\??)\b[a-zA-Z_]\w+)/g;
5
5
  /**
6
- * This function iterates over the top-level scope of a JavaScript source code string.
7
- * It yields pairs of the index and the type of each top-level element found.
6
+ * This function iterates over the top-level scope of a JavaScript source code
7
+ * string. It yields pairs of the index and the type of each top-level element
8
+ * found.
8
9
  *
9
10
  * @example `{ foo: { bar: 1 } }` will yield:
10
- * - [0, "{"] for the opening brace
11
- * - [18, "}"] for the closing brace
12
- * - [18, ""] for the end of the string
11
+ * - [0, "{", Set[]] for the first opening brace
12
+ * - [18, "}", Set['foo', 'bar']] for the _second_ closing brace
13
+ * - [18, "", Set['foo', 'bar']] for the end of the string
13
14
  */
14
15
  export default function* iterateTopLevel(source, start = 0) {
16
+ const variables = new Set();
15
17
  let cursor = start;
16
18
  let depth = -1;
17
19
  const brackets = [];
@@ -25,7 +27,13 @@ export default function* iterateTopLevel(source, start = 0) {
25
27
  break parsing;
26
28
  }
27
29
  cursor = match.index;
28
- const [stop] = match;
30
+ const [stop, variable] = match;
31
+ if (variable) {
32
+ cursor += variable.length;
33
+ if (!reserved.has(variable))
34
+ variables.add(variable);
35
+ continue;
36
+ }
29
37
  // Check the type of the stopping point.
30
38
  switch (stop) {
31
39
  case "|": {
@@ -33,7 +41,7 @@ export default function* iterateTopLevel(source, start = 0) {
33
41
  // It's a pipe `|>` in the top-level scope
34
42
  if (depth < 0 && source[cursor] === ">") {
35
43
  cursor++;
36
- yield [cursor - 2, "|>"];
44
+ yield [cursor - 2, "|>", variables];
37
45
  }
38
46
  break;
39
47
  }
@@ -58,7 +66,7 @@ export default function* iterateTopLevel(source, start = 0) {
58
66
  case "{": {
59
67
  // It's an opening brace: yield if it's in the top-level scope.
60
68
  if (depth < 0)
61
- yield [cursor, "{"];
69
+ yield [cursor, "{", variables];
62
70
  cursor++;
63
71
  // Handle `{}`
64
72
  if (source[cursor] == "}")
@@ -71,7 +79,7 @@ export default function* iterateTopLevel(source, start = 0) {
71
79
  case "[": {
72
80
  // It's an opening brace: yield if it's in the top-level scope.
73
81
  if (depth < 0)
74
- yield [cursor, "["];
82
+ yield [cursor, "[", variables];
75
83
  cursor++;
76
84
  // Handle `[]`
77
85
  if (source[cursor] == "]")
@@ -87,7 +95,7 @@ export default function* iterateTopLevel(source, start = 0) {
87
95
  depth--;
88
96
  // Yield if it's in the top-level scope.
89
97
  if (depth < 0)
90
- yield [cursor, "]"];
98
+ yield [cursor, "]", variables];
91
99
  cursor++;
92
100
  break;
93
101
  }
@@ -97,13 +105,13 @@ export default function* iterateTopLevel(source, start = 0) {
97
105
  depth--;
98
106
  // Yield if it's in the top-level scope.
99
107
  if (depth < 0)
100
- yield [cursor, "}"];
108
+ yield [cursor, "}", variables];
101
109
  cursor++;
102
110
  break;
103
111
  }
104
112
  // If it doesn't match, but we're in the top-level scope, yield anyway.
105
113
  if (depth < 0) {
106
- yield [cursor, "}"];
114
+ yield [cursor, "}", variables];
107
115
  cursor++;
108
116
  break;
109
117
  }
@@ -179,5 +187,5 @@ export default function* iterateTopLevel(source, start = 0) {
179
187
  }
180
188
  }
181
189
  }
182
- return [max, ""];
190
+ return [max, "", variables];
183
191
  }
@@ -0,0 +1,64 @@
1
+ const variables = new Set([
2
+ // Words reserved by Vento, used internally. In general, don't use variable
3
+ // names starting with two underscores to be future-proof and avoid clashes.
4
+ "__file",
5
+ "__template",
6
+ "__env",
7
+ "__defaults",
8
+ "__err",
9
+ "__exports",
10
+ "__pos",
11
+ "__tmp",
12
+ // JS reserved words, and some "dangerous" words like `let`, `async`, `of` or
13
+ // `undefined`, which aren't technically reserved but don't name your
14
+ // variables that.
15
+ "async",
16
+ "await",
17
+ "break",
18
+ "case",
19
+ "catch",
20
+ "class",
21
+ "const",
22
+ "continue",
23
+ "debugger",
24
+ "default",
25
+ "delete",
26
+ "do",
27
+ "else",
28
+ "enum",
29
+ "export",
30
+ "extends",
31
+ "false",
32
+ "finally",
33
+ "for",
34
+ "function",
35
+ "if",
36
+ "import",
37
+ "in",
38
+ "instanceof",
39
+ "let",
40
+ "new",
41
+ "null",
42
+ "of",
43
+ "return",
44
+ "super",
45
+ "switch",
46
+ "this",
47
+ "throw",
48
+ "true",
49
+ "try",
50
+ "typeof",
51
+ "undefined",
52
+ "var",
53
+ "void",
54
+ "while",
55
+ "with",
56
+ "yield",
57
+ // Variables that are already defined globally
58
+ ...Object.getOwnPropertyNames(globalThis),
59
+ ]);
60
+ // Remove `name` from the reserved variables
61
+ // because it's widely used in templates
62
+ // and it can cause issues if it's reserved.
63
+ variables.delete("name");
64
+ export default variables;