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.
- package/CHANGELOG.md +49 -0
- package/README.md +2 -0
- package/{esm/src → core}/environment.js +100 -52
- package/core/errors.js +182 -0
- package/{esm/src → core}/js.js +23 -15
- package/core/reserved.js +64 -0
- package/{esm/src → core}/tokenizer.js +11 -6
- package/highlightjs-vento.js +24 -0
- package/{esm/src/loader.js → loaders/file.js} +4 -1
- package/loaders/filesystem.js +36 -0
- package/loaders/memory.js +24 -0
- package/loaders/module.js +50 -0
- package/{esm/src/url_loader.js → loaders/url.js} +8 -5
- package/loaders/utils.js +17 -0
- package/{esm/bare.js → mod.js} +7 -2
- package/package.json +161 -56
- package/{esm/plugins → plugins}/auto_trim.js +0 -1
- package/{esm/plugins → plugins}/echo.js +2 -7
- package/plugins/escape.js +40 -0
- package/{esm/plugins → plugins}/export.js +5 -8
- package/{esm/plugins → plugins}/for.js +8 -8
- package/{esm/plugins → plugins}/function.js +7 -12
- package/{esm/plugins → plugins}/if.js +6 -6
- package/{esm/plugins → plugins}/import.js +7 -6
- package/{esm/plugins → plugins}/include.js +6 -5
- package/{esm/plugins → plugins}/js.js +1 -2
- package/{esm/plugins → plugins}/layout.js +6 -9
- package/plugins/mod.js +30 -0
- package/{esm/plugins → plugins}/set.js +9 -11
- package/{esm/plugins → plugins}/trim.js +0 -1
- package/{esm/plugins → plugins}/unescape.js +0 -1
- package/prism-vento.js +26 -0
- package/{esm/src → types/core}/environment.d.ts +15 -18
- package/types/core/errors.d.ts +38 -0
- package/types/core/js.d.ts +11 -0
- package/types/core/reserved.d.ts +2 -0
- package/{esm/src → types/core}/tokenizer.d.ts +1 -3
- package/{esm/src/loader.d.ts → types/loaders/file.d.ts} +5 -4
- package/types/loaders/filesystem.d.ts +12 -0
- package/types/loaders/memory.d.ts +11 -0
- package/types/loaders/module.d.ts +19 -0
- package/{esm/src/url_loader.d.ts → types/loaders/url.d.ts} +5 -4
- package/types/loaders/utils.d.ts +1 -0
- package/{esm/bare.d.ts → types/mod.d.ts} +1 -3
- package/{esm → types}/plugins/auto_trim.d.ts +2 -4
- package/types/plugins/echo.d.ts +2 -0
- package/types/plugins/escape.d.ts +2 -0
- package/types/plugins/export.d.ts +2 -0
- package/types/plugins/for.d.ts +2 -0
- package/types/plugins/function.d.ts +2 -0
- package/types/plugins/if.d.ts +2 -0
- package/types/plugins/import.d.ts +2 -0
- package/types/plugins/include.d.ts +2 -0
- package/types/plugins/js.d.ts +2 -0
- package/types/plugins/layout.d.ts +2 -0
- package/types/plugins/mod.d.ts +2 -0
- package/types/plugins/set.d.ts +2 -0
- package/types/plugins/trim.d.ts +4 -0
- package/types/plugins/unescape.d.ts +2 -0
- package/types/web.d.ts +8 -0
- package/web.js +19 -0
- package/esm/_dnt.polyfills.d.ts +0 -12
- package/esm/_dnt.polyfills.d.ts.map +0 -1
- package/esm/_dnt.polyfills.js +0 -15
- package/esm/_dnt.shims.d.ts +0 -2
- package/esm/_dnt.shims.d.ts.map +0 -1
- package/esm/_dnt.shims.js +0 -57
- package/esm/_dnt.test_polyfills.d.ts.map +0 -1
- package/esm/_dnt.test_shims.d.ts.map +0 -1
- package/esm/bare.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/assert/_constants.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/assert/assert_equals.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/assert/assert_is_error.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/assert/assert_throws.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/assert/assertion_error.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/assert/equal.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/collections/_utils.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/collections/deep_merge.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/fmt/colors.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/front_matter/_formats.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/front_matter/create_extractor.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/front_matter/json.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/front_matter/mod.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/front_matter/test.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/front_matter/toml.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/front_matter/yaml.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/internal/diff.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/internal/format.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/internal/mod.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/toml/_parser.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/toml/parse.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_error.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_loader/loader.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_loader/loader_state.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_mark.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_state.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/binary.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/bool.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/float.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/function.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/int.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/map.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/merge.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/mod.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/nil.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/omap.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/pairs.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/regexp.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/seq.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/set.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/str.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/timestamp.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_type/undefined.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/_utils.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/parse.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/schema/core.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/schema/default.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/schema/extended.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/schema/failsafe.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/schema/json.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/schema/mod.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/schema.d.ts.map +0 -1
- package/esm/deps/deno.land/std@0.224.0/yaml/type.d.ts.map +0 -1
- package/esm/deps/jsr.io/@davidbonnet/astring/1.8.6/src/astring.d.ts +0 -110
- package/esm/deps/jsr.io/@davidbonnet/astring/1.8.6/src/astring.d.ts.map +0 -1
- package/esm/deps/jsr.io/@davidbonnet/astring/1.8.6/src/astring.js +0 -1159
- package/esm/deps.d.ts +0 -5
- package/esm/deps.d.ts.map +0 -1
- package/esm/deps.js +0 -3
- package/esm/mod.d.ts +0 -9
- package/esm/mod.d.ts.map +0 -1
- package/esm/mod.js +0 -36
- package/esm/package.json +0 -3
- package/esm/plugins/auto_trim.d.ts.map +0 -1
- package/esm/plugins/echo.d.ts +0 -4
- package/esm/plugins/echo.d.ts.map +0 -1
- package/esm/plugins/escape.d.ts +0 -4
- package/esm/plugins/escape.d.ts.map +0 -1
- package/esm/plugins/escape.js +0 -18
- package/esm/plugins/export.d.ts +0 -4
- package/esm/plugins/export.d.ts.map +0 -1
- package/esm/plugins/for.d.ts +0 -4
- package/esm/plugins/for.d.ts.map +0 -1
- package/esm/plugins/function.d.ts +0 -4
- package/esm/plugins/function.d.ts.map +0 -1
- package/esm/plugins/if.d.ts +0 -4
- package/esm/plugins/if.d.ts.map +0 -1
- package/esm/plugins/import.d.ts +0 -4
- package/esm/plugins/import.d.ts.map +0 -1
- package/esm/plugins/include.d.ts +0 -4
- package/esm/plugins/include.d.ts.map +0 -1
- package/esm/plugins/js.d.ts +0 -4
- package/esm/plugins/js.d.ts.map +0 -1
- package/esm/plugins/layout.d.ts +0 -4
- package/esm/plugins/layout.d.ts.map +0 -1
- package/esm/plugins/set.d.ts +0 -4
- package/esm/plugins/set.d.ts.map +0 -1
- package/esm/plugins/trim.d.ts +0 -6
- package/esm/plugins/trim.d.ts.map +0 -1
- package/esm/plugins/unescape.d.ts +0 -4
- package/esm/plugins/unescape.d.ts.map +0 -1
- package/esm/src/environment.d.ts.map +0 -1
- package/esm/src/errors.d.ts +0 -22
- package/esm/src/errors.d.ts.map +0 -1
- package/esm/src/errors.js +0 -42
- package/esm/src/js.d.ts +0 -12
- package/esm/src/js.d.ts.map +0 -1
- package/esm/src/loader.d.ts.map +0 -1
- package/esm/src/tokenizer.d.ts.map +0 -1
- package/esm/src/transformer.d.ts +0 -3
- package/esm/src/transformer.d.ts.map +0 -1
- package/esm/src/transformer.js +0 -219
- package/esm/src/url_loader.d.ts.map +0 -1
- package/esm/test/auto_trim.test.d.ts.map +0 -1
- package/esm/test/comment.test.d.ts.map +0 -1
- package/esm/test/compile.test.d.ts.map +0 -1
- package/esm/test/echo.test.d.ts.map +0 -1
- package/esm/test/escape.test.d.ts.map +0 -1
- package/esm/test/for.test.d.ts.map +0 -1
- package/esm/test/function.test.d.ts.map +0 -1
- package/esm/test/if.test.d.ts.map +0 -1
- package/esm/test/import.test.d.ts.map +0 -1
- package/esm/test/include.test.d.ts.map +0 -1
- package/esm/test/js.test.d.ts.map +0 -1
- package/esm/test/layout.test.d.ts.map +0 -1
- package/esm/test/print.test.d.ts.map +0 -1
- package/esm/test/safe.test.d.ts.map +0 -1
- package/esm/test/set.test.d.ts.map +0 -1
- package/esm/test/tokenizer.test.d.ts.map +0 -1
- package/esm/test/unescape.test.d.ts.map +0 -1
- package/esm/test/utils.d.ts.map +0 -1
- 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 "
|
2
|
-
import * as dntShim from "../_dnt.shims.js";
|
1
|
+
import iterateTopLevel from "./js.js";
|
3
2
|
import tokenize from "./tokenizer.js";
|
4
|
-
import {
|
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
|
-
|
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
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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((
|
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",
|
139
|
+
compileTokens(tokens, outputVar = "__exports.content", closeToken) {
|
105
140
|
const compiled = [];
|
141
|
+
let openToken;
|
106
142
|
tokens: while (tokens.length > 0) {
|
107
|
-
|
108
|
-
|
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(
|
160
|
+
compiled.push(`/*__pos:${pos}*/`);
|
122
161
|
for (const tag of this.tags) {
|
123
|
-
const compiledTag = tag(this,
|
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
|
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
|
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
|
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
|
-
|
175
|
-
|
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(
|
226
|
+
return Object.hasOwn(globalThis[obj], prop);
|
182
227
|
}
|
183
228
|
}
|
229
|
+
function callMethod(position,
|
184
230
|
// deno-lint-ignore no-explicit-any
|
185
|
-
|
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
|
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
|
+
}
|
package/{esm/src → core}/js.js
RENAMED
@@ -1,17 +1,19 @@
|
|
1
|
-
import "
|
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
|
7
|
-
* It yields pairs of the index and the type of each top-level element
|
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, "{"]
|
11
|
-
* - [18, "}"]
|
12
|
-
* - [18, ""]
|
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
|
}
|
package/core/reserved.js
ADDED
@@ -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;
|