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.
Files changed (43) hide show
  1. package/README.md +304 -73
  2. package/cli/cli.mjs +1 -1
  3. package/cli/commands/build.js +3 -1
  4. package/cli/commands/help.js +2 -0
  5. package/cli/commands/init.js +25 -6
  6. package/cli/constants.js +2 -1
  7. package/cli/helpers/transpile.js +5 -2
  8. package/constants/html_props.js +1 -0
  9. package/core/evaluator.js +1061 -0
  10. package/core/formats.js +15 -7
  11. package/core/helpers/config-loader.js +16 -8
  12. package/core/helpers/lib.js +72 -0
  13. package/core/helpers/preprocessor.js +202 -0
  14. package/core/helpers/runtimeOutput.js +28 -0
  15. package/core/helpers/url.js +12 -0
  16. package/core/labels.js +9 -2
  17. package/core/lexer.js +228 -61
  18. package/core/modules.js +338 -60
  19. package/core/parser.js +275 -55
  20. package/core/tokenTypes.js +11 -0
  21. package/core/transpiler.js +352 -66
  22. package/core/validator.js +70 -7
  23. package/formatter/tag.js +31 -7
  24. package/grammar.ebnf +21 -10
  25. package/helpers/fetch-fs.js +37 -0
  26. package/helpers/safeDataParser.js +3 -3
  27. package/helpers/spinner.js +97 -0
  28. package/helpers/utils.js +46 -0
  29. package/helpers/virtual-fs.js +29 -0
  30. package/index.browser.js +87 -0
  31. package/index.js +23 -332
  32. package/index.shared.js +443 -0
  33. package/mappers/languages/html.js +50 -9
  34. package/mappers/languages/json.js +81 -38
  35. package/mappers/languages/jsonc.js +82 -0
  36. package/mappers/languages/markdown.js +88 -48
  37. package/mappers/languages/mdx.js +50 -15
  38. package/mappers/languages/text.js +67 -0
  39. package/mappers/languages/xml.js +6 -6
  40. package/mappers/mapper.js +36 -4
  41. package/mappers/shared/index.js +12 -13
  42. package/package.json +11 -2
  43. 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, "&lt;")
361
+ .replace(/>/g, "&gt;")
362
+ .replace(/"/g, "&quot;")
363
+ .replace(/'/g, "&#39;");
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();