mutorjs 1.2.0 → 1.3.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/LICENSE +21 -0
- package/README.md +1615 -90
- package/dist/cli.cjs +1783 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/index.cjs +1349 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1326 -12
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +1479 -15
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +5 -4
- package/dist/server.d.ts +5 -4
- package/dist/server.js +1462 -15
- package/dist/server.js.map +1 -1
- package/package.json +4 -1
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,1783 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/bin/cli.ts
|
|
22
|
+
var cli_exports = {};
|
|
23
|
+
__export(cli_exports, {
|
|
24
|
+
handleBuildCommand: () => handleBuildCommand,
|
|
25
|
+
handleCompileCommand: () => handleCompileCommand,
|
|
26
|
+
handleRenderCommand: () => handleRenderCommand,
|
|
27
|
+
parseArgs: () => parseArgs,
|
|
28
|
+
safeParseJsonFile: () => safeParseJsonFile,
|
|
29
|
+
safeReadFile: () => safeReadFile,
|
|
30
|
+
safeWriteFile: () => safeWriteFile
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(cli_exports);
|
|
33
|
+
var import_node_fs2 = require("fs");
|
|
34
|
+
var import_node_process = require("process");
|
|
35
|
+
|
|
36
|
+
// package.json
|
|
37
|
+
var version = "1.3.1";
|
|
38
|
+
|
|
39
|
+
// src/core/mutor.server.ts
|
|
40
|
+
var import_node_fs = require("fs");
|
|
41
|
+
var import_promises = require("fs/promises");
|
|
42
|
+
var import_node_path2 = require("path");
|
|
43
|
+
|
|
44
|
+
// src/utils/construct-pointer.ts
|
|
45
|
+
function constructPointer(pos, offset) {
|
|
46
|
+
return `${" ".repeat(pos + offset + 1)}^`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/core/error.ts
|
|
50
|
+
var MutorError = class _MutorError extends Error {
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "MutorError";
|
|
54
|
+
Object.setPrototypeOf(this, _MutorError.prototype);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var MutorCompilerError = class _MutorCompilerError extends MutorError {
|
|
58
|
+
constructor(message, line, lineText, column, file) {
|
|
59
|
+
const gutterWidth = line.toString().length + 2;
|
|
60
|
+
let report = `${message}
|
|
61
|
+
|
|
62
|
+
`;
|
|
63
|
+
report += `at ${file}:${line}:${column + 1}
|
|
64
|
+
`;
|
|
65
|
+
if (line > 1) {
|
|
66
|
+
report += `${(line - 1).toString().padStart(gutterWidth - 2)} | ...
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
report += `${line} | ${lineText}
|
|
70
|
+
`;
|
|
71
|
+
report += constructPointer(column, gutterWidth);
|
|
72
|
+
super(report);
|
|
73
|
+
this.name = "MutorCompilerError";
|
|
74
|
+
Object.setPrototypeOf(this, _MutorCompilerError.prototype);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/core/constants.ts
|
|
79
|
+
var keywords = /* @__PURE__ */ new Set([
|
|
80
|
+
"for",
|
|
81
|
+
"if",
|
|
82
|
+
"else",
|
|
83
|
+
"true",
|
|
84
|
+
"false",
|
|
85
|
+
"null",
|
|
86
|
+
"undefined",
|
|
87
|
+
"end",
|
|
88
|
+
"in",
|
|
89
|
+
"of"
|
|
90
|
+
]);
|
|
91
|
+
var operators = /* @__PURE__ */ new Set([
|
|
92
|
+
"::",
|
|
93
|
+
// Namespace access
|
|
94
|
+
"||",
|
|
95
|
+
// Or
|
|
96
|
+
"??",
|
|
97
|
+
// Nullish coalesce
|
|
98
|
+
"&&",
|
|
99
|
+
// And
|
|
100
|
+
"**",
|
|
101
|
+
// Power
|
|
102
|
+
"^",
|
|
103
|
+
// Bitwise XOr
|
|
104
|
+
"|",
|
|
105
|
+
// Bitwise Or
|
|
106
|
+
"&",
|
|
107
|
+
// Bitwise And
|
|
108
|
+
"!",
|
|
109
|
+
// Not
|
|
110
|
+
"-",
|
|
111
|
+
// Minus
|
|
112
|
+
"%",
|
|
113
|
+
// Modulus
|
|
114
|
+
"+",
|
|
115
|
+
// Plus
|
|
116
|
+
"*",
|
|
117
|
+
// Times
|
|
118
|
+
"/",
|
|
119
|
+
// Divide
|
|
120
|
+
">",
|
|
121
|
+
// Greater than
|
|
122
|
+
"<",
|
|
123
|
+
// Less than
|
|
124
|
+
">=",
|
|
125
|
+
// Greater or equal
|
|
126
|
+
"<=",
|
|
127
|
+
// Less or equal
|
|
128
|
+
"==",
|
|
129
|
+
// Strict equal
|
|
130
|
+
"!=",
|
|
131
|
+
// Strict not equal
|
|
132
|
+
">>",
|
|
133
|
+
// Bitwise right shift
|
|
134
|
+
"<<",
|
|
135
|
+
// Bitwise left shift
|
|
136
|
+
".",
|
|
137
|
+
// Property acess
|
|
138
|
+
"?.",
|
|
139
|
+
// Optional property access
|
|
140
|
+
"(",
|
|
141
|
+
// Open parentheses
|
|
142
|
+
")",
|
|
143
|
+
// Close parentheses
|
|
144
|
+
"[",
|
|
145
|
+
// Square open parentheses
|
|
146
|
+
"]",
|
|
147
|
+
// Square close parentheses
|
|
148
|
+
",",
|
|
149
|
+
// Comma
|
|
150
|
+
":",
|
|
151
|
+
// Column
|
|
152
|
+
"?"
|
|
153
|
+
// Ternary operator
|
|
154
|
+
]);
|
|
155
|
+
var equalityOperators = /* @__PURE__ */ new Set(["==", "!="]);
|
|
156
|
+
var comparisonOperators = /* @__PURE__ */ new Set([">", "<", ">=", "<="]);
|
|
157
|
+
var additiveOperators = /* @__PURE__ */ new Set(["+", "-"]);
|
|
158
|
+
var multiplicativeOperators = /* @__PURE__ */ new Set(["*", "/", "%"]);
|
|
159
|
+
var propertyAccessOperators = /* @__PURE__ */ new Set([".", "?.", "[", "::"]);
|
|
160
|
+
var unaryOperators = /* @__PURE__ */ new Set(["-", "+", "!"]);
|
|
161
|
+
var ESCAPE_MAP = {
|
|
162
|
+
"&": "&",
|
|
163
|
+
"<": "<",
|
|
164
|
+
">": ">",
|
|
165
|
+
'"': """,
|
|
166
|
+
"'": "'"
|
|
167
|
+
};
|
|
168
|
+
var defaultConfig = {
|
|
169
|
+
build: {
|
|
170
|
+
include: /* @__PURE__ */ new Set([".html", ".txt"]),
|
|
171
|
+
exclude: /* @__PURE__ */ new Set(["node_modules", ".git"])
|
|
172
|
+
},
|
|
173
|
+
autoEscape: true,
|
|
174
|
+
allowedProps: /* @__PURE__ */ new Set(),
|
|
175
|
+
forbiddenProps: /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]),
|
|
176
|
+
allowFnCalls: false,
|
|
177
|
+
delimiters: {
|
|
178
|
+
closingTag: "}}",
|
|
179
|
+
openingTag: "{{",
|
|
180
|
+
openingTagEscape: "\\",
|
|
181
|
+
whitespaceTrim: "~",
|
|
182
|
+
commentTag: "#"
|
|
183
|
+
},
|
|
184
|
+
keepOpeningTagEscapeDelimiter: false,
|
|
185
|
+
cache: {
|
|
186
|
+
active: true,
|
|
187
|
+
maxSize: 50 * 1024 * 1024
|
|
188
|
+
// 50MB
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
var namespaces = {
|
|
192
|
+
JSON: {
|
|
193
|
+
stringify(value) {
|
|
194
|
+
try {
|
|
195
|
+
return JSON.stringify(value);
|
|
196
|
+
} catch {
|
|
197
|
+
throw new MutorError("JSON.stringify failed: invalid value");
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
parse(str) {
|
|
201
|
+
if (typeof str !== "string") {
|
|
202
|
+
throw new MutorError("JSON.parse expects a string");
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(str);
|
|
206
|
+
} catch {
|
|
207
|
+
throw new MutorError("JSON.parse failed: invalid JSON string");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
Object: {
|
|
212
|
+
keys(obj) {
|
|
213
|
+
if (!obj || typeof obj !== "object") {
|
|
214
|
+
throw new MutorError("Object.keys expects an object");
|
|
215
|
+
}
|
|
216
|
+
return Object.keys(obj);
|
|
217
|
+
},
|
|
218
|
+
values(obj) {
|
|
219
|
+
if (!obj || typeof obj !== "object") {
|
|
220
|
+
throw new MutorError("Object.values expects an object");
|
|
221
|
+
}
|
|
222
|
+
return Object.values(obj);
|
|
223
|
+
},
|
|
224
|
+
entries(obj) {
|
|
225
|
+
if (!obj || typeof obj !== "object") {
|
|
226
|
+
throw new MutorError("Object.entries expects an object");
|
|
227
|
+
}
|
|
228
|
+
return Object.entries(obj);
|
|
229
|
+
},
|
|
230
|
+
hasOwn(obj, key) {
|
|
231
|
+
if (!obj || typeof obj !== "object") {
|
|
232
|
+
throw new MutorError("Object.hasOwn expects an object");
|
|
233
|
+
}
|
|
234
|
+
return Object.hasOwn(obj, key);
|
|
235
|
+
},
|
|
236
|
+
freeze(obj) {
|
|
237
|
+
if (!obj || typeof obj !== "object") {
|
|
238
|
+
throw new MutorError("Object.freeze expects an object");
|
|
239
|
+
}
|
|
240
|
+
return Object.freeze(obj);
|
|
241
|
+
},
|
|
242
|
+
seal(obj) {
|
|
243
|
+
if (!obj || typeof obj !== "object") {
|
|
244
|
+
throw new MutorError("Object.seal expects an object");
|
|
245
|
+
}
|
|
246
|
+
return Object.seal(obj);
|
|
247
|
+
},
|
|
248
|
+
fromEntries(entries) {
|
|
249
|
+
if (!Array.isArray(entries)) {
|
|
250
|
+
throw new MutorError("Object.fromEntries expects an array");
|
|
251
|
+
}
|
|
252
|
+
return Object.fromEntries(entries);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
Array: {
|
|
256
|
+
isArray(value) {
|
|
257
|
+
return Array.isArray(value);
|
|
258
|
+
},
|
|
259
|
+
from(value) {
|
|
260
|
+
return Array.from(value);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
Number: {
|
|
264
|
+
isFinite(value) {
|
|
265
|
+
return Number.isFinite(value);
|
|
266
|
+
},
|
|
267
|
+
isNaN(value) {
|
|
268
|
+
return Number.isNaN(value);
|
|
269
|
+
},
|
|
270
|
+
parseInt(value, radix = 10) {
|
|
271
|
+
return Number.parseInt(value, radix);
|
|
272
|
+
},
|
|
273
|
+
parseFloat(value) {
|
|
274
|
+
return Number.parseFloat(value);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
String: {
|
|
278
|
+
fromCharCode(...args) {
|
|
279
|
+
return String.fromCharCode(...args);
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
Math: {
|
|
283
|
+
abs(x) {
|
|
284
|
+
return Math.abs(x);
|
|
285
|
+
},
|
|
286
|
+
floor(x) {
|
|
287
|
+
return Math.floor(x);
|
|
288
|
+
},
|
|
289
|
+
ceil(x) {
|
|
290
|
+
return Math.ceil(x);
|
|
291
|
+
},
|
|
292
|
+
round(x) {
|
|
293
|
+
return Math.round(x);
|
|
294
|
+
},
|
|
295
|
+
max(...args) {
|
|
296
|
+
return Math.max(...args);
|
|
297
|
+
},
|
|
298
|
+
min(...args) {
|
|
299
|
+
return Math.min(...args);
|
|
300
|
+
},
|
|
301
|
+
random() {
|
|
302
|
+
return Math.random();
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
Date: {
|
|
306
|
+
now() {
|
|
307
|
+
return Date.now();
|
|
308
|
+
},
|
|
309
|
+
parse(str) {
|
|
310
|
+
if (typeof str !== "string") {
|
|
311
|
+
throw new MutorError("Date.parse expects a string");
|
|
312
|
+
}
|
|
313
|
+
return Date.parse(str);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
Boolean: {
|
|
317
|
+
valueOf(value) {
|
|
318
|
+
return Boolean(value);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// src/utils/escape-fn.ts
|
|
324
|
+
function escapeFn(e) {
|
|
325
|
+
if (typeof e !== "string") return e;
|
|
326
|
+
return /[&<>"']/.test(e) ? e.replace(/[&<>"']/g, (char) => ESCAPE_MAP[char]) : e;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/utils/to-absolute-path.ts
|
|
330
|
+
var import_node_path = require("path");
|
|
331
|
+
function toAbsolutePath(basePath, ...relativePaths) {
|
|
332
|
+
const absoluteBase = (0, import_node_path.isAbsolute)(basePath) ? basePath : (0, import_node_path.resolve)(process.cwd(), basePath);
|
|
333
|
+
if (relativePaths.length) {
|
|
334
|
+
const baseDir = (0, import_node_path.dirname)(absoluteBase);
|
|
335
|
+
return (0, import_node_path.resolve)(baseDir, ...relativePaths);
|
|
336
|
+
}
|
|
337
|
+
return absoluteBase;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/utils/validate-computed-prop.ts
|
|
341
|
+
function validateComputedProp(r, allowedProps, forbiddenProps) {
|
|
342
|
+
if (forbiddenProps.has(r) && !allowedProps.has(r)) {
|
|
343
|
+
throw new MutorError(
|
|
344
|
+
`Forbidden property access. Access to this computed property "${r}" is forbidden.`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return r;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/utils/validate-context.ts
|
|
351
|
+
var OBJECT = "object";
|
|
352
|
+
var MUTOR_SAFE = /* @__PURE__ */ Symbol("__mutor_safe_context");
|
|
353
|
+
function validateContext(ctx) {
|
|
354
|
+
if (!ctx || typeof ctx !== OBJECT) {
|
|
355
|
+
return ctx;
|
|
356
|
+
}
|
|
357
|
+
if (MUTOR_SAFE in ctx) {
|
|
358
|
+
return ctx;
|
|
359
|
+
}
|
|
360
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
361
|
+
function walk(value, path = "") {
|
|
362
|
+
if (!value || typeof value !== OBJECT) return value;
|
|
363
|
+
if (seen.has(value)) return value;
|
|
364
|
+
seen.add(value);
|
|
365
|
+
const proto = Object.getPrototypeOf(value);
|
|
366
|
+
if (proto && proto !== Object.prototype && proto !== Array.prototype) {
|
|
367
|
+
throw new MutorError(`Unsafe prototype detected at ${path || "root"}`);
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(value)) {
|
|
370
|
+
for (let i = 0; i < value.length; i++) {
|
|
371
|
+
value[i] = walk(value[i], `${path}[${i}]`);
|
|
372
|
+
}
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
if (value instanceof Map) {
|
|
376
|
+
for (const [k, v] of value.entries()) {
|
|
377
|
+
if (typeof k === OBJECT) walk(k, `${path}.mapKey`);
|
|
378
|
+
value.set(k, walk(v, `${path}.mapValue`));
|
|
379
|
+
}
|
|
380
|
+
return value;
|
|
381
|
+
}
|
|
382
|
+
if (value instanceof Set) {
|
|
383
|
+
const next = /* @__PURE__ */ new Set();
|
|
384
|
+
for (const v of value.values()) {
|
|
385
|
+
next.add(walk(v, path));
|
|
386
|
+
}
|
|
387
|
+
value.clear();
|
|
388
|
+
for (const v of next) value.add(v);
|
|
389
|
+
return value;
|
|
390
|
+
}
|
|
391
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
392
|
+
for (const key of Object.keys(descriptors)) {
|
|
393
|
+
const desc = descriptors[key];
|
|
394
|
+
if (desc.get || desc.set) {
|
|
395
|
+
throw new MutorError(`Getter/setter not allowed: ${path}.${key}`);
|
|
396
|
+
}
|
|
397
|
+
const prop = value[key];
|
|
398
|
+
if (prop && typeof prop === OBJECT) {
|
|
399
|
+
value[key] = walk(prop, `${path}.${key}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
const safeData = walk(ctx);
|
|
405
|
+
if (safeData && typeof safeData === OBJECT) {
|
|
406
|
+
Object.defineProperty(safeData, MUTOR_SAFE, {
|
|
407
|
+
value: true,
|
|
408
|
+
enumerable: false,
|
|
409
|
+
writable: false,
|
|
410
|
+
configurable: false
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return safeData;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/utils/escape-raw-text.ts
|
|
417
|
+
function escapeRawText(text) {
|
|
418
|
+
return text.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/utils/get-line-and-column-nums.ts
|
|
422
|
+
function getLineAndColumnNumbers(str, idx) {
|
|
423
|
+
const lines = str.slice(0, idx).split("\n");
|
|
424
|
+
const line = lines.length;
|
|
425
|
+
const lineIndex = str.lastIndexOf("\n", idx - 1) + 1;
|
|
426
|
+
return { line, lineIndex };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/utils/get-line-snapshot.ts
|
|
430
|
+
function getLineSnapshot(str, lineIdx, idx) {
|
|
431
|
+
const nextNewlineIdx = str.indexOf("\n", lineIdx);
|
|
432
|
+
const line = str.slice(
|
|
433
|
+
lineIdx,
|
|
434
|
+
nextNewlineIdx === -1 ? void 0 : nextNewlineIdx
|
|
435
|
+
);
|
|
436
|
+
return { line, pos: idx - lineIdx };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/core/build.ts
|
|
440
|
+
function build(ast, context) {
|
|
441
|
+
const { scope, forbiddenProps, allowedProps } = context;
|
|
442
|
+
function prefixWithCtx(ident) {
|
|
443
|
+
return scope.includes(ident) ? ident : `ctx.${ident}`;
|
|
444
|
+
}
|
|
445
|
+
function buildExpr(expr) {
|
|
446
|
+
switch (expr.type) {
|
|
447
|
+
case 17 /* END */:
|
|
448
|
+
return "}";
|
|
449
|
+
case 5 /* NUMBER */:
|
|
450
|
+
return expr.value;
|
|
451
|
+
case 4 /* STRING */:
|
|
452
|
+
return `\`${/\$/.test(expr.value) ? expr.value.replaceAll("$", "\\$") : expr.value}\``;
|
|
453
|
+
case 10 /* BOOLEAN */:
|
|
454
|
+
return expr.true ? "true" : "false";
|
|
455
|
+
case 12 /* NULL */:
|
|
456
|
+
return "null";
|
|
457
|
+
case 11 /* UNDEFINED */:
|
|
458
|
+
return "undefined";
|
|
459
|
+
case 7 /* IDENT */:
|
|
460
|
+
return prefixWithCtx(expr.value);
|
|
461
|
+
case 9 /* GROUP */:
|
|
462
|
+
return `(${buildExpr(expr.expr)})`;
|
|
463
|
+
case 2 /* UNARY */:
|
|
464
|
+
return `${expr.operator}${buildExpr(expr.expr)}`;
|
|
465
|
+
case 0 /* BINARY */:
|
|
466
|
+
return `${buildExpr(expr.left)} ${expr.operator} ${buildExpr(expr.right)}`;
|
|
467
|
+
case 1 /* TERNARY */:
|
|
468
|
+
return `${buildExpr(expr.condition)} ? ${buildExpr(expr.left)} : ${buildExpr(expr.right)}`;
|
|
469
|
+
case 8 /* PROP_ACCESS */:
|
|
470
|
+
return buildPropAccess(expr);
|
|
471
|
+
case 3 /* CALL */:
|
|
472
|
+
return buildCall(expr);
|
|
473
|
+
case 6 /* NAMESPACE */:
|
|
474
|
+
return buildNamespace(expr);
|
|
475
|
+
case 13 /* FOR */:
|
|
476
|
+
return buildForLoop(expr);
|
|
477
|
+
case 16 /* ELSE */:
|
|
478
|
+
return "} else {";
|
|
479
|
+
case 14 /* IF */:
|
|
480
|
+
return buildIfBlock(expr);
|
|
481
|
+
case 15 /* ELSE_IF */:
|
|
482
|
+
return buildElseIfBlock(expr);
|
|
483
|
+
default:
|
|
484
|
+
throw new MutorError(
|
|
485
|
+
`Unsupported expression type: ${expr.type}`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function buildNamespace(expr) {
|
|
490
|
+
if (expr.left.type !== 7 /* IDENT */) {
|
|
491
|
+
throw {
|
|
492
|
+
message: "Invalid usage of namespace operator.",
|
|
493
|
+
pos: expr.pos
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return `namespaces.${expr.left.value}.${expr.right.value}`;
|
|
497
|
+
}
|
|
498
|
+
function buildPropAccess(expr) {
|
|
499
|
+
const left = buildExpr(expr.left);
|
|
500
|
+
if (expr.bracketNotation) {
|
|
501
|
+
const right = buildExpr(expr.right);
|
|
502
|
+
const optionalChain = expr.optional ? "?." : "";
|
|
503
|
+
return expr.right.type === 4 /* STRING */ || expr.right.type === 5 /* NUMBER */ ? `${left}${optionalChain}[${right}]` : `${left}${optionalChain}[validateComputedProps(${right}, allowedProps, forbiddenProps)]`;
|
|
504
|
+
} else {
|
|
505
|
+
const propName = expr.right.value;
|
|
506
|
+
const optionalChain = expr.optional ? "?." : ".";
|
|
507
|
+
if (forbiddenProps.has(propName) && !allowedProps.has(propName)) {
|
|
508
|
+
throw { message: "Forbidden property access.", pos: expr.right.pos };
|
|
509
|
+
}
|
|
510
|
+
return `${left}${optionalChain}${propName}`;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function buildCall(expr) {
|
|
514
|
+
const func = buildExpr(expr.expr);
|
|
515
|
+
const optionalChain = expr.optional ? "?." : "";
|
|
516
|
+
const args = expr.args.map((arg) => buildExpr(arg)).join(", ");
|
|
517
|
+
return `${func}${optionalChain}(${args})`;
|
|
518
|
+
}
|
|
519
|
+
function buildForLoop(expr) {
|
|
520
|
+
const { iterable, loopType, variable } = expr;
|
|
521
|
+
return `for(const ${variable} ${loopType === 1 /* IN */ ? "in" : "of"} ${build(iterable, context)}){`;
|
|
522
|
+
}
|
|
523
|
+
function buildIfBlock(expr) {
|
|
524
|
+
const { condition } = expr;
|
|
525
|
+
return `if(${build(condition, context)}){`;
|
|
526
|
+
}
|
|
527
|
+
function buildElseIfBlock(expr) {
|
|
528
|
+
const { condition } = expr;
|
|
529
|
+
return `}else if(${build(condition, context)}){`;
|
|
530
|
+
}
|
|
531
|
+
return buildExpr(ast);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/utils/get-token-type-words.ts
|
|
535
|
+
function getTokenTypeWords(type) {
|
|
536
|
+
switch (type) {
|
|
537
|
+
case 0 /* IDENT */:
|
|
538
|
+
return "identifier";
|
|
539
|
+
case 1 /* KEYWORD */:
|
|
540
|
+
return "keyword";
|
|
541
|
+
case 2 /* NUMBER */:
|
|
542
|
+
return "number";
|
|
543
|
+
case 4 /* OPERATOR */:
|
|
544
|
+
return "operator";
|
|
545
|
+
case 3 /* STRING */:
|
|
546
|
+
return "string";
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/core/generate-ast.ts
|
|
551
|
+
function generateAst(tokens, config) {
|
|
552
|
+
let cursor = 0, generatingNamespace = false;
|
|
553
|
+
function expectOrThrow(type, value) {
|
|
554
|
+
const token = tokens[cursor];
|
|
555
|
+
const lastToken = tokens[tokens.length - 1];
|
|
556
|
+
if (!token) {
|
|
557
|
+
throw {
|
|
558
|
+
message: `Unexpected end of expression. Expected ${value ? `'${value}'` : `${type === 0 /* IDENT */ ? "an" : "a"} ${getTokenTypeWords(type)}`}.`,
|
|
559
|
+
pos: lastToken.pos + lastToken.value.length - 1
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (token.type !== type) {
|
|
563
|
+
throw {
|
|
564
|
+
message: `Unexpected token type. Expected ${value ? `'${value}'` : getTokenTypeWords(type)} but got ${getTokenTypeWords(token.type)} instead.`,
|
|
565
|
+
pos: token.pos
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
if (value !== void 0 && token.value !== value) {
|
|
569
|
+
throw {
|
|
570
|
+
message: `Unexpected token '${token?.value}'. Expected ${type === 0 /* IDENT */ ? "an" : "a"} ${getTokenTypeWords(type)} instead.`,
|
|
571
|
+
pos: token.pos
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return tokens[cursor++];
|
|
575
|
+
}
|
|
576
|
+
function parseForLoop() {
|
|
577
|
+
const pos = tokens[cursor - 1].pos;
|
|
578
|
+
const variable = expectOrThrow(0 /* IDENT */).value;
|
|
579
|
+
let token;
|
|
580
|
+
try {
|
|
581
|
+
token = expectOrThrow(1 /* KEYWORD */, "in");
|
|
582
|
+
} catch {
|
|
583
|
+
token = expectOrThrow(1 /* KEYWORD */, "of");
|
|
584
|
+
}
|
|
585
|
+
const loopType = token.value === "in" ? 1 /* IN */ : 0 /* OF */;
|
|
586
|
+
const iterable = parseTernaryExpr();
|
|
587
|
+
return { type: 13 /* FOR */, loopType, iterable, variable, pos };
|
|
588
|
+
}
|
|
589
|
+
function parseIfExpression() {
|
|
590
|
+
const condition = parseTernaryExpr();
|
|
591
|
+
return { condition, pos: condition.pos, type: 14 /* IF */ };
|
|
592
|
+
}
|
|
593
|
+
function parseElseExpression() {
|
|
594
|
+
const pos = tokens[cursor - 1].pos;
|
|
595
|
+
try {
|
|
596
|
+
expectOrThrow(1 /* KEYWORD */, "if");
|
|
597
|
+
return { ...parseIfExpression(), type: 15 /* ELSE_IF */, pos };
|
|
598
|
+
} catch {
|
|
599
|
+
return { type: 16 /* ELSE */, pos };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function extractFnArgs() {
|
|
603
|
+
const args = [];
|
|
604
|
+
const emptyArgs = tokens[cursor]?.type === 4 /* OPERATOR */ && tokens[cursor]?.value === ")";
|
|
605
|
+
if (emptyArgs) {
|
|
606
|
+
return args;
|
|
607
|
+
}
|
|
608
|
+
args.push(parseTernaryExpr());
|
|
609
|
+
while (tokens[cursor]?.type === 4 /* OPERATOR */ && tokens[cursor]?.value === "," && tokens[cursor]?.value !== ")") {
|
|
610
|
+
cursor++;
|
|
611
|
+
args.push(parseTernaryExpr());
|
|
612
|
+
}
|
|
613
|
+
return args;
|
|
614
|
+
}
|
|
615
|
+
function parsePrimaryExpr() {
|
|
616
|
+
const token = tokens[cursor++];
|
|
617
|
+
if (token?.type === 2 /* NUMBER */) {
|
|
618
|
+
return { type: 5 /* NUMBER */, value: token.value, pos: token.pos };
|
|
619
|
+
}
|
|
620
|
+
if (token?.type === 3 /* STRING */) {
|
|
621
|
+
return { type: 4 /* STRING */, value: token.value, pos: token.pos };
|
|
622
|
+
}
|
|
623
|
+
if (token?.type === 1 /* KEYWORD */) {
|
|
624
|
+
if (token.value === "for" && cursor === 1) {
|
|
625
|
+
return parseForLoop();
|
|
626
|
+
}
|
|
627
|
+
if (token.value === "true" || token.value === "false") {
|
|
628
|
+
return {
|
|
629
|
+
type: 10 /* BOOLEAN */,
|
|
630
|
+
true: token.value === "true",
|
|
631
|
+
pos: token.pos
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
if (token.value === "undefined") {
|
|
635
|
+
return { type: 11 /* UNDEFINED */, pos: token.pos };
|
|
636
|
+
}
|
|
637
|
+
if (token.value === "null") {
|
|
638
|
+
return { type: 12 /* NULL */, pos: token.pos };
|
|
639
|
+
}
|
|
640
|
+
if (token.value === "end" && tokens.length === 1) {
|
|
641
|
+
return { type: 17 /* END */, pos: token.pos };
|
|
642
|
+
}
|
|
643
|
+
if (token.value === "if" && cursor === 1) {
|
|
644
|
+
return parseIfExpression();
|
|
645
|
+
}
|
|
646
|
+
if (token.value === "else" && cursor === 1) {
|
|
647
|
+
return parseElseExpression();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (token?.type === 0 /* IDENT */) {
|
|
651
|
+
return { type: 7 /* IDENT */, value: token.value, pos: token.pos };
|
|
652
|
+
}
|
|
653
|
+
if (token?.type === 4 /* OPERATOR */ && token.value === "(") {
|
|
654
|
+
const expr = parseTernaryExpr();
|
|
655
|
+
expectOrThrow(4 /* OPERATOR */, ")");
|
|
656
|
+
return { type: 9 /* GROUP */, expr, pos: token.pos };
|
|
657
|
+
}
|
|
658
|
+
if (token?.type === 4 /* OPERATOR */ && unaryOperators.has(token.value)) {
|
|
659
|
+
const operator = token.value;
|
|
660
|
+
const expr = parseTernaryExpr();
|
|
661
|
+
return { type: 2 /* UNARY */, operator, expr, pos: token.pos };
|
|
662
|
+
}
|
|
663
|
+
if (cursor > tokens.length) {
|
|
664
|
+
throw {
|
|
665
|
+
message: `Unexpected end of expression.`,
|
|
666
|
+
pos: tokens[tokens.length - 1].pos
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
throw {
|
|
670
|
+
message: `Unexpected token '${token?.value}'.`,
|
|
671
|
+
pos: token.pos
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function parsePropertyAccess() {
|
|
675
|
+
let left = parsePrimaryExpr();
|
|
676
|
+
while (tokens[cursor]) {
|
|
677
|
+
const token = tokens[cursor];
|
|
678
|
+
if (token?.type === 4 /* OPERATOR */ && token?.value === "(") {
|
|
679
|
+
cursor++;
|
|
680
|
+
if (!generatingNamespace && !config.allowFnCalls) {
|
|
681
|
+
throw {
|
|
682
|
+
message: "Function calls are not allowed.",
|
|
683
|
+
pos: tokens[cursor - 1].pos
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const args = extractFnArgs();
|
|
687
|
+
expectOrThrow(4 /* OPERATOR */, ")");
|
|
688
|
+
left = {
|
|
689
|
+
type: 3 /* CALL */,
|
|
690
|
+
expr: left,
|
|
691
|
+
args,
|
|
692
|
+
pos: tokens[cursor - 1].pos
|
|
693
|
+
};
|
|
694
|
+
} else if (token?.type === 4 /* OPERATOR */ && token?.value === "?." && tokens[cursor + 1]?.type === 4 /* OPERATOR */ && tokens[cursor + 1]?.value === "(") {
|
|
695
|
+
cursor++;
|
|
696
|
+
cursor++;
|
|
697
|
+
if (!generatingNamespace && !config.allowFnCalls) {
|
|
698
|
+
throw {
|
|
699
|
+
message: "Function calls are not allowed.",
|
|
700
|
+
pos: tokens[cursor - 1].pos
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const args = extractFnArgs();
|
|
704
|
+
expectOrThrow(4 /* OPERATOR */, ")");
|
|
705
|
+
left = {
|
|
706
|
+
type: 3 /* CALL */,
|
|
707
|
+
expr: left,
|
|
708
|
+
args,
|
|
709
|
+
optional: true,
|
|
710
|
+
pos: tokens[cursor - 1].pos
|
|
711
|
+
};
|
|
712
|
+
} else if (token?.type === 4 /* OPERATOR */ && propertyAccessOperators.has(token?.value)) {
|
|
713
|
+
const isNamespace = token?.value === "::";
|
|
714
|
+
const isBracketNotation = token?.value === "[";
|
|
715
|
+
const isOptional = token?.value === "?.";
|
|
716
|
+
cursor++;
|
|
717
|
+
if (isNamespace && (tokens[cursor - 2]?.type !== 0 /* IDENT */ || tokens[cursor]?.type !== 0 /* IDENT */)) {
|
|
718
|
+
throw {
|
|
719
|
+
message: `Invalid namespaces access. Expected syntax <IDENTIFIER>::<IDENTIFIER>, but got '${tokens[cursor - 2]?.value}::${tokens[cursor]?.value}' instead.`,
|
|
720
|
+
pos: tokens[cursor]?.pos
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (isNamespace) {
|
|
724
|
+
generatingNamespace = true;
|
|
725
|
+
const right = parsePrimaryExpr();
|
|
726
|
+
left = {
|
|
727
|
+
type: 6 /* NAMESPACE */,
|
|
728
|
+
left,
|
|
729
|
+
right,
|
|
730
|
+
pos: tokens[cursor - 1].pos
|
|
731
|
+
};
|
|
732
|
+
} else if (isBracketNotation) {
|
|
733
|
+
const right = parseTernaryExpr();
|
|
734
|
+
expectOrThrow(4 /* OPERATOR */, "]");
|
|
735
|
+
left = {
|
|
736
|
+
type: 8 /* PROP_ACCESS */,
|
|
737
|
+
right,
|
|
738
|
+
left,
|
|
739
|
+
bracketNotation: true,
|
|
740
|
+
pos: tokens[cursor - 1].pos
|
|
741
|
+
};
|
|
742
|
+
} else if (isOptional) {
|
|
743
|
+
if (tokens[cursor]?.type === 4 /* OPERATOR */ && tokens[cursor]?.value === "[") {
|
|
744
|
+
cursor++;
|
|
745
|
+
const right = parseTernaryExpr();
|
|
746
|
+
expectOrThrow(4 /* OPERATOR */, "]");
|
|
747
|
+
left = {
|
|
748
|
+
type: 8 /* PROP_ACCESS */,
|
|
749
|
+
left,
|
|
750
|
+
right,
|
|
751
|
+
bracketNotation: true,
|
|
752
|
+
optional: true,
|
|
753
|
+
pos: tokens[cursor - 1].pos
|
|
754
|
+
};
|
|
755
|
+
} else {
|
|
756
|
+
const right = parsePrimaryExpr();
|
|
757
|
+
left = {
|
|
758
|
+
type: 8 /* PROP_ACCESS */,
|
|
759
|
+
left,
|
|
760
|
+
right,
|
|
761
|
+
optional: true,
|
|
762
|
+
pos: tokens[cursor - 1].pos
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
const right = parsePrimaryExpr();
|
|
767
|
+
left = {
|
|
768
|
+
type: 8 /* PROP_ACCESS */,
|
|
769
|
+
left,
|
|
770
|
+
right,
|
|
771
|
+
pos: tokens[cursor - 1].pos
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
generatingNamespace = false;
|
|
779
|
+
return left;
|
|
780
|
+
}
|
|
781
|
+
function parseMultiplicativeExpr() {
|
|
782
|
+
let left = parsePropertyAccess();
|
|
783
|
+
while (tokens[cursor]?.type === 4 /* OPERATOR */ && multiplicativeOperators.has(tokens[cursor]?.value)) {
|
|
784
|
+
const operator = tokens[cursor++].value;
|
|
785
|
+
const right = parsePropertyAccess();
|
|
786
|
+
left = {
|
|
787
|
+
type: 0 /* BINARY */,
|
|
788
|
+
left,
|
|
789
|
+
right,
|
|
790
|
+
operator,
|
|
791
|
+
pos: tokens[cursor - 1].pos
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
return left;
|
|
795
|
+
}
|
|
796
|
+
function parseAdditiveExpr() {
|
|
797
|
+
let left = parseMultiplicativeExpr();
|
|
798
|
+
while (tokens[cursor]?.type === 4 /* OPERATOR */ && additiveOperators.has(tokens[cursor]?.value)) {
|
|
799
|
+
const operator = tokens[cursor++].value;
|
|
800
|
+
const right = parseMultiplicativeExpr();
|
|
801
|
+
left = {
|
|
802
|
+
type: 0 /* BINARY */,
|
|
803
|
+
left,
|
|
804
|
+
right,
|
|
805
|
+
operator,
|
|
806
|
+
pos: tokens[cursor - 1].pos
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
return left;
|
|
810
|
+
}
|
|
811
|
+
function parseBitwiseExpr() {
|
|
812
|
+
let left = parseAdditiveExpr();
|
|
813
|
+
while (tokens[cursor]?.type === 4 /* OPERATOR */ && comparisonOperators.has(tokens[cursor]?.value)) {
|
|
814
|
+
const operator = tokens[cursor++].value;
|
|
815
|
+
const right = parseAdditiveExpr();
|
|
816
|
+
left = {
|
|
817
|
+
type: 0 /* BINARY */,
|
|
818
|
+
left,
|
|
819
|
+
right,
|
|
820
|
+
operator,
|
|
821
|
+
pos: tokens[cursor - 1].pos
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return left;
|
|
825
|
+
}
|
|
826
|
+
function parseComparisonExpr() {
|
|
827
|
+
let left = parseBitwiseExpr();
|
|
828
|
+
while (tokens[cursor]?.type === 4 /* OPERATOR */ && comparisonOperators.has(tokens[cursor]?.value)) {
|
|
829
|
+
const operator = tokens[cursor++].value;
|
|
830
|
+
const right = parseBitwiseExpr();
|
|
831
|
+
left = {
|
|
832
|
+
type: 0 /* BINARY */,
|
|
833
|
+
left,
|
|
834
|
+
right,
|
|
835
|
+
operator,
|
|
836
|
+
pos: tokens[cursor - 1].pos
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
return left;
|
|
840
|
+
}
|
|
841
|
+
function parseEqualityExpr() {
|
|
842
|
+
let left = parseComparisonExpr();
|
|
843
|
+
while (tokens[cursor]?.type === 4 /* OPERATOR */ && equalityOperators.has(tokens[cursor]?.value)) {
|
|
844
|
+
const operator = tokens[cursor++].value;
|
|
845
|
+
const right = parseComparisonExpr();
|
|
846
|
+
left = {
|
|
847
|
+
type: 0 /* BINARY */,
|
|
848
|
+
left,
|
|
849
|
+
right,
|
|
850
|
+
operator,
|
|
851
|
+
pos: tokens[cursor - 1].pos
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
return left;
|
|
855
|
+
}
|
|
856
|
+
function parseLogicalOrExpr() {
|
|
857
|
+
let left = parseLogicalAndExpr();
|
|
858
|
+
while (tokens[cursor]?.type === 4 /* OPERATOR */ && tokens[cursor]?.value === "||") {
|
|
859
|
+
const operator = tokens[cursor++].value;
|
|
860
|
+
const right = parseLogicalAndExpr();
|
|
861
|
+
left = {
|
|
862
|
+
type: 0 /* BINARY */,
|
|
863
|
+
left,
|
|
864
|
+
right,
|
|
865
|
+
operator,
|
|
866
|
+
pos: tokens[cursor - 1].pos
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
return left;
|
|
870
|
+
}
|
|
871
|
+
function parseLogicalAndExpr() {
|
|
872
|
+
let left = parseNullishCoalesceExpr();
|
|
873
|
+
while (tokens[cursor]?.type === 4 /* OPERATOR */ && tokens[cursor]?.value === "&&") {
|
|
874
|
+
const operator = tokens[cursor++].value;
|
|
875
|
+
const right = parseNullishCoalesceExpr();
|
|
876
|
+
left = {
|
|
877
|
+
type: 0 /* BINARY */,
|
|
878
|
+
left,
|
|
879
|
+
right,
|
|
880
|
+
operator,
|
|
881
|
+
pos: tokens[cursor - 1].pos
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
return left;
|
|
885
|
+
}
|
|
886
|
+
function parseNullishCoalesceExpr() {
|
|
887
|
+
let left = parseEqualityExpr();
|
|
888
|
+
while (tokens[cursor]?.type === 4 /* OPERATOR */ && tokens[cursor]?.value === "??") {
|
|
889
|
+
const operator = tokens[cursor++].value;
|
|
890
|
+
const right = parseEqualityExpr();
|
|
891
|
+
left = {
|
|
892
|
+
type: 0 /* BINARY */,
|
|
893
|
+
left,
|
|
894
|
+
right,
|
|
895
|
+
operator,
|
|
896
|
+
pos: tokens[cursor - 1].pos
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
return left;
|
|
900
|
+
}
|
|
901
|
+
function parseTernaryExpr() {
|
|
902
|
+
const condition = parseLogicalOrExpr();
|
|
903
|
+
if (tokens[cursor]?.type !== 4 /* OPERATOR */ || tokens[cursor]?.value !== "?") {
|
|
904
|
+
return condition;
|
|
905
|
+
}
|
|
906
|
+
cursor++;
|
|
907
|
+
const left = parseTernaryExpr();
|
|
908
|
+
expectOrThrow(4 /* OPERATOR */, ":");
|
|
909
|
+
const right = parseTernaryExpr();
|
|
910
|
+
return {
|
|
911
|
+
type: 1 /* TERNARY */,
|
|
912
|
+
left,
|
|
913
|
+
right,
|
|
914
|
+
condition,
|
|
915
|
+
pos: tokens[cursor - 1].pos
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
const ast = parseTernaryExpr();
|
|
919
|
+
return ast;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// src/core/parse.ts
|
|
923
|
+
function parse(templateBlock, { delimiters }) {
|
|
924
|
+
const openingTagWithWhitespaceCtrl = `${delimiters.openingTag}${delimiters.whitespaceTrim}`;
|
|
925
|
+
const closingTagWithWhitespaceCtrl = `${delimiters.whitespaceTrim}${delimiters.closingTag}`;
|
|
926
|
+
const leftTrim = templateBlock.startsWith(openingTagWithWhitespaceCtrl);
|
|
927
|
+
const rightTrim = templateBlock.endsWith(closingTagWithWhitespaceCtrl);
|
|
928
|
+
const isComment = templateBlock.startsWith(
|
|
929
|
+
leftTrim ? openingTagWithWhitespaceCtrl + delimiters.commentTag : delimiters.openingTag + delimiters.commentTag
|
|
930
|
+
);
|
|
931
|
+
if (isComment) {
|
|
932
|
+
return { isComment, leftTrim, rightTrim };
|
|
933
|
+
}
|
|
934
|
+
const inner = templateBlock.slice(
|
|
935
|
+
leftTrim ? openingTagWithWhitespaceCtrl.length : delimiters.openingTag.length,
|
|
936
|
+
templateBlock.length - (rightTrim ? closingTagWithWhitespaceCtrl.length : delimiters.closingTag.length)
|
|
937
|
+
);
|
|
938
|
+
const trimmed = inner.trim();
|
|
939
|
+
const isBlock = trimmed.startsWith("for") || trimmed.startsWith("if") || trimmed.startsWith("else");
|
|
940
|
+
const requiresBlockClose = trimmed.startsWith("for") || trimmed.startsWith("if");
|
|
941
|
+
const isBlockEnd = trimmed === "end";
|
|
942
|
+
const hasContext = trimmed.startsWith("for");
|
|
943
|
+
return {
|
|
944
|
+
leftTrim,
|
|
945
|
+
rightTrim,
|
|
946
|
+
inner,
|
|
947
|
+
isBlock,
|
|
948
|
+
isBlockEnd,
|
|
949
|
+
hasContext,
|
|
950
|
+
requiresBlockClose
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/core/tokenize.ts
|
|
955
|
+
function tokenize(expr) {
|
|
956
|
+
let cursor = 0, char = "";
|
|
957
|
+
const tokens = [];
|
|
958
|
+
function accumulateKeywordOrIdentifier() {
|
|
959
|
+
let buffer = "";
|
|
960
|
+
if (/[a-zA-Z$_]/.test(char)) {
|
|
961
|
+
let j = cursor;
|
|
962
|
+
while (/[a-zA-Z$_0-9]/.test(expr[j]) && j < expr.length) {
|
|
963
|
+
buffer += expr[j];
|
|
964
|
+
j++;
|
|
965
|
+
}
|
|
966
|
+
tokens.push({
|
|
967
|
+
type: keywords.has(buffer) ? 1 /* KEYWORD */ : 0 /* IDENT */,
|
|
968
|
+
value: buffer,
|
|
969
|
+
pos: cursor
|
|
970
|
+
});
|
|
971
|
+
cursor = j;
|
|
972
|
+
char = expr[cursor];
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function accumulateStr() {
|
|
976
|
+
let buffer = "";
|
|
977
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
978
|
+
let j = cursor + 1;
|
|
979
|
+
while (expr[j] !== char && j < expr.length) {
|
|
980
|
+
buffer += expr[j];
|
|
981
|
+
j++;
|
|
982
|
+
}
|
|
983
|
+
if (j > expr.length) {
|
|
984
|
+
throw { pos: cursor, message: `Found string without closing quote.` };
|
|
985
|
+
}
|
|
986
|
+
tokens.push({ type: 3 /* STRING */, value: buffer, pos: cursor });
|
|
987
|
+
cursor = j;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function accumulateNumber() {
|
|
991
|
+
if (/[0-9]/.test(char)) {
|
|
992
|
+
let j = cursor, buffer = "";
|
|
993
|
+
while (/[0-9.oxe]/.test(expr[j]) && j < expr.length) {
|
|
994
|
+
buffer += expr[j];
|
|
995
|
+
j++;
|
|
996
|
+
}
|
|
997
|
+
const numVal = Number(buffer);
|
|
998
|
+
const isNan = Number.isNaN(numVal);
|
|
999
|
+
if (isNan) {
|
|
1000
|
+
throw { pos: cursor, message: "Found invalid number literal." };
|
|
1001
|
+
}
|
|
1002
|
+
tokens.push({ type: 2 /* NUMBER */, value: `${numVal}`, pos: cursor });
|
|
1003
|
+
cursor = j - 1;
|
|
1004
|
+
char = expr[cursor];
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function accumulateOperator() {
|
|
1008
|
+
const op = `${char}${expr[cursor + 1]}`;
|
|
1009
|
+
if (operators.has(op)) {
|
|
1010
|
+
tokens.push({ type: 4 /* OPERATOR */, value: op, pos: cursor });
|
|
1011
|
+
cursor++;
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (operators.has(char)) {
|
|
1015
|
+
tokens.push({ type: 4 /* OPERATOR */, value: char, pos: cursor });
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
while (cursor < expr.length) {
|
|
1020
|
+
char = expr[cursor];
|
|
1021
|
+
accumulateNumber();
|
|
1022
|
+
accumulateKeywordOrIdentifier();
|
|
1023
|
+
accumulateStr();
|
|
1024
|
+
accumulateOperator();
|
|
1025
|
+
if (!/[a-zA-Z$_0-9\s\t\r\n'"`]/.test(char) && !operators.has(char) && !operators.has(expr[cursor - 1] + char)) {
|
|
1026
|
+
throw {
|
|
1027
|
+
message: `Unexpected token '${char}' in expression.`,
|
|
1028
|
+
pos: cursor
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
cursor++;
|
|
1032
|
+
}
|
|
1033
|
+
return tokens;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// src/core/compile.ts
|
|
1037
|
+
function compile(src, config, meta) {
|
|
1038
|
+
const scope = [];
|
|
1039
|
+
const blockOpeningStack = [];
|
|
1040
|
+
const {
|
|
1041
|
+
delimiters,
|
|
1042
|
+
keepOpeningTagEscapeDelimiter,
|
|
1043
|
+
allowFnCalls,
|
|
1044
|
+
allowedProps,
|
|
1045
|
+
forbiddenProps,
|
|
1046
|
+
autoEscape
|
|
1047
|
+
} = config;
|
|
1048
|
+
let trimNext = false, cursor = 0, body = `let acc="";`;
|
|
1049
|
+
while (cursor < src.length) {
|
|
1050
|
+
let isEscaped2 = function() {
|
|
1051
|
+
let j = templateOpenTagIdx, count = 0;
|
|
1052
|
+
while (j >= delimiters.openingTagEscape.length && src.slice(j - delimiters.openingTagEscape.length, j) === delimiters.openingTagEscape) {
|
|
1053
|
+
count++;
|
|
1054
|
+
j -= delimiters.openingTagEscape.length;
|
|
1055
|
+
}
|
|
1056
|
+
return count % 2 === 1;
|
|
1057
|
+
};
|
|
1058
|
+
var isEscaped = isEscaped2;
|
|
1059
|
+
const templateOpenTagIdx = src.indexOf(delimiters.openingTag, cursor);
|
|
1060
|
+
if (templateOpenTagIdx === -1) {
|
|
1061
|
+
let lastChunk = src.slice(cursor);
|
|
1062
|
+
if (trimNext) lastChunk = lastChunk.trimStart();
|
|
1063
|
+
if (lastChunk) body += `acc+=\`${escapeRawText(lastChunk)}\`;`;
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
if (isEscaped2()) {
|
|
1067
|
+
let escapedChunk = src.slice(
|
|
1068
|
+
cursor,
|
|
1069
|
+
keepOpeningTagEscapeDelimiter ? templateOpenTagIdx + delimiters.openingTagEscape.length + 1 : templateOpenTagIdx - delimiters.openingTag.length + 1
|
|
1070
|
+
);
|
|
1071
|
+
if (trimNext) {
|
|
1072
|
+
escapedChunk = escapedChunk.trimStart();
|
|
1073
|
+
trimNext = false;
|
|
1074
|
+
}
|
|
1075
|
+
body += `acc+=\`${escapeRawText(escapedChunk)}\`;`;
|
|
1076
|
+
if (!keepOpeningTagEscapeDelimiter)
|
|
1077
|
+
body += `acc+=\`${delimiters.openingTag}\`;`;
|
|
1078
|
+
cursor = templateOpenTagIdx + delimiters.openingTag.length;
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
const templateEndTagIdx = src.indexOf(
|
|
1082
|
+
delimiters.closingTag,
|
|
1083
|
+
templateOpenTagIdx
|
|
1084
|
+
);
|
|
1085
|
+
if (templateEndTagIdx === -1) {
|
|
1086
|
+
const { line, lineIndex } = getLineAndColumnNumbers(
|
|
1087
|
+
src,
|
|
1088
|
+
templateOpenTagIdx
|
|
1089
|
+
);
|
|
1090
|
+
const { line: lineText, pos } = getLineSnapshot(
|
|
1091
|
+
src,
|
|
1092
|
+
lineIndex,
|
|
1093
|
+
templateOpenTagIdx
|
|
1094
|
+
);
|
|
1095
|
+
throw new MutorCompilerError(
|
|
1096
|
+
"No closing tag found.",
|
|
1097
|
+
line,
|
|
1098
|
+
lineText,
|
|
1099
|
+
pos,
|
|
1100
|
+
meta.path
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
const template = src.slice(
|
|
1104
|
+
templateOpenTagIdx,
|
|
1105
|
+
templateEndTagIdx + delimiters.closingTag.length
|
|
1106
|
+
);
|
|
1107
|
+
const {
|
|
1108
|
+
inner,
|
|
1109
|
+
leftTrim,
|
|
1110
|
+
rightTrim,
|
|
1111
|
+
isBlock,
|
|
1112
|
+
isBlockEnd,
|
|
1113
|
+
hasContext,
|
|
1114
|
+
requiresBlockClose,
|
|
1115
|
+
isComment
|
|
1116
|
+
} = parse(template, { delimiters });
|
|
1117
|
+
let rawText = src.slice(cursor, templateOpenTagIdx);
|
|
1118
|
+
if (rawText) {
|
|
1119
|
+
if (trimNext) {
|
|
1120
|
+
rawText = rawText.trimStart();
|
|
1121
|
+
}
|
|
1122
|
+
if (leftTrim) {
|
|
1123
|
+
rawText = rawText.trimEnd();
|
|
1124
|
+
}
|
|
1125
|
+
if (rawText) {
|
|
1126
|
+
body += `acc+=\`${escapeRawText(rawText)}\`;`;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
trimNext = false;
|
|
1130
|
+
cursor = templateEndTagIdx + delimiters.closingTag.length;
|
|
1131
|
+
try {
|
|
1132
|
+
if (!isComment) {
|
|
1133
|
+
const tokens = tokenize(inner);
|
|
1134
|
+
const ast = generateAst(tokens, { allowFnCalls });
|
|
1135
|
+
if (isBlock && requiresBlockClose && hasContext) {
|
|
1136
|
+
scope.push(ast.variable);
|
|
1137
|
+
blockOpeningStack.push({
|
|
1138
|
+
type: 0 /* LOOP */,
|
|
1139
|
+
pos: templateOpenTagIdx
|
|
1140
|
+
});
|
|
1141
|
+
} else if (isBlock && requiresBlockClose && !hasContext) {
|
|
1142
|
+
blockOpeningStack.push({
|
|
1143
|
+
type: 1 /* NON_LOOP */,
|
|
1144
|
+
pos: templateOpenTagIdx
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
if (isBlockEnd) {
|
|
1148
|
+
const lastBlockOpened = blockOpeningStack.pop();
|
|
1149
|
+
if (lastBlockOpened?.type === 0 /* LOOP */) scope.pop();
|
|
1150
|
+
if (lastBlockOpened === void 0)
|
|
1151
|
+
throw {
|
|
1152
|
+
message: "Unexpected end of block",
|
|
1153
|
+
pos: templateOpenTagIdx
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
const js = build(ast, { allowedProps, forbiddenProps, scope });
|
|
1157
|
+
if (isBlock || isBlockEnd) {
|
|
1158
|
+
body += js;
|
|
1159
|
+
} else {
|
|
1160
|
+
body += autoEscape && !js.startsWith("namespaces.Mutor.include") ? `acc+=escapeFn(${js});` : `acc+=${js};`;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (rightTrim) trimNext = true;
|
|
1164
|
+
} catch (e) {
|
|
1165
|
+
const { message, pos: relPos } = e;
|
|
1166
|
+
const { line, lineIndex } = getLineAndColumnNumbers(
|
|
1167
|
+
src,
|
|
1168
|
+
templateOpenTagIdx
|
|
1169
|
+
);
|
|
1170
|
+
const { line: lineText, pos } = getLineSnapshot(
|
|
1171
|
+
src,
|
|
1172
|
+
lineIndex,
|
|
1173
|
+
templateOpenTagIdx
|
|
1174
|
+
);
|
|
1175
|
+
throw new MutorCompilerError(
|
|
1176
|
+
message,
|
|
1177
|
+
line,
|
|
1178
|
+
lineText,
|
|
1179
|
+
pos + relPos + (leftTrim ? delimiters.openingTag.length + delimiters.whitespaceTrim.length : delimiters.openingTag.length),
|
|
1180
|
+
meta.path
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (blockOpeningStack.length) {
|
|
1185
|
+
const lastPos = blockOpeningStack.pop()?.pos;
|
|
1186
|
+
const { line, lineIndex } = getLineAndColumnNumbers(src, lastPos);
|
|
1187
|
+
const { line: lineText, pos } = getLineSnapshot(src, lineIndex, lastPos);
|
|
1188
|
+
throw new MutorCompilerError(
|
|
1189
|
+
"Unclosed block detected.",
|
|
1190
|
+
line,
|
|
1191
|
+
lineText,
|
|
1192
|
+
pos + delimiters.openingTag.length,
|
|
1193
|
+
meta.path
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
body += `return acc;`;
|
|
1197
|
+
return new Function(
|
|
1198
|
+
"ctx",
|
|
1199
|
+
"namespaces",
|
|
1200
|
+
"allowedProps",
|
|
1201
|
+
"forbiddenProps",
|
|
1202
|
+
"escapeFn",
|
|
1203
|
+
"validateComputedProps",
|
|
1204
|
+
body
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/core/mutor.ts
|
|
1209
|
+
var Mutor = class {
|
|
1210
|
+
constructor(config = {}) {
|
|
1211
|
+
this.__currentRenderedPath = "";
|
|
1212
|
+
this.__includeStack = /* @__PURE__ */ new Set();
|
|
1213
|
+
this.__cacheSize = 0;
|
|
1214
|
+
this.__config = { ...defaultConfig };
|
|
1215
|
+
this.__compiledTemplatesMap = /* @__PURE__ */ new Map();
|
|
1216
|
+
this.__currentContext = null;
|
|
1217
|
+
this.__namespaces = {
|
|
1218
|
+
...namespaces,
|
|
1219
|
+
Mutor: {
|
|
1220
|
+
include: (path, ctx) => {
|
|
1221
|
+
if (this.__includeStack.has(path)) {
|
|
1222
|
+
throw new MutorError(
|
|
1223
|
+
`Circular include detected:
|
|
1224
|
+
${Array.from(this.__includeStack).join("\n")}
|
|
1225
|
+
${path}`
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
try {
|
|
1229
|
+
this.__includeStack.add(path);
|
|
1230
|
+
return this.renderComponent(path, ctx ?? this.__currentContext);
|
|
1231
|
+
} finally {
|
|
1232
|
+
this.__includeStack.delete(path);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
this.addConfig(config);
|
|
1238
|
+
Object.defineProperty(this.__namespaces.Mutor, "$$context", {
|
|
1239
|
+
get: () => {
|
|
1240
|
+
return this.__currentContext;
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
addConfig(conf) {
|
|
1245
|
+
const {
|
|
1246
|
+
autoEscape,
|
|
1247
|
+
delimiters: overrideDelimeters,
|
|
1248
|
+
allowedProps,
|
|
1249
|
+
forbiddenProps,
|
|
1250
|
+
keepOpeningTagEscapeDelimiter,
|
|
1251
|
+
allowFnCalls,
|
|
1252
|
+
cache,
|
|
1253
|
+
build: build2
|
|
1254
|
+
} = conf;
|
|
1255
|
+
this.__config = {
|
|
1256
|
+
build: {
|
|
1257
|
+
include: /* @__PURE__ */ new Set([...build2?.include || defaultConfig.build.include]),
|
|
1258
|
+
exclude: /* @__PURE__ */ new Set([
|
|
1259
|
+
...defaultConfig.build.exclude,
|
|
1260
|
+
...build2?.exclude || []
|
|
1261
|
+
])
|
|
1262
|
+
},
|
|
1263
|
+
autoEscape: autoEscape === true ? true : autoEscape !== false,
|
|
1264
|
+
allowedProps: allowedProps || defaultConfig.allowedProps,
|
|
1265
|
+
allowFnCalls: !!allowFnCalls,
|
|
1266
|
+
cache: { ...defaultConfig.cache, ...cache || {} },
|
|
1267
|
+
forbiddenProps: /* @__PURE__ */ new Set([
|
|
1268
|
+
...defaultConfig.forbiddenProps,
|
|
1269
|
+
...forbiddenProps || []
|
|
1270
|
+
]),
|
|
1271
|
+
keepOpeningTagEscapeDelimiter: keepOpeningTagEscapeDelimiter === true ? true : keepOpeningTagEscapeDelimiter !== false,
|
|
1272
|
+
delimiters: {
|
|
1273
|
+
...defaultConfig.delimiters,
|
|
1274
|
+
...overrideDelimeters || {}
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
return this.__config;
|
|
1278
|
+
}
|
|
1279
|
+
restoreDefaultConfig() {
|
|
1280
|
+
this.__config = { ...defaultConfig };
|
|
1281
|
+
}
|
|
1282
|
+
compile(template) {
|
|
1283
|
+
return compile(template, this.__config, {
|
|
1284
|
+
path: this.__currentRenderedPath || "anonymous"
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
render(template, context) {
|
|
1288
|
+
const prevContext = this.__currentContext;
|
|
1289
|
+
if (prevContext !== context) {
|
|
1290
|
+
this.__currentContext = context;
|
|
1291
|
+
}
|
|
1292
|
+
const result = this.compile(template)(
|
|
1293
|
+
validateContext(context),
|
|
1294
|
+
this.__namespaces,
|
|
1295
|
+
this.__config.allowedProps,
|
|
1296
|
+
this.__config.forbiddenProps,
|
|
1297
|
+
escapeFn,
|
|
1298
|
+
validateComputedProp
|
|
1299
|
+
);
|
|
1300
|
+
this.__currentContext = prevContext;
|
|
1301
|
+
return result;
|
|
1302
|
+
}
|
|
1303
|
+
renderComponent(identifier, context) {
|
|
1304
|
+
if (!this.__compiledTemplatesMap.has(identifier)) {
|
|
1305
|
+
throw new MutorError(
|
|
1306
|
+
`No template exists with the identifier '${identifier}'`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
const prevRenderComponentIdentifier = this.__currentRenderedPath;
|
|
1310
|
+
const prevContext = this.__currentContext;
|
|
1311
|
+
const compiled = this.__compiledTemplatesMap.get(identifier);
|
|
1312
|
+
this.__currentContext = context;
|
|
1313
|
+
this.__currentRenderedPath = identifier;
|
|
1314
|
+
const result = compiled.fn(
|
|
1315
|
+
validateContext(context),
|
|
1316
|
+
this.__namespaces,
|
|
1317
|
+
this.__config.allowedProps,
|
|
1318
|
+
this.__config.forbiddenProps,
|
|
1319
|
+
escapeFn,
|
|
1320
|
+
validateComputedProp
|
|
1321
|
+
);
|
|
1322
|
+
this.__currentContext = prevContext;
|
|
1323
|
+
this.__currentRenderedPath = prevRenderComponentIdentifier;
|
|
1324
|
+
return result;
|
|
1325
|
+
}
|
|
1326
|
+
registerComponent(identifier, template) {
|
|
1327
|
+
const templateSize = template.length * 2 + 500;
|
|
1328
|
+
if (this.__cacheSize + templateSize > this.__config.cache.maxSize) {
|
|
1329
|
+
if (!this.createEntrySpaceForTemplate(templateSize)) {
|
|
1330
|
+
throw new MutorError(
|
|
1331
|
+
`The template for the component '${identifier}' is too large. Consider increasing 'cache.maxSize' in the config`
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
this.__cacheSize += template.length * 2 + 500;
|
|
1336
|
+
this.__compiledTemplatesMap.set(identifier, {
|
|
1337
|
+
fn: this.compile(template),
|
|
1338
|
+
size: templateSize
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
reset() {
|
|
1342
|
+
this.__config = { ...defaultConfig };
|
|
1343
|
+
this.__compiledTemplatesMap.clear();
|
|
1344
|
+
this.__currentContext = null;
|
|
1345
|
+
this.__cacheSize = 0;
|
|
1346
|
+
}
|
|
1347
|
+
createEntrySpaceForTemplate(targetSize) {
|
|
1348
|
+
if (this.__cacheSize + targetSize < this.__config.cache.maxSize) {
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1351
|
+
if (targetSize > this.__config.cache.maxSize) {
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
const firstEntry = this.__compiledTemplatesMap.entries().next().value;
|
|
1355
|
+
if (firstEntry) {
|
|
1356
|
+
const [oldestKey, oldestData] = firstEntry;
|
|
1357
|
+
this.__compiledTemplatesMap.delete(oldestKey);
|
|
1358
|
+
this.__cacheSize -= oldestData.size;
|
|
1359
|
+
}
|
|
1360
|
+
return this.createEntrySpaceForTemplate(targetSize);
|
|
1361
|
+
}
|
|
1362
|
+
getDiagnostics() {
|
|
1363
|
+
const maxSize = this.__config.cache.maxSize;
|
|
1364
|
+
return {
|
|
1365
|
+
bytesUsed: this.__cacheSize,
|
|
1366
|
+
bytesMax: maxSize,
|
|
1367
|
+
readableUsed: `${(this.__cacheSize / 1024 / 1024).toFixed(2)} MB`,
|
|
1368
|
+
readableMax: `${(maxSize / 1024 / 1024).toFixed(2)} MB`,
|
|
1369
|
+
totalEntries: this.__compiledTemplatesMap.size,
|
|
1370
|
+
percentFull: maxSize > 0 ? Math.min(100, Math.round(this.__cacheSize / maxSize * 100)) : 0,
|
|
1371
|
+
avgTemplateSize: this.__compiledTemplatesMap.size > 0 ? Math.round(this.__cacheSize / this.__compiledTemplatesMap.size) : 0
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// src/core/mutor.server.ts
|
|
1377
|
+
var Mutor2 = class extends Mutor {
|
|
1378
|
+
constructor(config = {}) {
|
|
1379
|
+
super(config);
|
|
1380
|
+
this.__namespaces.Mutor.include = (path, context) => {
|
|
1381
|
+
const resolvedPath = toAbsolutePath(this.__currentRenderedPath, path);
|
|
1382
|
+
if (this.__includeStack.has(resolvedPath)) {
|
|
1383
|
+
throw new MutorError(
|
|
1384
|
+
`Circular include detected:
|
|
1385
|
+
${Array.from(this.__includeStack).join("\n")}
|
|
1386
|
+
${resolvedPath}`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
this.__includeStack.add(resolvedPath);
|
|
1391
|
+
return this.renderFile(resolvedPath, context ?? this.__currentContext);
|
|
1392
|
+
} finally {
|
|
1393
|
+
this.__includeStack.delete(resolvedPath);
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
renderFile(path, context) {
|
|
1398
|
+
const absolutePath = toAbsolutePath(path);
|
|
1399
|
+
let compiled;
|
|
1400
|
+
const prevContext = this.__currentContext;
|
|
1401
|
+
const prevRenderComponentIdentifier = this.__currentRenderedPath;
|
|
1402
|
+
this.__currentContext = context;
|
|
1403
|
+
this.__currentRenderedPath = path;
|
|
1404
|
+
if (this.__config.cache.active && this.__compiledTemplatesMap.has(absolutePath)) {
|
|
1405
|
+
compiled = this.__compiledTemplatesMap.get(absolutePath).fn;
|
|
1406
|
+
} else {
|
|
1407
|
+
const template = (0, import_node_fs.readFileSync)(absolutePath, "utf-8");
|
|
1408
|
+
compiled = this.compile(template);
|
|
1409
|
+
if (this.__config.cache.active) {
|
|
1410
|
+
const templateSize = template.length * 2 + 500;
|
|
1411
|
+
if (this.__cacheSize + templateSize > this.__config.cache.maxSize) {
|
|
1412
|
+
if (this.createEntrySpaceForTemplate(templateSize)) {
|
|
1413
|
+
this.__compiledTemplatesMap.set(absolutePath, {
|
|
1414
|
+
fn: compiled,
|
|
1415
|
+
size: templateSize
|
|
1416
|
+
});
|
|
1417
|
+
this.__cacheSize += templateSize;
|
|
1418
|
+
}
|
|
1419
|
+
} else {
|
|
1420
|
+
this.__compiledTemplatesMap.set(absolutePath, {
|
|
1421
|
+
fn: compiled,
|
|
1422
|
+
size: templateSize
|
|
1423
|
+
});
|
|
1424
|
+
this.__cacheSize += templateSize;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
const result = compiled(
|
|
1429
|
+
validateContext(context),
|
|
1430
|
+
this.__namespaces,
|
|
1431
|
+
this.__config.allowedProps,
|
|
1432
|
+
this.__config.forbiddenProps,
|
|
1433
|
+
escapeFn,
|
|
1434
|
+
validateComputedProp
|
|
1435
|
+
);
|
|
1436
|
+
this.__currentContext = prevContext;
|
|
1437
|
+
this.__currentRenderedPath = prevRenderComponentIdentifier;
|
|
1438
|
+
return result;
|
|
1439
|
+
}
|
|
1440
|
+
async buildDir(src, destination, context) {
|
|
1441
|
+
const absoluteDestinationPath = toAbsolutePath(destination);
|
|
1442
|
+
const absoluteSrcPath = toAbsolutePath(src);
|
|
1443
|
+
await (0, import_promises.mkdir)(absoluteDestinationPath, { recursive: true });
|
|
1444
|
+
const entries = await (0, import_promises.readdir)(absoluteSrcPath, { withFileTypes: true });
|
|
1445
|
+
await Promise.all(
|
|
1446
|
+
entries.map(async (entry) => {
|
|
1447
|
+
const srcPath = (0, import_node_path2.join)(absoluteSrcPath, entry.name);
|
|
1448
|
+
const destinationPath = (0, import_node_path2.join)(absoluteDestinationPath, entry.name);
|
|
1449
|
+
if (this.__config.build.exclude.has(entry.name)) {
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if (entry.isDirectory()) {
|
|
1453
|
+
return this.buildDir(srcPath, destinationPath, context);
|
|
1454
|
+
}
|
|
1455
|
+
const extension = (0, import_node_path2.extname)(srcPath);
|
|
1456
|
+
if (this.__config.build.include.has(extension)) {
|
|
1457
|
+
const rendered = this.renderFile(srcPath, context);
|
|
1458
|
+
await (0, import_promises.writeFile)(destinationPath, rendered, "utf-8");
|
|
1459
|
+
} else {
|
|
1460
|
+
return await (0, import_promises.copyFile)(srcPath, destinationPath);
|
|
1461
|
+
}
|
|
1462
|
+
})
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
async compileDir(src) {
|
|
1466
|
+
const absolutePath = toAbsolutePath(src);
|
|
1467
|
+
const entries = await (0, import_promises.readdir)(absolutePath, { withFileTypes: true });
|
|
1468
|
+
await Promise.all(
|
|
1469
|
+
entries.map(async (entry) => {
|
|
1470
|
+
const absoluteSrcPath = (0, import_node_path2.join)(absolutePath, entry.name);
|
|
1471
|
+
if (this.__config.build.exclude.has(entry.name)) {
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (entry.isDirectory()) {
|
|
1475
|
+
return this.compileDir(absoluteSrcPath);
|
|
1476
|
+
}
|
|
1477
|
+
const extension = (0, import_node_path2.extname)(absoluteSrcPath);
|
|
1478
|
+
if (this.__config.build.include.has(extension)) {
|
|
1479
|
+
try {
|
|
1480
|
+
const template = await (0, import_promises.readFile)(absoluteSrcPath, "utf-8");
|
|
1481
|
+
this.registerComponent(absoluteSrcPath, template);
|
|
1482
|
+
} catch {
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
})
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
// src/bin/cli-errors.ts
|
|
1491
|
+
var ExitCodes = {
|
|
1492
|
+
Success: 0,
|
|
1493
|
+
RuntimeError: 1,
|
|
1494
|
+
ArgumentError: 2
|
|
1495
|
+
};
|
|
1496
|
+
var CliError = class extends Error {
|
|
1497
|
+
constructor(message, exitCode = ExitCodes.RuntimeError) {
|
|
1498
|
+
super(message);
|
|
1499
|
+
this.exitCode = exitCode;
|
|
1500
|
+
this.name = "CliError";
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
var ArgumentError = class extends CliError {
|
|
1504
|
+
constructor(message) {
|
|
1505
|
+
super(message, ExitCodes.ArgumentError);
|
|
1506
|
+
this.name = "ArgumentError";
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
var FileReadError = class extends CliError {
|
|
1510
|
+
constructor(filePath, cause) {
|
|
1511
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
1512
|
+
super(
|
|
1513
|
+
`could not read file '${filePath}': ${reason}`,
|
|
1514
|
+
ExitCodes.RuntimeError
|
|
1515
|
+
);
|
|
1516
|
+
this.name = "FileReadError";
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
var FileWriteError = class extends CliError {
|
|
1520
|
+
constructor(filePath, cause) {
|
|
1521
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
1522
|
+
super(
|
|
1523
|
+
`could not write file '${filePath}': ${reason}`,
|
|
1524
|
+
ExitCodes.RuntimeError
|
|
1525
|
+
);
|
|
1526
|
+
this.name = "FileWriteError";
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
var JsonParseError = class extends CliError {
|
|
1530
|
+
constructor(filePath, cause) {
|
|
1531
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
1532
|
+
super(
|
|
1533
|
+
`failed to parse JSON in '${filePath}': ${reason}`,
|
|
1534
|
+
ExitCodes.RuntimeError
|
|
1535
|
+
);
|
|
1536
|
+
this.name = "JsonParseError";
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
// src/bin/cli.ts
|
|
1541
|
+
var COMMANDS = /* @__PURE__ */ new Set(["compile", "build", "render"]);
|
|
1542
|
+
var OPTIONS = /* @__PURE__ */ new Set(["--out", "--data", "--config"]);
|
|
1543
|
+
var VERSION = `Mutor.js v${version}`;
|
|
1544
|
+
var USAGE = `
|
|
1545
|
+
Usage: mutor <command> <input> [options]
|
|
1546
|
+
|
|
1547
|
+
Commands:
|
|
1548
|
+
compile <template> Compile a template file to its intermediate form
|
|
1549
|
+
build <dir> Render all templates in a directory using a data source
|
|
1550
|
+
render <template> Compile and immediately render a template
|
|
1551
|
+
|
|
1552
|
+
Options:
|
|
1553
|
+
--out <path> Output file or directory (defaults to stdout for compile/render)
|
|
1554
|
+
--data <path> JSON data file to use as render context (required for build/render)
|
|
1555
|
+
--config <path> JSON config file to pass to Mutor
|
|
1556
|
+
--version Print the version and exit
|
|
1557
|
+
--help Show this help message
|
|
1558
|
+
|
|
1559
|
+
Exit codes:
|
|
1560
|
+
0 success
|
|
1561
|
+
1 runtime error (I/O failure, render failure, etc.)
|
|
1562
|
+
2 argument error (bad flags, missing or wrong-typed values)
|
|
1563
|
+
`.trim();
|
|
1564
|
+
function safeReadFile(filePath) {
|
|
1565
|
+
try {
|
|
1566
|
+
return (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
throw new FileReadError(filePath, err);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function safeWriteFile(filePath, content) {
|
|
1572
|
+
try {
|
|
1573
|
+
(0, import_node_fs2.writeFileSync)(filePath, content, "utf-8");
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
throw new FileWriteError(filePath, err);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function safeParseJsonFile(filePath) {
|
|
1579
|
+
const raw = safeReadFile(filePath);
|
|
1580
|
+
try {
|
|
1581
|
+
return JSON.parse(raw);
|
|
1582
|
+
} catch (err) {
|
|
1583
|
+
throw new JsonParseError(filePath, err);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
function assertIsFile(filePath, flag) {
|
|
1587
|
+
let stat;
|
|
1588
|
+
try {
|
|
1589
|
+
stat = (0, import_node_fs2.statSync)(filePath);
|
|
1590
|
+
} catch {
|
|
1591
|
+
throw new ArgumentError(`${flag} path '${filePath}' does not exist`);
|
|
1592
|
+
}
|
|
1593
|
+
if (!stat.isFile()) {
|
|
1594
|
+
throw new ArgumentError(`${flag} path '${filePath}' is not a file`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
function assertIsDirectory(dirPath, flag) {
|
|
1598
|
+
let stat;
|
|
1599
|
+
try {
|
|
1600
|
+
stat = (0, import_node_fs2.statSync)(dirPath);
|
|
1601
|
+
} catch {
|
|
1602
|
+
throw new ArgumentError(`${flag} path '${dirPath}' does not exist`);
|
|
1603
|
+
}
|
|
1604
|
+
if (!stat.isDirectory()) {
|
|
1605
|
+
throw new ArgumentError(`${flag} path '${dirPath}' is not a directory`);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function assertExtension(filePath, flag, ...extensions) {
|
|
1609
|
+
const matches = extensions.some((ext) => filePath.endsWith(ext));
|
|
1610
|
+
if (!matches) {
|
|
1611
|
+
throw new ArgumentError(
|
|
1612
|
+
`${flag} expects a file with extension ${extensions.join(" or ")} \u2014 got '${filePath}'`
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
function parseArgs(rawArgs) {
|
|
1617
|
+
if (rawArgs.length === 0 || rawArgs[0] === "--help") {
|
|
1618
|
+
console.log(USAGE);
|
|
1619
|
+
(0, import_node_process.exit)(ExitCodes.Success);
|
|
1620
|
+
}
|
|
1621
|
+
if (rawArgs[0] === "--version") {
|
|
1622
|
+
console.log(VERSION);
|
|
1623
|
+
(0, import_node_process.exit)(ExitCodes.Success);
|
|
1624
|
+
}
|
|
1625
|
+
const command = rawArgs[0];
|
|
1626
|
+
if (!COMMANDS.has(command)) {
|
|
1627
|
+
throw new ArgumentError(`unknown command '${command}'
|
|
1628
|
+
|
|
1629
|
+
${USAGE}`);
|
|
1630
|
+
}
|
|
1631
|
+
const commandData = rawArgs[1];
|
|
1632
|
+
if (!commandData || commandData.startsWith("--")) {
|
|
1633
|
+
throw new ArgumentError(
|
|
1634
|
+
`command '${command}' requires an input path as its first argument
|
|
1635
|
+
|
|
1636
|
+
${USAGE}`
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
const struct = { command, commandData };
|
|
1640
|
+
for (let i = 2; i < rawArgs.length; i++) {
|
|
1641
|
+
const flag = rawArgs[i];
|
|
1642
|
+
if (!OPTIONS.has(flag)) {
|
|
1643
|
+
throw new ArgumentError(`unknown option '${flag}'
|
|
1644
|
+
|
|
1645
|
+
${USAGE}`);
|
|
1646
|
+
}
|
|
1647
|
+
const value = rawArgs[i + 1];
|
|
1648
|
+
if (!value || value.startsWith("--")) {
|
|
1649
|
+
throw new ArgumentError(`option '${flag}' requires a value`);
|
|
1650
|
+
}
|
|
1651
|
+
struct[flag] = value;
|
|
1652
|
+
i++;
|
|
1653
|
+
}
|
|
1654
|
+
return struct;
|
|
1655
|
+
}
|
|
1656
|
+
function handleCompileCommand(mutor, args) {
|
|
1657
|
+
const inputPath = toAbsolutePath(args.commandData);
|
|
1658
|
+
assertIsFile(inputPath, "<input>");
|
|
1659
|
+
const template = safeReadFile(inputPath);
|
|
1660
|
+
const compiled = mutor.compile(template);
|
|
1661
|
+
const output = compiled.toString();
|
|
1662
|
+
if (args["--out"]) {
|
|
1663
|
+
const outPath = toAbsolutePath(args["--out"]);
|
|
1664
|
+
registerCleanup(() => {
|
|
1665
|
+
try {
|
|
1666
|
+
(0, import_node_fs2.rmSync)(outPath, { force: true });
|
|
1667
|
+
} catch {
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
safeWriteFile(outPath, output);
|
|
1671
|
+
console.log(`Compiled \u2192 ${args["--out"]}`);
|
|
1672
|
+
} else {
|
|
1673
|
+
console.log(output);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
async function handleBuildCommand(mutor, args) {
|
|
1677
|
+
if (!args["--data"]) {
|
|
1678
|
+
throw new ArgumentError("'build' requires a data source via --data");
|
|
1679
|
+
}
|
|
1680
|
+
if (!args["--out"]) {
|
|
1681
|
+
throw new ArgumentError("'build' requires an output directory via --out");
|
|
1682
|
+
}
|
|
1683
|
+
const inputPath = toAbsolutePath(args.commandData);
|
|
1684
|
+
const dataPath = toAbsolutePath(args["--data"]);
|
|
1685
|
+
const outPath = toAbsolutePath(args["--out"]);
|
|
1686
|
+
assertIsDirectory(inputPath, "<input>");
|
|
1687
|
+
assertIsFile(dataPath, "--data");
|
|
1688
|
+
assertExtension(dataPath, "--data", ".json");
|
|
1689
|
+
const context = safeParseJsonFile(dataPath);
|
|
1690
|
+
let buildStarted = false;
|
|
1691
|
+
registerCleanup(() => {
|
|
1692
|
+
if (buildStarted) {
|
|
1693
|
+
console.warn("\nBuild interrupted \u2014 removing partial output...");
|
|
1694
|
+
try {
|
|
1695
|
+
(0, import_node_fs2.rmSync)(outPath, { recursive: true, force: true });
|
|
1696
|
+
} catch {
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
buildStarted = true;
|
|
1701
|
+
await mutor.buildDir(inputPath, outPath, context);
|
|
1702
|
+
buildStarted = false;
|
|
1703
|
+
console.log(`Built \u2192 ${args["--out"]}`);
|
|
1704
|
+
}
|
|
1705
|
+
async function handleRenderCommand(mutor, args) {
|
|
1706
|
+
if (!args["--data"]) {
|
|
1707
|
+
throw new ArgumentError("'render' requires a data source via --data");
|
|
1708
|
+
}
|
|
1709
|
+
const inputPath = toAbsolutePath(args.commandData);
|
|
1710
|
+
const dataPath = toAbsolutePath(args["--data"]);
|
|
1711
|
+
assertIsFile(inputPath, "<input>");
|
|
1712
|
+
assertIsFile(dataPath, "--data");
|
|
1713
|
+
assertExtension(dataPath, "--data", ".json");
|
|
1714
|
+
const context = safeParseJsonFile(dataPath);
|
|
1715
|
+
const template = safeReadFile(inputPath);
|
|
1716
|
+
const output = mutor.render(template, context);
|
|
1717
|
+
if (args["--out"]) {
|
|
1718
|
+
const outPath = toAbsolutePath(args["--out"]);
|
|
1719
|
+
registerCleanup(() => {
|
|
1720
|
+
try {
|
|
1721
|
+
(0, import_node_fs2.rmSync)(outPath, { force: true });
|
|
1722
|
+
} catch {
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
safeWriteFile(outPath, output);
|
|
1726
|
+
console.log(`Rendered \u2192 ${args["--out"]}`);
|
|
1727
|
+
} else {
|
|
1728
|
+
console.log(output);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
var cleanupFn = null;
|
|
1732
|
+
function registerCleanup(fn) {
|
|
1733
|
+
cleanupFn = fn;
|
|
1734
|
+
}
|
|
1735
|
+
function handleSignal(signal) {
|
|
1736
|
+
console.warn(`
|
|
1737
|
+
Received ${signal}.`);
|
|
1738
|
+
cleanupFn?.();
|
|
1739
|
+
(0, import_node_process.exit)(ExitCodes.RuntimeError);
|
|
1740
|
+
}
|
|
1741
|
+
process.on("SIGINT", () => handleSignal("SIGINT"));
|
|
1742
|
+
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
|
1743
|
+
async function main() {
|
|
1744
|
+
const args = parseArgs(import_node_process.argv.slice(2));
|
|
1745
|
+
const mutor = new Mutor2();
|
|
1746
|
+
if (args["--config"]) {
|
|
1747
|
+
const configPath = toAbsolutePath(args["--config"]);
|
|
1748
|
+
assertIsFile(configPath, "--config");
|
|
1749
|
+
assertExtension(configPath, "--config", ".json");
|
|
1750
|
+
const config = safeParseJsonFile(configPath);
|
|
1751
|
+
mutor.addConfig(config);
|
|
1752
|
+
}
|
|
1753
|
+
switch (args.command) {
|
|
1754
|
+
case "compile":
|
|
1755
|
+
handleCompileCommand(mutor, args);
|
|
1756
|
+
break;
|
|
1757
|
+
case "build":
|
|
1758
|
+
await handleBuildCommand(mutor, args);
|
|
1759
|
+
break;
|
|
1760
|
+
case "render":
|
|
1761
|
+
await handleRenderCommand(mutor, args);
|
|
1762
|
+
break;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
main().catch((err) => {
|
|
1766
|
+
if (err instanceof CliError) {
|
|
1767
|
+
console.error(`error: ${err.message}`);
|
|
1768
|
+
(0, import_node_process.exit)(err.exitCode);
|
|
1769
|
+
}
|
|
1770
|
+
console.error("unexpected error:", err);
|
|
1771
|
+
(0, import_node_process.exit)(ExitCodes.RuntimeError);
|
|
1772
|
+
});
|
|
1773
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1774
|
+
0 && (module.exports = {
|
|
1775
|
+
handleBuildCommand,
|
|
1776
|
+
handleCompileCommand,
|
|
1777
|
+
handleRenderCommand,
|
|
1778
|
+
parseArgs,
|
|
1779
|
+
safeParseJsonFile,
|
|
1780
|
+
safeReadFile,
|
|
1781
|
+
safeWriteFile
|
|
1782
|
+
});
|
|
1783
|
+
//# sourceMappingURL=cli.cjs.map
|