ventojs 2.0.0-canary.0 → 2.0.0-canary.2
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 +28 -2
- package/core/environment.js +97 -41
- package/core/errors.js +253 -24
- package/core/reserved.js +1 -0
- package/core/tokenizer.js +11 -5
- package/loaders/file.js +1 -1
- package/loaders/filesystem.js +36 -0
- package/loaders/memory.js +7 -7
- package/loaders/module.js +51 -0
- package/loaders/url.js +3 -9
- package/loaders/utils.js +17 -0
- package/package.json +26 -8
- package/plugins/auto_trim.js +2 -1
- package/plugins/echo.js +2 -6
- package/plugins/escape.js +32 -9
- package/plugins/export.js +5 -7
- package/plugins/for.js +10 -6
- package/plugins/function.js +8 -12
- package/plugins/if.js +6 -5
- package/plugins/import.js +7 -5
- package/plugins/include.js +6 -3
- package/plugins/js.js +1 -1
- package/plugins/layout.js +8 -9
- package/plugins/set.js +5 -7
- package/types/core/environment.d.ts +14 -16
- package/types/core/errors.d.ts +43 -13
- package/types/core/tokenizer.d.ts +1 -1
- package/types/loaders/file.d.ts +1 -1
- package/types/loaders/filesystem.d.ts +12 -0
- package/types/loaders/memory.d.ts +1 -1
- package/types/loaders/module.d.ts +19 -0
- package/types/loaders/utils.d.ts +1 -0
- /package/types/{browser.d.ts → web.d.ts} +0 -0
- /package/{browser.js → web.js} +0 -0
package/CHANGELOG.md
CHANGED
@@ -5,20 +5,46 @@ 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
7
|
## 2.0.0 - Unreleased
|
8
|
-
Vento 2.0 is now dependency-free and compatible with browsers without
|
8
|
+
Vento 2.0 is now dependency-free and compatible with browsers without a build step.
|
9
9
|
|
10
10
|
### Added
|
11
11
|
- Build-less browser support.
|
12
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
|
+
- `core/errors.ts` module to format errors.
|
13
17
|
|
14
18
|
### Changed
|
15
19
|
- Renamed `src` directory to `core`.
|
16
20
|
- Moved all loaders to the `loaders` root directory.
|
17
|
-
- Implemented a different approach to resolve the variables without using `meriyah` to analyze the code
|
21
|
+
- Implemented a different approach to resolve the variables without using `meriyah` to analyze the code [#128].
|
22
|
+
- The signature of `tag` plugins has changed:
|
23
|
+
```diff
|
24
|
+
-- (env: Environment, code: string, output: string, tokens: Tokens[])
|
25
|
+
++ (env: Environment, token: Token, output: string, tokens: Tokens[])
|
26
|
+
```
|
27
|
+
- 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:
|
28
|
+
```diff
|
29
|
+
-- env.compileTokens(tokens, tmpOutput, ["/code"]);
|
30
|
+
-- if (tokens.length && (tokens[0][0] !== "tag" || tokens[0][1] !== "/code")) {
|
31
|
+
-- throw new Error("missing closing tag");
|
32
|
+
-- }
|
33
|
+
++ env.compileTokens(tokens, tmpOutput, "/code");
|
34
|
+
```
|
18
35
|
|
19
36
|
### Removed
|
37
|
+
- `runStringSync` function.
|
20
38
|
- Deprecated option `useWith`.
|
21
39
|
- All extenal dependencies (`meriyah`, `estree`, etc).
|
22
40
|
- `bare.ts` file since now it's useless.
|
23
41
|
|
42
|
+
### Fixed
|
43
|
+
- Functions output when the autoescape is enabled [#95]
|
44
|
+
- Improved escape filter performance [#134]
|
45
|
+
|
46
|
+
[#95]: https://github.com/ventojs/vento/issues/95
|
24
47
|
[#128]: https://github.com/ventojs/vento/issues/128
|
48
|
+
[#131]: https://github.com/ventojs/vento/issues/131
|
49
|
+
[#134]: https://github.com/ventojs/vento/issues/134
|
50
|
+
[#137]: https://github.com/ventojs/vento/issues/137
|
package/core/environment.js
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import iterateTopLevel from "./js.js";
|
2
2
|
import tokenize from "./tokenizer.js";
|
3
|
-
import {
|
3
|
+
import { createError, SourceError } from "./errors.js";
|
4
4
|
export class Environment {
|
5
5
|
cache = new Map();
|
6
6
|
options;
|
@@ -9,6 +9,10 @@ export class Environment {
|
|
9
9
|
filters = {};
|
10
10
|
utils = {
|
11
11
|
callMethod,
|
12
|
+
createError,
|
13
|
+
safeString(str) {
|
14
|
+
return new SafeString(str);
|
15
|
+
},
|
12
16
|
};
|
13
17
|
constructor(options) {
|
14
18
|
this.options = options;
|
@@ -16,8 +20,8 @@ export class Environment {
|
|
16
20
|
use(plugin) {
|
17
21
|
plugin(this);
|
18
22
|
}
|
19
|
-
async run(file, data, from) {
|
20
|
-
const template = await this.load(file, from);
|
23
|
+
async run(file, data, from, position) {
|
24
|
+
const template = await this.load(file, from, position);
|
21
25
|
return await template(data);
|
22
26
|
}
|
23
27
|
async runString(source, data, file) {
|
@@ -33,16 +37,26 @@ export class Environment {
|
|
33
37
|
const template = this.compile(source, file);
|
34
38
|
return await template(data);
|
35
39
|
}
|
36
|
-
|
37
|
-
const template = this.compile(source, "", {}, true);
|
38
|
-
return template(data);
|
39
|
-
}
|
40
|
-
compile(source, path, defaults, sync = false) {
|
40
|
+
compile(source, path, defaults) {
|
41
41
|
if (typeof source !== "string") {
|
42
|
-
throw new
|
42
|
+
throw new TypeError(`The source code of "${path}" must be a string. Got ${typeof source}`);
|
43
43
|
}
|
44
44
|
const tokens = this.tokenize(source, path);
|
45
|
-
|
45
|
+
const lastToken = tokens.at(-1);
|
46
|
+
if (lastToken[0] != "string") {
|
47
|
+
throw new SourceError("Unclosed tag", lastToken[2], path, source);
|
48
|
+
}
|
49
|
+
let code = "";
|
50
|
+
try {
|
51
|
+
code = this.compileTokens(tokens).join("\n");
|
52
|
+
}
|
53
|
+
catch (error) {
|
54
|
+
if (error instanceof SourceError) {
|
55
|
+
error.file ??= path;
|
56
|
+
error.source ??= source;
|
57
|
+
}
|
58
|
+
throw error;
|
59
|
+
}
|
46
60
|
const { dataVarname, autoDataVarname } = this.options;
|
47
61
|
if (autoDataVarname) {
|
48
62
|
const generator = iterateTopLevel(code);
|
@@ -57,24 +71,35 @@ export class Environment {
|
|
57
71
|
`;
|
58
72
|
}
|
59
73
|
}
|
60
|
-
const constructor = new Function("__file", "__env", "__defaults", "__err", `return${sync ? "" : " async"} function (${dataVarname}) {
|
61
|
-
let __pos = 0;
|
62
74
|
try {
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
75
|
+
const constructor = new Function("__env", `return async function __template(${dataVarname}) {
|
76
|
+
let __pos=0;
|
77
|
+
try {
|
78
|
+
${dataVarname} = Object.assign({}, __template.defaults, ${dataVarname});
|
79
|
+
const __exports = { content: "" };
|
80
|
+
${code}
|
81
|
+
return __exports;
|
82
|
+
} catch (error) {
|
83
|
+
throw __env.utils.createError(error, __template, __pos);
|
84
|
+
}
|
85
|
+
}`);
|
86
|
+
const template = constructor(this);
|
87
|
+
template.path = path;
|
88
|
+
template.code = constructor.toString();
|
89
|
+
template.source = source;
|
90
|
+
template.defaults = defaults || {};
|
91
|
+
return template;
|
92
|
+
}
|
93
|
+
catch (error) {
|
94
|
+
if (error instanceof SyntaxError) {
|
95
|
+
throw createError(error, { source, code, path });
|
96
|
+
}
|
97
|
+
if (error instanceof SourceError) {
|
98
|
+
error.file ??= path;
|
99
|
+
error.source ??= source;
|
100
|
+
}
|
101
|
+
throw error;
|
102
|
+
}
|
78
103
|
}
|
79
104
|
tokenize(source, path) {
|
80
105
|
let tokens = tokenize(source);
|
@@ -86,7 +111,15 @@ export class Environment {
|
|
86
111
|
}
|
87
112
|
return tokens;
|
88
113
|
}
|
89
|
-
async load(file, from) {
|
114
|
+
async load(file, from, position) {
|
115
|
+
if (!file) {
|
116
|
+
throw position !== undefined
|
117
|
+
? new SourceError(`Invalid template (${typeof file})`, position, from)
|
118
|
+
: new Error(`Invalid template (${typeof file})`);
|
119
|
+
}
|
120
|
+
if (typeof file !== "string") {
|
121
|
+
throw new TypeError(`The template filename must be a string. Got ${typeof file}`);
|
122
|
+
}
|
90
123
|
const path = this.options.loader.resolve(from || "", file);
|
91
124
|
let cached = this.cache.get(path);
|
92
125
|
if (cached) {
|
@@ -97,17 +130,32 @@ export class Environment {
|
|
97
130
|
.split("?")[0]
|
98
131
|
.split("#")[0];
|
99
132
|
cached = this.options.loader.load(cleanPath)
|
100
|
-
.
|
133
|
+
.catch((error) => {
|
134
|
+
throw position !== undefined
|
135
|
+
? new SourceError(`Error loading template: ${error.message}`, position, from)
|
136
|
+
: error;
|
137
|
+
})
|
138
|
+
.then((result) => {
|
139
|
+
if (typeof result === "function") {
|
140
|
+
return result(this);
|
141
|
+
}
|
142
|
+
const { source, data } = result;
|
143
|
+
return this.compile(source, path, data);
|
144
|
+
});
|
101
145
|
this.cache.set(path, cached);
|
102
146
|
return await cached;
|
103
147
|
}
|
104
|
-
compileTokens(tokens, outputVar = "__exports.content",
|
148
|
+
compileTokens(tokens, outputVar = "__exports.content", closeToken) {
|
105
149
|
const compiled = [];
|
150
|
+
let openToken;
|
106
151
|
tokens: while (tokens.length > 0) {
|
107
|
-
|
108
|
-
|
152
|
+
const token = tokens.shift();
|
153
|
+
const [type, code, position] = token;
|
154
|
+
openToken ??= token;
|
155
|
+
// We found the closing tag, so we stop compiling
|
156
|
+
if (closeToken && type === "tag" && closeToken === code) {
|
157
|
+
return compiled;
|
109
158
|
}
|
110
|
-
const [type, code, pos] = tokens.shift();
|
111
159
|
if (type === "comment") {
|
112
160
|
continue;
|
113
161
|
}
|
@@ -118,9 +166,9 @@ export class Environment {
|
|
118
166
|
continue;
|
119
167
|
}
|
120
168
|
if (type === "tag") {
|
121
|
-
compiled.push(`__pos
|
169
|
+
compiled.push(`__pos=${position};`);
|
122
170
|
for (const tag of this.tags) {
|
123
|
-
const compiledTag = tag(this,
|
171
|
+
const compiledTag = tag(this, token, outputVar, tokens);
|
124
172
|
if (typeof compiledTag === "string") {
|
125
173
|
compiled.push(compiledTag);
|
126
174
|
continue tokens;
|
@@ -131,17 +179,22 @@ export class Environment {
|
|
131
179
|
compiled.push(`${outputVar} += (${expression}) ?? "";`);
|
132
180
|
continue;
|
133
181
|
}
|
134
|
-
throw new
|
182
|
+
throw new SourceError(`Unknown token type "${type}"`, position);
|
183
|
+
}
|
184
|
+
// If we reach here, it means we have an open token that wasn't closed
|
185
|
+
if (closeToken) {
|
186
|
+
throw new SourceError(`Missing closing tag ("${closeToken}" tag is expected)`, openToken[2]);
|
135
187
|
}
|
136
188
|
return compiled;
|
137
189
|
}
|
138
190
|
compileFilters(tokens, output, autoescape = false) {
|
139
191
|
let unescaped = false;
|
140
192
|
while (tokens.length > 0 && tokens[0][0] === "filter") {
|
141
|
-
const
|
193
|
+
const token = tokens.shift();
|
194
|
+
const [, code, position] = token;
|
142
195
|
const match = code.match(/^(await\s+)?([\w.]+)(?:\((.*)\))?$/);
|
143
196
|
if (!match) {
|
144
|
-
throw new
|
197
|
+
throw new SourceError(`Invalid filter: ${code}`, position);
|
145
198
|
}
|
146
199
|
const [_, isAsync, name, args] = match;
|
147
200
|
if (!Object.hasOwn(this.filters, name)) {
|
@@ -154,7 +207,7 @@ export class Environment {
|
|
154
207
|
}
|
155
208
|
else {
|
156
209
|
// It's a prototype's method (e.g. `String.toUpperCase()`)
|
157
|
-
output = `${isAsync ? "await " : ""}__env.utils.callMethod(${output}, "${name}", ${args ? args : ""})`;
|
210
|
+
output = `${isAsync ? "await " : ""}__env.utils.callMethod(${position}, ${output}, "${name}", ${args ? args : ""})`;
|
158
211
|
}
|
159
212
|
}
|
160
213
|
else {
|
@@ -182,16 +235,19 @@ function isGlobal(name) {
|
|
182
235
|
return Object.hasOwn(globalThis[obj], prop);
|
183
236
|
}
|
184
237
|
}
|
238
|
+
function callMethod(position,
|
185
239
|
// deno-lint-ignore no-explicit-any
|
186
|
-
|
240
|
+
thisObject, method, ...args) {
|
187
241
|
if (thisObject === null || thisObject === undefined) {
|
188
242
|
return thisObject;
|
189
243
|
}
|
190
244
|
if (typeof thisObject[method] === "function") {
|
191
245
|
return thisObject[method](...args);
|
192
246
|
}
|
193
|
-
throw new
|
247
|
+
throw new SourceError(`Method "${method}" is not a function of ${typeof thisObject} variable`, position);
|
194
248
|
}
|
195
249
|
function checkAsync(fn) {
|
196
250
|
return fn.constructor?.name === "AsyncFunction";
|
197
251
|
}
|
252
|
+
export class SafeString extends String {
|
253
|
+
}
|
package/core/errors.js
CHANGED
@@ -1,34 +1,263 @@
|
|
1
|
-
class
|
2
|
-
name = this.constructor.name;
|
1
|
+
export class VentoError extends Error {
|
3
2
|
}
|
4
|
-
export class
|
5
|
-
path;
|
6
|
-
source;
|
3
|
+
export class SourceError extends VentoError {
|
7
4
|
position;
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
this.
|
5
|
+
file;
|
6
|
+
source;
|
7
|
+
constructor(message, position, file, source) {
|
8
|
+
super(message);
|
9
|
+
this.name = "SourceError";
|
13
10
|
this.position = position;
|
11
|
+
this.file = file;
|
12
|
+
this.source = source;
|
13
|
+
}
|
14
|
+
getContext() {
|
15
|
+
return {
|
16
|
+
type: this.name,
|
17
|
+
message: this.message,
|
18
|
+
position: this.position,
|
19
|
+
file: this.file,
|
20
|
+
source: this.source,
|
21
|
+
};
|
14
22
|
}
|
15
23
|
}
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
export class RuntimeError extends VentoError {
|
25
|
+
#context;
|
26
|
+
position;
|
27
|
+
constructor(error, context, position) {
|
28
|
+
super(error.message);
|
29
|
+
this.name = error.name || "JavaScriptError";
|
30
|
+
this.#context = context;
|
31
|
+
this.cause = error;
|
32
|
+
this.position = position;
|
33
|
+
}
|
34
|
+
async getContext() {
|
35
|
+
const { code, source, path } = this.#context;
|
36
|
+
// If we don't have the position, we cannot provide a context
|
37
|
+
// Try to get the context from a SyntaxError
|
38
|
+
if (this.position === undefined) {
|
39
|
+
try {
|
40
|
+
return (await getSyntaxErrorContext(this.cause, this.#context)) ??
|
41
|
+
{
|
42
|
+
type: this.name || "JavaScriptError",
|
43
|
+
message: this.message,
|
44
|
+
source,
|
45
|
+
code,
|
46
|
+
file: path,
|
47
|
+
};
|
27
48
|
}
|
49
|
+
catch {
|
50
|
+
return {
|
51
|
+
type: this.name || "JavaScriptError",
|
52
|
+
message: this.message,
|
53
|
+
source,
|
54
|
+
code,
|
55
|
+
file: path,
|
56
|
+
};
|
57
|
+
}
|
58
|
+
}
|
59
|
+
// Capture the exact position of the error in the compiled code
|
60
|
+
for (const frame of getStackFrames(this.cause)) {
|
61
|
+
if (frame.file !== "<anonymous>") {
|
62
|
+
continue;
|
63
|
+
}
|
64
|
+
return {
|
65
|
+
type: this.name || "JavaScriptError",
|
66
|
+
message: this.message,
|
67
|
+
source,
|
68
|
+
position: this.position,
|
69
|
+
code,
|
70
|
+
line: frame.line,
|
71
|
+
column: frame.column,
|
72
|
+
file: path,
|
73
|
+
};
|
74
|
+
}
|
75
|
+
// As a fallback, return the error with the available context
|
76
|
+
return {
|
77
|
+
type: this.name || "JavaScriptError",
|
78
|
+
message: this.message,
|
79
|
+
source,
|
80
|
+
position: this.position,
|
81
|
+
code,
|
82
|
+
file: path,
|
83
|
+
};
|
84
|
+
}
|
85
|
+
}
|
86
|
+
/** Create or complete VentoError with extra info from the template */
|
87
|
+
export function createError(error, context, position) {
|
88
|
+
if (error instanceof RuntimeError)
|
89
|
+
return error;
|
90
|
+
// If the error is a SourceError, we can fill the missing context information
|
91
|
+
if (error instanceof SourceError) {
|
92
|
+
error.file ??= context.path;
|
93
|
+
error.source ??= context.source;
|
94
|
+
error.position ??= position;
|
95
|
+
return error;
|
96
|
+
}
|
97
|
+
// JavaScript syntax errors can be parsed to get accurate position
|
98
|
+
return new RuntimeError(error, context, position);
|
99
|
+
}
|
100
|
+
const colors = {
|
101
|
+
number: (n) => `\x1b[33m${n}\x1b[39m`,
|
102
|
+
dim: (line) => `\x1b[2m${line}\x1b[22m`,
|
103
|
+
error: (msg) => `\x1b[31m${msg}\x1b[39m`,
|
104
|
+
};
|
105
|
+
const plain = {
|
106
|
+
number: (n) => n,
|
107
|
+
dim: (line) => line,
|
108
|
+
error: (msg) => msg,
|
109
|
+
};
|
110
|
+
const formats = {
|
111
|
+
colors,
|
112
|
+
plain,
|
113
|
+
};
|
114
|
+
/** Prints an error to the console in a formatted way. */
|
115
|
+
export async function printError(error, format = plain) {
|
116
|
+
if (error instanceof VentoError) {
|
117
|
+
const context = await error.getContext();
|
118
|
+
const fmt = typeof format === "string" ? formats[format] || plain : format;
|
119
|
+
if (context) {
|
120
|
+
console.error(stringifyError(context, fmt));
|
121
|
+
return;
|
122
|
+
}
|
123
|
+
}
|
124
|
+
console.error(error);
|
125
|
+
}
|
126
|
+
/** Converts an error context into a formatted string representation. */
|
127
|
+
export function stringifyError(context, format = plain) {
|
128
|
+
const { type, message, source, position, code, line, column, file } = context;
|
129
|
+
const output = [];
|
130
|
+
// Print error type and message
|
131
|
+
output.push(`${format.error(type)}: ${message}`);
|
132
|
+
// If we don't know the position, we cannot print the source code
|
133
|
+
if (position === undefined || source === undefined) {
|
134
|
+
if (file) {
|
135
|
+
output.push(format.dim(file));
|
28
136
|
}
|
29
|
-
|
30
|
-
|
137
|
+
return output.join("\n");
|
138
|
+
}
|
139
|
+
const sourceLines = codeToLines(source);
|
140
|
+
const [sourceLine, sourceColumn] = getSourceLineColumn(sourceLines, position);
|
141
|
+
// Print file location if available
|
142
|
+
if (file) {
|
143
|
+
output.push(format.dim(`${file}:${sourceLine}:${sourceColumn}`));
|
144
|
+
}
|
145
|
+
const pad = sourceLine.toString().length;
|
146
|
+
// Print the latest lines of the source code before the error
|
147
|
+
for (let line = Math.max(sourceLine - 3, 1); line <= sourceLine; line++) {
|
148
|
+
const sidebar = ` ${format.number(`${line}`.padStart(pad))} ${format.dim("|")} `;
|
149
|
+
output.push(sidebar + sourceLines[line - 1].trimEnd());
|
150
|
+
}
|
151
|
+
// If we don't have the compiled code, return the tag position
|
152
|
+
const indent = ` ${" ".repeat(pad)} ${format.dim("|")}`;
|
153
|
+
// If we don't have the compiled code, print the tag position
|
154
|
+
if (!code || line === undefined || column === undefined) {
|
155
|
+
output.push(`${indent} ${" ".repeat(sourceColumn - 1)}${format.error(`^ ${message}`)}`);
|
156
|
+
return output.join("\n");
|
157
|
+
}
|
158
|
+
// Print the compiled code with the error position
|
159
|
+
const codeLines = codeToLines(code);
|
160
|
+
output.push(`${indent} ${" ".repeat(sourceColumn - 1)}${format.error("^")}`);
|
161
|
+
output.push(`${indent} ${format.dim(codeLines[line - 1]?.trimEnd() || "")}`);
|
162
|
+
output.push(`${indent} ${" ".repeat(column)} ${format.error(`^ ${message}`)}`);
|
163
|
+
return output.join("\n");
|
164
|
+
}
|
165
|
+
/**
|
166
|
+
* Extracts the context from a SyntaxError
|
167
|
+
* It does not work on Node.js and Bun due to the lack of position information
|
168
|
+
* in the stack trace of a dynamic import error.
|
169
|
+
*/
|
170
|
+
async function getSyntaxErrorContext(error, context) {
|
171
|
+
const { source, code } = context;
|
172
|
+
const url = URL.createObjectURL(new Blob([code], { type: "application/javascript" }));
|
173
|
+
const err = await import(url).catch((e) => e);
|
174
|
+
URL.revokeObjectURL(url);
|
175
|
+
for (const frame of getStackFrames(err)) {
|
176
|
+
if (!frame.file.startsWith("blob:")) {
|
177
|
+
continue;
|
31
178
|
}
|
179
|
+
return {
|
180
|
+
type: "SyntaxError",
|
181
|
+
message: error.message,
|
182
|
+
source,
|
183
|
+
position: searchPosition(frame, code) ?? 0,
|
184
|
+
code,
|
185
|
+
line: frame.line,
|
186
|
+
column: frame.column,
|
187
|
+
file: context.path,
|
188
|
+
};
|
32
189
|
}
|
33
|
-
|
190
|
+
}
|
191
|
+
const LINE_TERMINATOR = /(\r\n?|[\n\u2028\u2029])/;
|
192
|
+
/** Convert the source code into an array of lines */
|
193
|
+
function codeToLines(code) {
|
194
|
+
const doubleLines = code.split(LINE_TERMINATOR);
|
195
|
+
const lines = [];
|
196
|
+
for (let i = 0; i < doubleLines.length; i += 2) {
|
197
|
+
lines.push(`${doubleLines[i]}${doubleLines[i + 1] ?? ""}`);
|
198
|
+
}
|
199
|
+
return lines;
|
200
|
+
}
|
201
|
+
const POSITION_VARIABLE = /^__pos=(\d+);$/;
|
202
|
+
/** Search the closest token to an error */
|
203
|
+
function searchPosition(frame, code) {
|
204
|
+
const posLine = codeToLines(code)
|
205
|
+
.slice(0, frame.line - 1)
|
206
|
+
.findLast((line) => POSITION_VARIABLE.test(line.trim()));
|
207
|
+
if (posLine) {
|
208
|
+
return Number(posLine.trim().slice(6, -1));
|
209
|
+
}
|
210
|
+
}
|
211
|
+
/** Get the line and column number of a position in the code */
|
212
|
+
function getSourceLineColumn(lines, position) {
|
213
|
+
if (position < 0) {
|
214
|
+
return [1, 1]; // Position is before the start of the source
|
215
|
+
}
|
216
|
+
let index = 0;
|
217
|
+
for (const [line, content] of lines.entries()) {
|
218
|
+
const length = content.length;
|
219
|
+
if (position < index + length) {
|
220
|
+
return [line + 1, position - index + 1];
|
221
|
+
}
|
222
|
+
index += content.length;
|
223
|
+
}
|
224
|
+
throw new Error(`Position ${position} is out of bounds for the provided source lines.`);
|
225
|
+
}
|
226
|
+
/** Returns every combination of file, line and column of an error stack */
|
227
|
+
// deno-lint-ignore no-explicit-any
|
228
|
+
function* getStackFrames(error) {
|
229
|
+
// Firefox specific
|
230
|
+
const { columnNumber, lineNumber, fileName } = error;
|
231
|
+
if (columnNumber !== undefined && lineNumber !== undefined && fileName) {
|
232
|
+
yield {
|
233
|
+
file: normalizeFile(fileName),
|
234
|
+
line: lineNumber,
|
235
|
+
column: columnNumber,
|
236
|
+
};
|
237
|
+
}
|
238
|
+
const { stack } = error;
|
239
|
+
if (!stack) {
|
240
|
+
return;
|
241
|
+
}
|
242
|
+
const matches = stack.matchAll(/([^(\s,]+):(\d+):(\d+)/g);
|
243
|
+
for (const match of matches) {
|
244
|
+
const [_, file, line, column] = match;
|
245
|
+
// Skip Node, Bun & Deno internal stack frames
|
246
|
+
if (file.startsWith("node:") || file.startsWith("ext:") || file === "native") {
|
247
|
+
continue;
|
248
|
+
}
|
249
|
+
yield {
|
250
|
+
file: normalizeFile(file),
|
251
|
+
line: Number(line),
|
252
|
+
column: Number(column),
|
253
|
+
};
|
254
|
+
}
|
255
|
+
}
|
256
|
+
function normalizeFile(file) {
|
257
|
+
if (!file)
|
258
|
+
return "<anonymous>";
|
259
|
+
// Firefox may return "Function" for anonymous functions
|
260
|
+
if (file === "Function")
|
261
|
+
return "<anonymous>";
|
262
|
+
return file;
|
34
263
|
}
|
package/core/reserved.js
CHANGED
package/core/tokenizer.js
CHANGED
@@ -8,12 +8,12 @@ export default function tokenize(source) {
|
|
8
8
|
const index = source.indexOf("{{");
|
9
9
|
const code = index === -1 ? source : source.slice(0, index);
|
10
10
|
tokens.push([type, code, position]);
|
11
|
-
if (index === -1) {
|
12
|
-
break;
|
13
|
-
}
|
14
11
|
position += index;
|
15
12
|
source = source.slice(index);
|
16
13
|
type = source.startsWith("{{#") ? "comment" : "tag";
|
14
|
+
if (index === -1) {
|
15
|
+
break;
|
16
|
+
}
|
17
17
|
continue;
|
18
18
|
}
|
19
19
|
if (type === "comment") {
|
@@ -42,9 +42,11 @@ export default function tokenize(source) {
|
|
42
42
|
return curr;
|
43
43
|
}
|
44
44
|
// Filters
|
45
|
-
tokens.push(["filter", code]);
|
45
|
+
tokens.push(["filter", code, position + prev]);
|
46
46
|
return curr;
|
47
47
|
});
|
48
|
+
if (indexes[lastIndex] == Infinity)
|
49
|
+
return tokens;
|
48
50
|
position += indexes[lastIndex];
|
49
51
|
source = source.slice(indexes[lastIndex]);
|
50
52
|
type = "string";
|
@@ -64,6 +66,9 @@ export default function tokenize(source) {
|
|
64
66
|
continue;
|
65
67
|
}
|
66
68
|
}
|
69
|
+
if (type == "string") {
|
70
|
+
tokens.push([type, "", position]);
|
71
|
+
}
|
67
72
|
return tokens;
|
68
73
|
}
|
69
74
|
/**
|
@@ -82,5 +87,6 @@ export function parseTag(source) {
|
|
82
87
|
indexes.push(index + 2);
|
83
88
|
return indexes;
|
84
89
|
}
|
85
|
-
|
90
|
+
indexes.push(Infinity);
|
91
|
+
return indexes;
|
86
92
|
}
|
package/loaders/file.js
CHANGED
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
|
3
3
|
import process from "node:process";
|
4
4
|
/**
|
5
5
|
* Vento file loader for loading templates from the file system.
|
6
|
-
* Used by
|
6
|
+
* Used by Node-like runtimes (Node, Deno, Bun, ...)
|
7
7
|
*/
|
8
8
|
export class FileLoader {
|
9
9
|
#root;
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { join } from "./utils.js";
|
2
|
+
/**
|
3
|
+
* Vento FileSystem API loader for loading templates.
|
4
|
+
* Used by browser environments.
|
5
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/File_System_API
|
6
|
+
*/
|
7
|
+
export class FileSystemLoader {
|
8
|
+
#handle;
|
9
|
+
constructor(handle) {
|
10
|
+
this.#handle = handle;
|
11
|
+
}
|
12
|
+
async load(file) {
|
13
|
+
const parts = file.split("/");
|
14
|
+
let currentHandle = this.#handle;
|
15
|
+
while (parts.length > 1) {
|
16
|
+
const part = parts.shift();
|
17
|
+
if (part) {
|
18
|
+
currentHandle = await currentHandle.getDirectoryHandle(part, {
|
19
|
+
create: false,
|
20
|
+
});
|
21
|
+
}
|
22
|
+
}
|
23
|
+
const entry = await currentHandle.getFileHandle(parts[0], {
|
24
|
+
create: false,
|
25
|
+
});
|
26
|
+
const fileHandle = await entry.getFile();
|
27
|
+
const source = await fileHandle.text();
|
28
|
+
return { source };
|
29
|
+
}
|
30
|
+
resolve(from, file) {
|
31
|
+
if (file.startsWith(".")) {
|
32
|
+
return join(from, "..", file);
|
33
|
+
}
|
34
|
+
return join(file);
|
35
|
+
}
|
36
|
+
}
|