sommark 4.0.3 → 4.2.0
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/README.md +304 -73
- package/cli/cli.mjs +1 -1
- package/cli/commands/build.js +3 -1
- package/cli/commands/help.js +2 -0
- package/cli/commands/init.js +25 -6
- package/cli/constants.js +2 -1
- package/cli/helpers/transpile.js +5 -2
- package/constants/html_props.js +1 -0
- package/core/evaluator.js +1061 -0
- package/core/formats.js +15 -7
- package/core/helpers/config-loader.js +16 -8
- package/core/helpers/lib.js +72 -0
- package/core/helpers/preprocessor.js +202 -0
- package/core/helpers/runtimeOutput.js +28 -0
- package/core/helpers/url.js +12 -0
- package/core/labels.js +9 -2
- package/core/lexer.js +228 -61
- package/core/modules.js +338 -60
- package/core/parser.js +275 -55
- package/core/tokenTypes.js +11 -0
- package/core/transpiler.js +352 -66
- package/core/validator.js +70 -7
- package/formatter/tag.js +31 -7
- package/grammar.ebnf +21 -10
- package/helpers/fetch-fs.js +37 -0
- package/helpers/safeDataParser.js +3 -3
- package/helpers/spinner.js +97 -0
- package/helpers/utils.js +46 -0
- package/helpers/virtual-fs.js +29 -0
- package/index.browser.js +87 -0
- package/index.js +23 -332
- package/index.shared.js +443 -0
- package/mappers/languages/html.js +50 -9
- package/mappers/languages/json.js +81 -38
- package/mappers/languages/jsonc.js +82 -0
- package/mappers/languages/markdown.js +88 -48
- package/mappers/languages/mdx.js +50 -15
- package/mappers/languages/text.js +67 -0
- package/mappers/languages/xml.js +6 -6
- package/mappers/mapper.js +36 -4
- package/mappers/shared/index.js +12 -13
- package/package.json +11 -2
- package/core/formatter.js +0 -215
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
import { getQuickJS } from "quickjs-emscripten";
|
|
2
|
+
import path from "pathe";
|
|
3
|
+
import * as acorn from "acorn";
|
|
4
|
+
import SomMark, { registerHostCompile, registerHostSettings } from "./helpers/lib.js";
|
|
5
|
+
import { formatMessage } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
// Global tracker to ensure deep recursive Smark compilation never exceeds safe boundaries
|
|
8
|
+
let globalCompilationDepth = 0;
|
|
9
|
+
|
|
10
|
+
async function prefetchImports(code, baseDir, fsImpl) {
|
|
11
|
+
if (!fsImpl?.readFile) return;
|
|
12
|
+
let ast;
|
|
13
|
+
try { ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "module" }); }
|
|
14
|
+
catch { return; }
|
|
15
|
+
|
|
16
|
+
for (const node of ast.body) {
|
|
17
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
18
|
+
const importPath = node.source.value;
|
|
19
|
+
const resolved = /^https?:\/\//.test(baseDir)
|
|
20
|
+
? new URL(importPath, baseDir.endsWith("/") ? baseDir : baseDir + "/").href
|
|
21
|
+
: path.resolve(baseDir, importPath);
|
|
22
|
+
|
|
23
|
+
if (fsImpl.existsSync(resolved)) continue; // already cached
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const content = await fsImpl.readFile(resolved);
|
|
27
|
+
if (resolved.endsWith(".js")) {
|
|
28
|
+
const nextBase = /^https?:\/\//.test(resolved)
|
|
29
|
+
? resolved.slice(0, resolved.lastIndexOf("/") + 1)
|
|
30
|
+
: path.dirname(resolved);
|
|
31
|
+
await prefetchImports(content, nextBase, fsImpl);
|
|
32
|
+
}
|
|
33
|
+
} catch { /* let QuickJS surface the error */ }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let compilerClass = null;
|
|
38
|
+
|
|
39
|
+
export function setCompilerClass(cls) {
|
|
40
|
+
compilerClass = cls;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Pure, top-level stateless adapters to avoid circular references and closures over EvaluatorState
|
|
44
|
+
const customFetchAdapter = async (input, init, security = {}) => {
|
|
45
|
+
const allowFetch = security?.allowFetch !== false;
|
|
46
|
+
if (!allowFetch) {
|
|
47
|
+
throw new Error("Fetch Error: fetch is disabled in this environment.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const url = input.toString();
|
|
51
|
+
try {
|
|
52
|
+
const parsedUrl = new URL(url);
|
|
53
|
+
const protocol = parsedUrl.protocol.toLowerCase();
|
|
54
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
55
|
+
|
|
56
|
+
// 1. Enforce HTTPS (HTTP Blocked by default unless allowHttp is true)
|
|
57
|
+
const allowHttp = security?.allowHttp === true;
|
|
58
|
+
if (protocol === "http:" && !allowHttp) {
|
|
59
|
+
throw new Error("Fetch Security Error: HTTP requests are disabled. Use HTTPS instead.");
|
|
60
|
+
}
|
|
61
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
62
|
+
throw new Error(`Fetch Security Error: Unsupported protocol '${protocol}'. Only HTTP/HTTPS is allowed.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. SSRF Protection: Block localhost, loopbacks, link-local, and RFC 1918 private network IP ranges
|
|
66
|
+
const isLocal = hostname === "localhost" ||
|
|
67
|
+
hostname === "127.0.0.1" ||
|
|
68
|
+
hostname === "0.0.0.0" ||
|
|
69
|
+
hostname === "[::1]" ||
|
|
70
|
+
hostname === "::" ||
|
|
71
|
+
hostname.startsWith("127.") ||
|
|
72
|
+
hostname.startsWith("10.") ||
|
|
73
|
+
hostname.startsWith("192.168.") ||
|
|
74
|
+
hostname.startsWith("169.254.") ||
|
|
75
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname);
|
|
76
|
+
|
|
77
|
+
if (isLocal) {
|
|
78
|
+
throw new Error("SSRF Protection: Requests to local or private IP addresses are forbidden.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Whitelisted Origins Check
|
|
82
|
+
const allowedOrigins = security?.allowedOrigins;
|
|
83
|
+
if (allowedOrigins && allowedOrigins.length > 0) {
|
|
84
|
+
const origin = parsedUrl.origin.toLowerCase();
|
|
85
|
+
const isOriginAllowed = allowedOrigins.some(allowed => {
|
|
86
|
+
try {
|
|
87
|
+
const allowedUrl = new URL(allowed);
|
|
88
|
+
return origin === allowedUrl.origin.toLowerCase();
|
|
89
|
+
} catch {
|
|
90
|
+
return hostname === allowed.toLowerCase() || hostname.endsWith("." + allowed.toLowerCase());
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
if (!isOriginAllowed) {
|
|
94
|
+
throw new Error(`Fetch Security Error: Origin '${origin}' is not whitelisted.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. Whitelisted Extensions Check
|
|
99
|
+
const allowedExtensions = security?.allowedExtensions;
|
|
100
|
+
if (allowedExtensions && allowedExtensions.length > 0) {
|
|
101
|
+
const ext = path.extname(parsedUrl.pathname).toLowerCase();
|
|
102
|
+
if (!allowedExtensions.includes(ext)) {
|
|
103
|
+
throw new Error(`Fetch Security Error: Extension '${ext || "(none)"}' is not whitelisted.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
throw new Error(e.message.startsWith("Fetch Security Error:") || e.message.startsWith("SSRF Protection:")
|
|
108
|
+
? e.message
|
|
109
|
+
: "Fetch Security Error: " + e.message);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const res = await fetch(url, init);
|
|
113
|
+
const bodyText = await res.text();
|
|
114
|
+
|
|
115
|
+
const headers = {};
|
|
116
|
+
res.headers.forEach((val, key) => {
|
|
117
|
+
headers[key.toLowerCase()] = val;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
status: res.status,
|
|
122
|
+
ok: res.ok,
|
|
123
|
+
statusText: res.statusText,
|
|
124
|
+
url: res.url,
|
|
125
|
+
type: res.type,
|
|
126
|
+
redirected: res.redirected,
|
|
127
|
+
bodyText,
|
|
128
|
+
headers
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const customCompileAdapter = async (src, options, parentSecurity = {}) => {
|
|
133
|
+
const maxDepth = parentSecurity?.maxDepth ?? 5;
|
|
134
|
+
if (globalCompilationDepth >= maxDepth) {
|
|
135
|
+
throw new Error(`Recursion Guard: Maximum Smark compilation depth exceeded (limit is ${maxDepth}).`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
globalCompilationDepth++;
|
|
139
|
+
try {
|
|
140
|
+
const cleanOptions = JSON.parse(JSON.stringify(options || {}));
|
|
141
|
+
if (!compilerClass) {
|
|
142
|
+
throw new Error("Compiler class is not registered in the evaluator.");
|
|
143
|
+
}
|
|
144
|
+
const compilerOptions = {
|
|
145
|
+
...cleanOptions,
|
|
146
|
+
src,
|
|
147
|
+
format: cleanOptions.format || "html",
|
|
148
|
+
security: parentSecurity
|
|
149
|
+
};
|
|
150
|
+
const sm = new compilerClass(compilerOptions);
|
|
151
|
+
return await sm.transpile();
|
|
152
|
+
} finally {
|
|
153
|
+
globalCompilationDepth--;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Register statically once at module loading
|
|
158
|
+
registerHostCompile(customCompileAdapter);
|
|
159
|
+
|
|
160
|
+
let defaultFs = null;
|
|
161
|
+
let quickJSInstance = null;
|
|
162
|
+
async function getQuickJSModule() {
|
|
163
|
+
if (!quickJSInstance) {
|
|
164
|
+
quickJSInstance = await getQuickJS();
|
|
165
|
+
}
|
|
166
|
+
return quickJSInstance;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function objectToHandle(context, obj) {
|
|
170
|
+
if (obj === undefined) {
|
|
171
|
+
return context.undefined;
|
|
172
|
+
}
|
|
173
|
+
const jsonStr = JSON.stringify(obj);
|
|
174
|
+
const stringHandle = context.newString(jsonStr);
|
|
175
|
+
const jsonHandle = context.getProp(context.global, "JSON");
|
|
176
|
+
const parseHandle = context.getProp(jsonHandle, "parse");
|
|
177
|
+
const result = context.callFunction(parseHandle, jsonHandle, stringHandle);
|
|
178
|
+
stringHandle.dispose();
|
|
179
|
+
parseHandle.dispose();
|
|
180
|
+
jsonHandle.dispose();
|
|
181
|
+
return result.unwrap();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function expose(context, vars, pendingDeferreds) {
|
|
185
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
186
|
+
let handle;
|
|
187
|
+
if (typeof value === "function") {
|
|
188
|
+
handle = context.newFunction(key, (...args) => {
|
|
189
|
+
try {
|
|
190
|
+
const jsArgs = args.map(arg => context.dump(arg));
|
|
191
|
+
const res = value(...jsArgs);
|
|
192
|
+
if (res instanceof Promise || (res && typeof res === "object" && typeof res.then === "function")) {
|
|
193
|
+
const deferred = context.newPromise();
|
|
194
|
+
if (pendingDeferreds) {
|
|
195
|
+
pendingDeferreds.add(deferred);
|
|
196
|
+
}
|
|
197
|
+
res.then(
|
|
198
|
+
(resolvedVal) => {
|
|
199
|
+
try {
|
|
200
|
+
if (!context.alive) return;
|
|
201
|
+
if (resolvedVal === undefined) {
|
|
202
|
+
deferred.resolve();
|
|
203
|
+
} else {
|
|
204
|
+
const valHandle = objectToHandle(context, resolvedVal);
|
|
205
|
+
deferred.resolve(valHandle);
|
|
206
|
+
valHandle.dispose();
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
if (context.alive) {
|
|
210
|
+
const errHandle = context.newError(e.message || String(e));
|
|
211
|
+
deferred.reject(errHandle);
|
|
212
|
+
errHandle.dispose();
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
if (pendingDeferreds) {
|
|
216
|
+
pendingDeferreds.delete(deferred);
|
|
217
|
+
}
|
|
218
|
+
if (context.alive) {
|
|
219
|
+
deferred.dispose();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
(rejectedErr) => {
|
|
224
|
+
try {
|
|
225
|
+
if (!context.alive) return;
|
|
226
|
+
const errHandle = context.newError(rejectedErr.message || String(rejectedErr));
|
|
227
|
+
deferred.reject(errHandle);
|
|
228
|
+
errHandle.dispose();
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// ignore
|
|
231
|
+
} finally {
|
|
232
|
+
if (pendingDeferreds) {
|
|
233
|
+
pendingDeferreds.delete(deferred);
|
|
234
|
+
}
|
|
235
|
+
if (context.alive) {
|
|
236
|
+
deferred.dispose();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
return deferred.handle.dup();
|
|
242
|
+
} else if (res === undefined) {
|
|
243
|
+
return;
|
|
244
|
+
} else {
|
|
245
|
+
return objectToHandle(context, res);
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
throw context.newError(err.message || String(err));
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
handle = objectToHandle(context, value);
|
|
253
|
+
}
|
|
254
|
+
context.setProp(context.global, key, handle);
|
|
255
|
+
handle.dispose();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
class EvaluatorState {
|
|
260
|
+
constructor() {
|
|
261
|
+
this.runtime = null;
|
|
262
|
+
this.context = null;
|
|
263
|
+
this.baseDir = "/";
|
|
264
|
+
this.scopes = [{}];
|
|
265
|
+
this.dynamicTagsStack = [new Map()];
|
|
266
|
+
this.deadline = 0;
|
|
267
|
+
this.pendingDeferreds = new Set();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async init(baseDir = null, security = {}, settings = {}, mapperFile = null) {
|
|
271
|
+
if (baseDir) {
|
|
272
|
+
this.baseDir = baseDir;
|
|
273
|
+
} else if (settings?.instance?.cwd) {
|
|
274
|
+
this.baseDir = settings.instance.cwd;
|
|
275
|
+
} else {
|
|
276
|
+
this.baseDir = "/";
|
|
277
|
+
}
|
|
278
|
+
this.scopes = [{}];
|
|
279
|
+
this.dynamicTagsStack = [new Map()];
|
|
280
|
+
this.security = security;
|
|
281
|
+
this.settings = settings;
|
|
282
|
+
this.mapperFile = mapperFile;
|
|
283
|
+
registerHostSettings(settings);
|
|
284
|
+
|
|
285
|
+
this.nodeFs = defaultFs;
|
|
286
|
+
|
|
287
|
+
if (this.context) {
|
|
288
|
+
this.expose({
|
|
289
|
+
__allowRaw: this.security.allowRaw !== false
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const QuickJS = await getQuickJSModule();
|
|
295
|
+
this.runtime = QuickJS.newRuntime();
|
|
296
|
+
this.context = this.runtime.newContext();
|
|
297
|
+
|
|
298
|
+
this.deadline = 0;
|
|
299
|
+
this.runtime.setInterruptHandler(() => {
|
|
300
|
+
return this.deadline > 0 && Date.now() > this.deadline;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
this.expose({
|
|
304
|
+
__hostSomMarkVersion: SomMark.version,
|
|
305
|
+
__hostSomMarkSettings: () => {
|
|
306
|
+
const clean = { ...SomMark.settings };
|
|
307
|
+
delete clean.instance;
|
|
308
|
+
delete clean.fs;
|
|
309
|
+
return JSON.stringify(clean);
|
|
310
|
+
},
|
|
311
|
+
__hostCompile: async (src, options) => {
|
|
312
|
+
return await customCompileAdapter(src, options, this.security);
|
|
313
|
+
},
|
|
314
|
+
__hostFetch: async (input, initStr) => {
|
|
315
|
+
const init = initStr ? JSON.parse(initStr) : undefined;
|
|
316
|
+
return await customFetchAdapter(input, init, this.security);
|
|
317
|
+
},
|
|
318
|
+
__hostRegisterDynamicTag: (id, options) => {
|
|
319
|
+
this.registerDynamicTag(id, options);
|
|
320
|
+
},
|
|
321
|
+
__hostRemoveDynamicTag: (id) => {
|
|
322
|
+
const activeMap = this.dynamicTagsStack[this.dynamicTagsStack.length - 1];
|
|
323
|
+
activeMap.delete(id);
|
|
324
|
+
},
|
|
325
|
+
__hostGetTagInfo: (id) => {
|
|
326
|
+
if (!this.mapperFile) return null;
|
|
327
|
+
const target = this.mapperFile.get(id);
|
|
328
|
+
if (!target) return null;
|
|
329
|
+
return JSON.stringify({
|
|
330
|
+
options: target.options || {}
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
__hostCallTagRender: async (id, payloadStr) => {
|
|
334
|
+
if (!this.mapperFile) return "";
|
|
335
|
+
const target = this.mapperFile.get(id);
|
|
336
|
+
if (!target) return "";
|
|
337
|
+
const payload = JSON.parse(payloadStr);
|
|
338
|
+
return await target.render.call(this.mapperFile, payload);
|
|
339
|
+
},
|
|
340
|
+
__allowRaw: this.security.allowRaw !== false
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Setup standard library and namespace
|
|
344
|
+
const setupRes = this.context.evalCode(`
|
|
345
|
+
const __nativeFetch = globalThis.fetch;
|
|
346
|
+
class TagBuilder {
|
|
347
|
+
constructor(tagName) {
|
|
348
|
+
this.tagName = tagName;
|
|
349
|
+
this._children = "";
|
|
350
|
+
this._attr = [];
|
|
351
|
+
this._is_self_close = false;
|
|
352
|
+
}
|
|
353
|
+
attributes(obj) {
|
|
354
|
+
if (obj && typeof obj === "object") {
|
|
355
|
+
Object.entries(obj).forEach(([key, value]) => {
|
|
356
|
+
if (value === true) this._attr.push(key);
|
|
357
|
+
else if (value !== false && value !== null && value !== undefined) {
|
|
358
|
+
const esc = String(value)
|
|
359
|
+
.replace(/&/g, "&")
|
|
360
|
+
.replace(/</g, "<")
|
|
361
|
+
.replace(/>/g, ">")
|
|
362
|
+
.replace(/"/g, """)
|
|
363
|
+
.replace(/'/g, "'");
|
|
364
|
+
this._attr.push(\`\${key}="\${esc}"\`);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return this;
|
|
369
|
+
}
|
|
370
|
+
body(nodes) {
|
|
371
|
+
if (nodes) {
|
|
372
|
+
this._children += (this._children ? " " : "") + nodes;
|
|
373
|
+
}
|
|
374
|
+
return this.builder();
|
|
375
|
+
}
|
|
376
|
+
selfClose() {
|
|
377
|
+
this._is_self_close = true;
|
|
378
|
+
return this.builder();
|
|
379
|
+
}
|
|
380
|
+
builder() {
|
|
381
|
+
const props = this._attr.length > 0 ? " " + this._attr.join(" ") : "";
|
|
382
|
+
if (this._is_self_close) {
|
|
383
|
+
return \`<\${this.tagName}\${props} />\`;
|
|
384
|
+
}
|
|
385
|
+
return \`<\${this.tagName}\${props}>\${this._children}</\${this.tagName}>\`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const SomMark = {
|
|
390
|
+
version: __hostSomMarkVersion,
|
|
391
|
+
__dynamicTags: new Map(),
|
|
392
|
+
register: function(id, render, options = {}) {
|
|
393
|
+
if (typeof id !== "string") {
|
|
394
|
+
throw new Error("SomMark.register Error: Tag ID must be a string.");
|
|
395
|
+
}
|
|
396
|
+
if (typeof render !== "function") {
|
|
397
|
+
throw new Error("SomMark.register Error: Render function must be a function.");
|
|
398
|
+
}
|
|
399
|
+
this.__dynamicTags.set(id, { render, options });
|
|
400
|
+
__hostRegisterDynamicTag(id, options);
|
|
401
|
+
},
|
|
402
|
+
get: function(id) {
|
|
403
|
+
if (typeof id !== "string") {
|
|
404
|
+
throw new Error("SomMark.get Error: Tag ID must be a string.");
|
|
405
|
+
}
|
|
406
|
+
const local = this.__dynamicTags.get(id);
|
|
407
|
+
if (local) {
|
|
408
|
+
return {
|
|
409
|
+
options: local.options || {},
|
|
410
|
+
render: local.render
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const hostInfoStr = __hostGetTagInfo(id);
|
|
414
|
+
if (hostInfoStr) {
|
|
415
|
+
const hostInfo = JSON.parse(hostInfoStr);
|
|
416
|
+
return {
|
|
417
|
+
options: hostInfo.options || {},
|
|
418
|
+
render: async function(payload) {
|
|
419
|
+
return await __hostCallTagRender(id, JSON.stringify(payload));
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
},
|
|
425
|
+
removeOutput: function(id) {
|
|
426
|
+
if (typeof id !== "string") {
|
|
427
|
+
throw new Error("SomMark.removeOutput Error: Tag ID must be a string.");
|
|
428
|
+
}
|
|
429
|
+
this.__dynamicTags.delete(id);
|
|
430
|
+
__hostRemoveDynamicTag(id);
|
|
431
|
+
},
|
|
432
|
+
includesId: function(ids) {
|
|
433
|
+
if (!Array.isArray(ids)) {
|
|
434
|
+
throw new Error("SomMark.includesId Error: Expected an array of IDs.");
|
|
435
|
+
}
|
|
436
|
+
if (ids.some(id => this.__dynamicTags.has(id))) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
return ids.some(id => __hostGetTagInfo(id) !== null);
|
|
440
|
+
},
|
|
441
|
+
tag: function(tagName) {
|
|
442
|
+
if (typeof tagName !== "string") {
|
|
443
|
+
throw new Error("SomMark.tag Error: Tag name must be a string.");
|
|
444
|
+
}
|
|
445
|
+
return new TagBuilder(tagName);
|
|
446
|
+
},
|
|
447
|
+
get settings() {
|
|
448
|
+
const parsed = JSON.parse(__hostSomMarkSettings() || "{}");
|
|
449
|
+
Object.defineProperty(parsed, "__raw", {
|
|
450
|
+
value: JSON.stringify(parsed),
|
|
451
|
+
enumerable: false,
|
|
452
|
+
writable: false,
|
|
453
|
+
configurable: false
|
|
454
|
+
});
|
|
455
|
+
return Object.freeze(parsed);
|
|
456
|
+
},
|
|
457
|
+
fetch: async (input, init) => {
|
|
458
|
+
const plainRes = await __hostFetch(input.toString(), init ? JSON.stringify(init) : "");
|
|
459
|
+
return {
|
|
460
|
+
status: plainRes.status,
|
|
461
|
+
ok: plainRes.ok,
|
|
462
|
+
statusText: plainRes.statusText,
|
|
463
|
+
url: plainRes.url,
|
|
464
|
+
type: plainRes.type,
|
|
465
|
+
redirected: plainRes.redirected,
|
|
466
|
+
headers: {
|
|
467
|
+
get: (name) => plainRes.headers[name.toLowerCase()] || null,
|
|
468
|
+
forEach: (cb) => {
|
|
469
|
+
Object.keys(plainRes.headers).forEach(key => cb(plainRes.headers[key], key));
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
text: async () => plainRes.bodyText,
|
|
473
|
+
json: async () => JSON.parse(plainRes.bodyText),
|
|
474
|
+
clone: function() { return { ...this }; }
|
|
475
|
+
};
|
|
476
|
+
},
|
|
477
|
+
compile: async (src, options) => {
|
|
478
|
+
if (src === null || src === undefined) {
|
|
479
|
+
throw new Error("SomMark.compile Error: Template source cannot be null or undefined.");
|
|
480
|
+
}
|
|
481
|
+
if (typeof src === "function") {
|
|
482
|
+
throw new Error("SomMark.compile Error: Cannot pass a function as the template source. Did you forget to invoke/call it?");
|
|
483
|
+
}
|
|
484
|
+
if (src instanceof Promise || (typeof src === "object" && typeof src.then === "function")) {
|
|
485
|
+
throw new Error("SomMark.compile Error: Cannot pass a Promise as the template source. Did you forget to use 'await'?");
|
|
486
|
+
}
|
|
487
|
+
if (typeof src !== "string") {
|
|
488
|
+
throw new Error("SomMark.compile Error: Template source must be a string.");
|
|
489
|
+
}
|
|
490
|
+
return await __hostCompile(src, options);
|
|
491
|
+
},
|
|
492
|
+
raw: (html) => {
|
|
493
|
+
if (typeof __allowRaw !== "undefined" && !__allowRaw) {
|
|
494
|
+
throw new Error("Security Error: SomMark.raw is disabled in this environment.");
|
|
495
|
+
}
|
|
496
|
+
if (html === null || html === undefined) {
|
|
497
|
+
return { __raw: "" };
|
|
498
|
+
}
|
|
499
|
+
if (typeof html === "function") {
|
|
500
|
+
throw new Error("SomMark.raw Error: Cannot pass a function directly to SomMark.raw. Did you forget to invoke/call it?");
|
|
501
|
+
}
|
|
502
|
+
if (html instanceof Promise || (typeof html === "object" && typeof html.then === "function")) {
|
|
503
|
+
throw new Error("SomMark.raw Error: Cannot pass a Promise directly to SomMark.raw. Did you forget to use 'await'?");
|
|
504
|
+
}
|
|
505
|
+
if (typeof html === "object" && !html.__raw) {
|
|
506
|
+
throw new Error("SomMark.raw Error: Cannot render an object directly.");
|
|
507
|
+
}
|
|
508
|
+
return { __raw: String(html.__raw !== undefined ? html.__raw : html) };
|
|
509
|
+
},
|
|
510
|
+
static: (expr) => {
|
|
511
|
+
if (typeof expr !== "string") {
|
|
512
|
+
throw new Error("SomMark.static Error: Argument must be a string.");
|
|
513
|
+
}
|
|
514
|
+
return globalThis.eval(expr);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
Object.freeze(SomMark);
|
|
519
|
+
|
|
520
|
+
Object.defineProperty(globalThis, "SomMark", {
|
|
521
|
+
value: SomMark,
|
|
522
|
+
writable: false,
|
|
523
|
+
configurable: false
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
delete globalThis.fetch;
|
|
527
|
+
delete globalThis.process;
|
|
528
|
+
`);
|
|
529
|
+
|
|
530
|
+
if (setupRes.error) {
|
|
531
|
+
const err = this.context.dump(setupRes.error);
|
|
532
|
+
setupRes.error.dispose();
|
|
533
|
+
throw new Error("VM initialization failed: " + JSON.stringify(err));
|
|
534
|
+
}
|
|
535
|
+
setupRes.value.dispose();
|
|
536
|
+
|
|
537
|
+
// Configure module loader using virtual FS implementation
|
|
538
|
+
this.runtime.setModuleLoader((moduleName) => {
|
|
539
|
+
try {
|
|
540
|
+
const isRaw = moduleName.endsWith("?raw");
|
|
541
|
+
const cleanModuleName = isRaw ? moduleName.slice(0, -4) : moduleName;
|
|
542
|
+
const resolvedPath = /^https?:\/\//.test(this.baseDir)
|
|
543
|
+
? new URL(cleanModuleName, this.baseDir.endsWith("/") ? this.baseDir : this.baseDir + "/").href
|
|
544
|
+
: path.resolve(this.baseDir, cleanModuleName);
|
|
545
|
+
|
|
546
|
+
const fsImpl = this.settings?.fs || this.settings?.instance?.fs || this.nodeFs;
|
|
547
|
+
if (!fsImpl) {
|
|
548
|
+
throw new Error("No filesystem implementation available.");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (fsImpl.existsSync(resolvedPath)) {
|
|
552
|
+
let source = fsImpl.readFileSync(resolvedPath, "utf8");
|
|
553
|
+
|
|
554
|
+
if (isRaw) {
|
|
555
|
+
const escapedSource = source.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
|
|
556
|
+
return `export default \`${escapedSource}\`;`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (resolvedPath.endsWith(".json")) {
|
|
560
|
+
source = `export default ${source};`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (resolvedPath.endsWith(".smark")) {
|
|
564
|
+
const escapedSource = source.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
|
|
565
|
+
source = `
|
|
566
|
+
export default async (variables = {}) => {
|
|
567
|
+
return await SomMark.compile(\`${escapedSource}\`, { variables });
|
|
568
|
+
};
|
|
569
|
+
`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return source;
|
|
573
|
+
}
|
|
574
|
+
throw new Error(`Module not found: ${moduleName}`);
|
|
575
|
+
} catch (err) {
|
|
576
|
+
throw err;
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
expose(vars) {
|
|
582
|
+
if (!this.context) return;
|
|
583
|
+
expose(this.context, vars, this.pendingDeferreds);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
pushScope() {
|
|
587
|
+
this.scopes.push({});
|
|
588
|
+
this.dynamicTagsStack.push(new Map());
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async popScope() {
|
|
592
|
+
if (this.scopes.length > 1) {
|
|
593
|
+
const popped = this.scopes.pop();
|
|
594
|
+
this.dynamicTagsStack.pop();
|
|
595
|
+
const keysToDelete = Object.keys(popped);
|
|
596
|
+
if (keysToDelete.length > 0 && this.context) {
|
|
597
|
+
try {
|
|
598
|
+
const deleteCode = keysToDelete.map(k => `delete globalThis['${k}'];`).join(" ");
|
|
599
|
+
const deleteRes = this.context.evalCode(deleteCode, "cleanup.js");
|
|
600
|
+
if (deleteRes.value) deleteRes.value.dispose();
|
|
601
|
+
if (deleteRes.error) deleteRes.error.dispose();
|
|
602
|
+
} catch (e) {
|
|
603
|
+
// ignore
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (this.context) {
|
|
607
|
+
const merged = {};
|
|
608
|
+
for (const scope of this.scopes) {
|
|
609
|
+
Object.assign(merged, scope);
|
|
610
|
+
}
|
|
611
|
+
this.expose(merged);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
hasDynamicTag(id) {
|
|
617
|
+
for (let i = this.dynamicTagsStack.length - 1; i >= 0; i--) {
|
|
618
|
+
if (this.dynamicTagsStack[i].has(id)) return true;
|
|
619
|
+
}
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
getDynamicTagOptions(id) {
|
|
624
|
+
for (let i = this.dynamicTagsStack.length - 1; i >= 0; i--) {
|
|
625
|
+
const entry = this.dynamicTagsStack[i].get(id);
|
|
626
|
+
if (entry) return entry.options;
|
|
627
|
+
}
|
|
628
|
+
return {};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
registerDynamicTag(id, options = {}) {
|
|
632
|
+
const activeMap = this.dynamicTagsStack[this.dynamicTagsStack.length - 1];
|
|
633
|
+
activeMap.set(id, { options });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async executeDynamicTag(id, payload) {
|
|
637
|
+
if (!this.context) throw new Error("EvaluatorState not initialized");
|
|
638
|
+
this.expose({
|
|
639
|
+
__activeTagPayload: () => JSON.stringify(payload)
|
|
640
|
+
});
|
|
641
|
+
const code = `
|
|
642
|
+
(() => {
|
|
643
|
+
const payload = JSON.parse(__activeTagPayload());
|
|
644
|
+
const tag = SomMark.__dynamicTags.get(${JSON.stringify(id)});
|
|
645
|
+
if (!tag) throw new Error("Tag not found inside VM: " + ${JSON.stringify(id)});
|
|
646
|
+
const res = tag.render({
|
|
647
|
+
args: payload.args,
|
|
648
|
+
content: payload.content,
|
|
649
|
+
textContent: payload.textContent,
|
|
650
|
+
nodeType: payload.nodeType,
|
|
651
|
+
isSelfClosing: payload.isSelfClosing
|
|
652
|
+
});
|
|
653
|
+
return res;
|
|
654
|
+
})()
|
|
655
|
+
`;
|
|
656
|
+
const evalRes = this.context.evalCode(code, "render_tag.js");
|
|
657
|
+
if (evalRes.error) {
|
|
658
|
+
const err = this.context.dump(evalRes.error);
|
|
659
|
+
evalRes.error.dispose();
|
|
660
|
+
throw err;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
let resultHandle = evalRes.unwrap();
|
|
664
|
+
const state = this.context.getPromiseState(resultHandle);
|
|
665
|
+
if (state && state.type === "pending") {
|
|
666
|
+
while (true) {
|
|
667
|
+
this.runtime.executePendingJobs();
|
|
668
|
+
const curState = this.context.getPromiseState(resultHandle);
|
|
669
|
+
if (curState.type !== "pending") {
|
|
670
|
+
if (curState.type === "fulfilled") {
|
|
671
|
+
resultHandle.dispose();
|
|
672
|
+
resultHandle = curState.value;
|
|
673
|
+
} else {
|
|
674
|
+
const errHandle = curState.error;
|
|
675
|
+
const err = this.context.dump(errHandle);
|
|
676
|
+
errHandle.dispose();
|
|
677
|
+
resultHandle.dispose();
|
|
678
|
+
throw err;
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const result = this.context.dump(resultHandle);
|
|
687
|
+
resultHandle.dispose();
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
_syncScopes() {
|
|
692
|
+
if (!this.context) return;
|
|
693
|
+
const allKeysSet = new Set();
|
|
694
|
+
for (const scope of this.scopes) {
|
|
695
|
+
for (const key of Object.keys(scope)) {
|
|
696
|
+
allKeysSet.add(key);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const allKeys = Array.from(allKeysSet);
|
|
700
|
+
if (allKeys.length > 0) {
|
|
701
|
+
try {
|
|
702
|
+
const getValuesCode = `export default { ${allKeys.map(k => `${JSON.stringify(k)}: globalThis['${k}']`).join(", ")} };`;
|
|
703
|
+
const valuesRes = this.context.evalCode(getValuesCode, "sync.js", { type: 'module' });
|
|
704
|
+
if (valuesRes.value) {
|
|
705
|
+
const syncedValuesObj = this.context.dump(valuesRes.value);
|
|
706
|
+
valuesRes.value.dispose();
|
|
707
|
+
if (syncedValuesObj && typeof syncedValuesObj === 'object' && 'default' in syncedValuesObj) {
|
|
708
|
+
const syncedValues = syncedValuesObj.default;
|
|
709
|
+
for (const [key, val] of Object.entries(syncedValues)) {
|
|
710
|
+
for (let s = this.scopes.length - 1; s >= 0; s--) {
|
|
711
|
+
if (key in this.scopes[s]) {
|
|
712
|
+
this.scopes[s][key] = val;
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} else if (valuesRes.error) {
|
|
719
|
+
valuesRes.error.dispose();
|
|
720
|
+
}
|
|
721
|
+
} catch (err) {
|
|
722
|
+
// ignore
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
inject(vars) {
|
|
728
|
+
if (!this.context) return;
|
|
729
|
+
const currentScope = this.scopes[this.scopes.length - 1];
|
|
730
|
+
Object.assign(currentScope, vars);
|
|
731
|
+
this.expose(vars);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async execute(code) {
|
|
735
|
+
if (!this.context) throw new Error("Evaluator not initialized");
|
|
736
|
+
|
|
737
|
+
const timeout = this.security?.timeout ?? 5000;
|
|
738
|
+
this.deadline = Date.now() + timeout;
|
|
739
|
+
|
|
740
|
+
const interval = setInterval(() => {
|
|
741
|
+
try {
|
|
742
|
+
this.runtime.executePendingJobs();
|
|
743
|
+
} catch (err) {
|
|
744
|
+
// ignore
|
|
745
|
+
}
|
|
746
|
+
}, 1);
|
|
747
|
+
|
|
748
|
+
try {
|
|
749
|
+
let autoExportedNames = [];
|
|
750
|
+
let hasExplicitExports = false;
|
|
751
|
+
try {
|
|
752
|
+
const ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true });
|
|
753
|
+
for (const node of ast.body) {
|
|
754
|
+
if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
|
|
755
|
+
hasExplicitExports = true;
|
|
756
|
+
}
|
|
757
|
+
if (node.type === 'VariableDeclaration') {
|
|
758
|
+
for (const decl of node.declarations) {
|
|
759
|
+
if (decl.id.type === 'Identifier') autoExportedNames.push(decl.id.name);
|
|
760
|
+
else if (decl.id.type === 'ObjectPattern') {
|
|
761
|
+
for (const prop of decl.id.properties) {
|
|
762
|
+
if (prop.value.type === 'Identifier') autoExportedNames.push(prop.value.name);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
} else if (node.type === 'FunctionDeclaration') {
|
|
767
|
+
if (node.id) autoExportedNames.push(node.id.name);
|
|
768
|
+
} else if (node.type === 'ImportDeclaration') {
|
|
769
|
+
for (const spec of node.specifiers) {
|
|
770
|
+
autoExportedNames.push(spec.local.name);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
} catch (e) {
|
|
775
|
+
// Ignore parsing errors for simple expression fragments
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const hasImportExport = hasExplicitExports || /\bimport\b/.test(code);
|
|
779
|
+
const hasAwait = /\bawait\b/.test(code);
|
|
780
|
+
|
|
781
|
+
let finalCode = code;
|
|
782
|
+
|
|
783
|
+
try {
|
|
784
|
+
const ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true });
|
|
785
|
+
const lastNode = ast.body[ast.body.length - 1];
|
|
786
|
+
if (lastNode && lastNode.type === 'ExpressionStatement') {
|
|
787
|
+
const start = lastNode.start;
|
|
788
|
+
finalCode = code.slice(0, start) + "export default " + code.slice(start);
|
|
789
|
+
} else if (lastNode && lastNode.type === 'ReturnStatement') {
|
|
790
|
+
const start = lastNode.start;
|
|
791
|
+
if (lastNode.argument) {
|
|
792
|
+
const argumentCode = code.slice(lastNode.argument.start, lastNode.argument.end);
|
|
793
|
+
finalCode = code.slice(0, start) + `export default (${argumentCode});` + code.slice(lastNode.end);
|
|
794
|
+
} else {
|
|
795
|
+
finalCode = code.slice(0, start) + "export default undefined;" + code.slice(lastNode.end);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} catch (err) {
|
|
799
|
+
// Ignore parsing errors and fallback to raw code
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (autoExportedNames.length > 0 && !hasExplicitExports) {
|
|
803
|
+
finalCode += `\nexport { ${autoExportedNames.join(', ')} };`;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const isModule = hasImportExport || hasAwait || autoExportedNames.length > 0 || finalCode.includes("export default");
|
|
807
|
+
|
|
808
|
+
const fsImpl = this.settings?.fs || this.settings?.instance?.fs || this.nodeFs;
|
|
809
|
+
if (isModule) await prefetchImports(finalCode, this.baseDir, fsImpl);
|
|
810
|
+
|
|
811
|
+
let result;
|
|
812
|
+
if (isModule) {
|
|
813
|
+
const evalRes = this.context.evalCode(finalCode, "main.js", { type: 'module' });
|
|
814
|
+
if (evalRes.error) {
|
|
815
|
+
const err = this.context.dump(evalRes.error);
|
|
816
|
+
evalRes.error.dispose();
|
|
817
|
+
throw err;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
let resultHandle = evalRes.unwrap();
|
|
821
|
+
const state = this.context.getPromiseState(resultHandle);
|
|
822
|
+
if (state && state.type === "pending") {
|
|
823
|
+
while (true) {
|
|
824
|
+
this.runtime.executePendingJobs();
|
|
825
|
+
const curState = this.context.getPromiseState(resultHandle);
|
|
826
|
+
if (curState.type !== "pending") {
|
|
827
|
+
if (curState.type === "fulfilled") {
|
|
828
|
+
resultHandle.dispose();
|
|
829
|
+
resultHandle = curState.value;
|
|
830
|
+
} else {
|
|
831
|
+
const errHandle = curState.error;
|
|
832
|
+
const err = this.context.dump(errHandle);
|
|
833
|
+
errHandle.dispose();
|
|
834
|
+
resultHandle.dispose();
|
|
835
|
+
throw err;
|
|
836
|
+
}
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
let defaultHandle = this.context.getProp(resultHandle, "default");
|
|
844
|
+
let resolvedDefaultHandle = defaultHandle;
|
|
845
|
+
let isPromise = false;
|
|
846
|
+
|
|
847
|
+
const defaultState = this.context.getPromiseState(defaultHandle);
|
|
848
|
+
if (defaultState && !defaultState.notAPromise) {
|
|
849
|
+
isPromise = true;
|
|
850
|
+
if (defaultState.type === "pending") {
|
|
851
|
+
while (true) {
|
|
852
|
+
this.runtime.executePendingJobs();
|
|
853
|
+
const curState = this.context.getPromiseState(defaultHandle);
|
|
854
|
+
if (curState.type !== "pending") {
|
|
855
|
+
if (curState.type === "fulfilled") {
|
|
856
|
+
resolvedDefaultHandle = curState.value;
|
|
857
|
+
} else {
|
|
858
|
+
const errHandle = curState.error;
|
|
859
|
+
const err = this.context.dump(errHandle);
|
|
860
|
+
errHandle.dispose();
|
|
861
|
+
defaultHandle.dispose();
|
|
862
|
+
resultHandle.dispose();
|
|
863
|
+
throw err;
|
|
864
|
+
}
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
868
|
+
}
|
|
869
|
+
} else if (defaultState.type === "fulfilled") {
|
|
870
|
+
resolvedDefaultHandle = defaultState.value;
|
|
871
|
+
} else if (defaultState.type === "rejected") {
|
|
872
|
+
const errHandle = defaultState.error;
|
|
873
|
+
const err = this.context.dump(errHandle);
|
|
874
|
+
errHandle.dispose();
|
|
875
|
+
defaultHandle.dispose();
|
|
876
|
+
resultHandle.dispose();
|
|
877
|
+
throw err;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const defaultValue = this.context.dump(resolvedDefaultHandle);
|
|
882
|
+
|
|
883
|
+
if (isPromise) {
|
|
884
|
+
resolvedDefaultHandle.dispose();
|
|
885
|
+
}
|
|
886
|
+
defaultHandle.dispose();
|
|
887
|
+
|
|
888
|
+
const res = this.context.dump(resultHandle);
|
|
889
|
+
|
|
890
|
+
this.context.setProp(this.context.global, "__tempModule", resultHandle);
|
|
891
|
+
const copyRes = this.context.evalCode(`
|
|
892
|
+
for (const key of Object.keys(__tempModule)) {
|
|
893
|
+
if (key !== "default") {
|
|
894
|
+
globalThis[key] = __tempModule[key];
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
delete globalThis.__tempModule;
|
|
898
|
+
`);
|
|
899
|
+
if (copyRes.error) {
|
|
900
|
+
copyRes.error.dispose();
|
|
901
|
+
} else {
|
|
902
|
+
copyRes.value.dispose();
|
|
903
|
+
}
|
|
904
|
+
resultHandle.dispose();
|
|
905
|
+
|
|
906
|
+
if (res && typeof res === 'object') {
|
|
907
|
+
const currentScope = this.scopes[this.scopes.length - 1];
|
|
908
|
+
for (const [key, val] of Object.entries(res)) {
|
|
909
|
+
if (key !== 'default') {
|
|
910
|
+
currentScope[key] = val;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if ('default' in res) {
|
|
914
|
+
result = defaultValue;
|
|
915
|
+
} else {
|
|
916
|
+
result = undefined;
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
919
|
+
result = res;
|
|
920
|
+
}
|
|
921
|
+
} else {
|
|
922
|
+
const evalRes = this.context.evalCode(code, "main.js");
|
|
923
|
+
if (evalRes.error) {
|
|
924
|
+
const err = this.context.dump(evalRes.error);
|
|
925
|
+
evalRes.error.dispose();
|
|
926
|
+
throw err;
|
|
927
|
+
}
|
|
928
|
+
let resultHandle = evalRes.unwrap();
|
|
929
|
+
const state = this.context.getPromiseState(resultHandle);
|
|
930
|
+
if (state && state.type === "pending") {
|
|
931
|
+
while (true) {
|
|
932
|
+
this.runtime.executePendingJobs();
|
|
933
|
+
const curState = this.context.getPromiseState(resultHandle);
|
|
934
|
+
if (curState.type !== "pending") {
|
|
935
|
+
if (curState.type === "fulfilled") {
|
|
936
|
+
resultHandle.dispose();
|
|
937
|
+
resultHandle = curState.value;
|
|
938
|
+
} else {
|
|
939
|
+
const errHandle = curState.error;
|
|
940
|
+
const err = this.context.dump(errHandle);
|
|
941
|
+
errHandle.dispose();
|
|
942
|
+
resultHandle.dispose();
|
|
943
|
+
throw err;
|
|
944
|
+
}
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
result = this.context.dump(resultHandle);
|
|
951
|
+
resultHandle.dispose();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
await this._syncScopes();
|
|
955
|
+
return result;
|
|
956
|
+
} catch (error) {
|
|
957
|
+
const stack = error.stack || "";
|
|
958
|
+
const match = stack.match(/main\.js:(\d+):(\d+)/) || stack.match(/:(\d+):(\d+)/);
|
|
959
|
+
|
|
960
|
+
const err = new Error(error.message || error);
|
|
961
|
+
if (match) {
|
|
962
|
+
err.line = parseInt(match[1]);
|
|
963
|
+
err.column = parseInt(match[2]);
|
|
964
|
+
}
|
|
965
|
+
throw err;
|
|
966
|
+
} finally {
|
|
967
|
+
this.deadline = 0;
|
|
968
|
+
clearInterval(interval);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
destroy() {
|
|
973
|
+
if (this.runtime) {
|
|
974
|
+
if (this.pendingDeferreds) {
|
|
975
|
+
for (const deferred of this.pendingDeferreds) {
|
|
976
|
+
try {
|
|
977
|
+
if (deferred.alive) {
|
|
978
|
+
deferred.dispose();
|
|
979
|
+
}
|
|
980
|
+
} catch (e) {}
|
|
981
|
+
}
|
|
982
|
+
this.pendingDeferreds.clear();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
try {
|
|
986
|
+
this.runtime.executePendingJobs();
|
|
987
|
+
} catch (e) {}
|
|
988
|
+
|
|
989
|
+
try {
|
|
990
|
+
if (this.context) {
|
|
991
|
+
this.context.dispose();
|
|
992
|
+
}
|
|
993
|
+
this.runtime.dispose();
|
|
994
|
+
} catch (e) {
|
|
995
|
+
console.warn(formatMessage("<$yellow:Warning:$> Safe context disposal warning: " + e.message));
|
|
996
|
+
}
|
|
997
|
+
this.runtime = null;
|
|
998
|
+
this.context = null;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
class Evaluator {
|
|
1004
|
+
constructor() {
|
|
1005
|
+
this.instances = [];
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
setDefaultFs(fs) {
|
|
1009
|
+
defaultFs = fs;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
get active() {
|
|
1013
|
+
if (this.instances.length === 0) {
|
|
1014
|
+
throw new Error("No active EvaluatorState instance. Did you call init()?");
|
|
1015
|
+
}
|
|
1016
|
+
return this.instances[this.instances.length - 1];
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async init(baseDir = null, security = {}, settings = {}, mapperFile = null) {
|
|
1020
|
+
const state = new EvaluatorState();
|
|
1021
|
+
await state.init(baseDir, security, settings, mapperFile);
|
|
1022
|
+
this.instances.push(state);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
destroy() {
|
|
1026
|
+
if (this.instances.length > 0) {
|
|
1027
|
+
const state = this.instances.pop();
|
|
1028
|
+
state.destroy();
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
pushScope() {
|
|
1033
|
+
this.active.pushScope();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async popScope() {
|
|
1037
|
+
await this.active.popScope();
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
inject(vars) {
|
|
1041
|
+
this.active.inject(vars);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
async execute(code) {
|
|
1045
|
+
return await this.active.execute(code);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
hasDynamicTag(id) {
|
|
1049
|
+
return this.active.hasDynamicTag(id);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
getDynamicTagOptions(id) {
|
|
1053
|
+
return this.active.getDynamicTagOptions(id);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async executeDynamicTag(id, payload) {
|
|
1057
|
+
return await this.active.executeDynamicTag(id, payload);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
export default new Evaluator();
|