ventojs 2.0.0-canary.0 → 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 +27 -2
- package/core/environment.js +85 -38
- package/core/errors.js +161 -13
- 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 +50 -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 +7 -6
- package/plugins/function.js +7 -11
- package/plugins/if.js +6 -5
- package/plugins/import.js +7 -5
- package/plugins/include.js +5 -3
- package/plugins/js.js +1 -1
- package/plugins/layout.js +6 -8
- package/plugins/set.js +5 -7
- package/types/core/environment.d.ts +13 -14
- package/types/core/errors.d.ts +35 -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,45 @@ 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]
|
13
16
|
|
14
17
|
### Changed
|
15
18
|
- Renamed `src` directory to `core`.
|
16
19
|
- Moved all loaders to the `loaders` root directory.
|
17
|
-
- Implemented a different approach to resolve the variables without using `meriyah` to analyze the code
|
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
|
+
```
|
18
34
|
|
19
35
|
### Removed
|
36
|
+
- `runStringSync` function.
|
20
37
|
- Deprecated option `useWith`.
|
21
38
|
- All extenal dependencies (`meriyah`, `estree`, etc).
|
22
39
|
- `bare.ts` file since now it's useless.
|
23
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
|
24
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/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, TokenError } 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;
|
@@ -33,16 +37,30 @@ 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
|
+
}
|
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
|
+
});
|
43
63
|
}
|
44
|
-
const tokens = this.tokenize(source, path);
|
45
|
-
let code = this.compileTokens(tokens).join("\n");
|
46
64
|
const { dataVarname, autoDataVarname } = this.options;
|
47
65
|
if (autoDataVarname) {
|
48
66
|
const generator = iterateTopLevel(code);
|
@@ -57,24 +75,35 @@ export class Environment {
|
|
57
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 {
|
@@ -182,16 +226,19 @@ function isGlobal(name) {
|
|
182
226
|
return Object.hasOwn(globalThis[obj], prop);
|
183
227
|
}
|
184
228
|
}
|
229
|
+
function callMethod(position,
|
185
230
|
// deno-lint-ignore no-explicit-any
|
186
|
-
|
231
|
+
thisObject, method, ...args) {
|
187
232
|
if (thisObject === null || thisObject === undefined) {
|
188
233
|
return thisObject;
|
189
234
|
}
|
190
235
|
if (typeof thisObject[method] === "function") {
|
191
236
|
return thisObject[method](...args);
|
192
237
|
}
|
193
|
-
throw new
|
238
|
+
throw new TokenError(`Method "${method}" is not a function of ${typeof thisObject} variable`, position);
|
194
239
|
}
|
195
240
|
function checkAsync(fn) {
|
196
241
|
return fn.constructor?.name === "AsyncFunction";
|
197
242
|
}
|
243
|
+
export class SafeString extends String {
|
244
|
+
}
|
package/core/errors.js
CHANGED
@@ -1,20 +1,168 @@
|
|
1
|
-
class
|
2
|
-
name = this.constructor.name;
|
1
|
+
export class VentoError extends Error {
|
3
2
|
}
|
4
|
-
export class
|
5
|
-
|
3
|
+
export class TokenError extends VentoError {
|
4
|
+
token;
|
6
5
|
source;
|
7
|
-
|
8
|
-
constructor(
|
9
|
-
|
10
|
-
|
11
|
-
this.
|
6
|
+
file;
|
7
|
+
constructor(message, token, source, file) {
|
8
|
+
super(message);
|
9
|
+
this.name = "TokenError";
|
10
|
+
this.token = token;
|
12
11
|
this.source = source;
|
13
|
-
this.
|
12
|
+
this.file = file;
|
14
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");
|
15
164
|
}
|
16
|
-
|
17
|
-
export function errorLine(source, position) {
|
165
|
+
function getLocation(file, source, position) {
|
18
166
|
let line = 1;
|
19
167
|
let column = 1;
|
20
168
|
for (let index = 0; index < position; index++) {
|
@@ -30,5 +178,5 @@ export function errorLine(source, position) {
|
|
30
178
|
column++;
|
31
179
|
}
|
32
180
|
}
|
33
|
-
return {
|
181
|
+
return `${file}:${line}:${column}`;
|
34
182
|
}
|
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
|
+
}
|
package/loaders/memory.js
CHANGED
@@ -1,24 +1,24 @@
|
|
1
|
-
import
|
1
|
+
import { join } from "./utils.js";
|
2
2
|
/**
|
3
3
|
* Vento loader for loading templates from an in-memory object.
|
4
4
|
* Used for testing or in-memory operations.
|
5
5
|
*/
|
6
6
|
export class MemoryLoader {
|
7
|
-
files
|
7
|
+
files;
|
8
8
|
constructor(files) {
|
9
|
-
this.files = files;
|
9
|
+
this.files = new Map(Object.entries(files));
|
10
10
|
}
|
11
11
|
load(file) {
|
12
|
-
|
12
|
+
const source = this.files.get(file);
|
13
|
+
if (source === undefined) {
|
13
14
|
throw new Error(`File not found: ${file}`);
|
14
15
|
}
|
15
|
-
const source = this.files[file];
|
16
16
|
return Promise.resolve({ source });
|
17
17
|
}
|
18
18
|
resolve(from, file) {
|
19
19
|
if (file.startsWith(".")) {
|
20
|
-
return
|
20
|
+
return join(from, "..", file);
|
21
21
|
}
|
22
|
-
return
|
22
|
+
return join(file);
|
23
23
|
}
|
24
24
|
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
/**
|
2
|
+
* Vento loader for loading templates from a ES modules.
|
3
|
+
* Used to load precompiled templates.
|
4
|
+
*/
|
5
|
+
export class ModuleLoader {
|
6
|
+
#root;
|
7
|
+
constructor(root) {
|
8
|
+
this.#root = root;
|
9
|
+
}
|
10
|
+
async load(file) {
|
11
|
+
const url = new URL(join(this.#root.pathname, file + ".js"), this.#root);
|
12
|
+
const module = await import(url.toString());
|
13
|
+
return module.default;
|
14
|
+
}
|
15
|
+
resolve(from, file) {
|
16
|
+
if (file.startsWith(".")) {
|
17
|
+
return join("/", dirname(from), file);
|
18
|
+
}
|
19
|
+
return join("/", file);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
function join(...parts) {
|
23
|
+
return parts.join("/").replace(/\/+/g, "/");
|
24
|
+
}
|
25
|
+
function dirname(path) {
|
26
|
+
const lastSlash = path.lastIndexOf("/");
|
27
|
+
return lastSlash === -1 ? "." : path.slice(0, lastSlash);
|
28
|
+
}
|
29
|
+
/**
|
30
|
+
* Exports a template as a string that can be used in an ES module.
|
31
|
+
* This is useful for precompiled templates.
|
32
|
+
*/
|
33
|
+
export function exportTemplate(template, options) {
|
34
|
+
if (!template.source) {
|
35
|
+
throw new Error("Template source is not defined");
|
36
|
+
}
|
37
|
+
const exportCode = `export default function (__env) {
|
38
|
+
${template.toString()};
|
39
|
+
|
40
|
+
${options?.source
|
41
|
+
? `__template.path = ${JSON.stringify(template.path)};
|
42
|
+
__template.code = ${JSON.stringify(template.code)};
|
43
|
+
__template.source = ${JSON.stringify(template.source)};`
|
44
|
+
: ""}
|
45
|
+
__template.defaults = ${JSON.stringify(template.defaults || {})};
|
46
|
+
|
47
|
+
return __template;
|
48
|
+
}`;
|
49
|
+
return exportCode;
|
50
|
+
}
|
package/loaders/url.js
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { join } from "./utils.js";
|
1
2
|
/**
|
2
3
|
* Vento URL loader for loading templates from a URL.
|
3
4
|
* Used by browser environments.
|
@@ -14,15 +15,8 @@ export class UrlLoader {
|
|
14
15
|
}
|
15
16
|
resolve(from, file) {
|
16
17
|
if (file.startsWith(".")) {
|
17
|
-
return join("
|
18
|
+
return join(from, "..", file);
|
18
19
|
}
|
19
|
-
return join(
|
20
|
+
return join(file);
|
20
21
|
}
|
21
22
|
}
|
22
|
-
function join(...parts) {
|
23
|
-
return parts.join("/").replace(/\/+/g, "/");
|
24
|
-
}
|
25
|
-
function dirname(path) {
|
26
|
-
const lastSlash = path.lastIndexOf("/");
|
27
|
-
return lastSlash === -1 ? "." : path.slice(0, lastSlash);
|
28
|
-
}
|
package/loaders/utils.js
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
// Adapted from https://gist.github.com/creationix/7435851
|
2
|
+
export function join(...paths) {
|
3
|
+
const parts = []
|
4
|
+
.concat(...paths.map((path) => path.split("/")))
|
5
|
+
.filter((part) => part && part !== ".");
|
6
|
+
const newParts = [];
|
7
|
+
for (const part of parts) {
|
8
|
+
if (part === "..") {
|
9
|
+
newParts.pop();
|
10
|
+
}
|
11
|
+
else {
|
12
|
+
newParts.push(part);
|
13
|
+
}
|
14
|
+
}
|
15
|
+
newParts.unshift(""); // Ensure always a leading slash
|
16
|
+
return newParts.join("/");
|
17
|
+
}
|