sommark 5.0.2 → 5.0.4

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/core/evaluator.js CHANGED
@@ -1,9 +1,21 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { getQuickJS } from "quickjs-emscripten";
2
3
  import path from "pathe";
3
4
  import * as acorn from "acorn";
4
5
  import SomMark, { registerHostCompile, registerHostSettings } from "./helpers/lib.js";
5
6
  import { formatMessage } from "./errors.js";
6
7
 
8
+ // Each transpile() call gets its own isolated EvaluatorState stack via async context.
9
+ const evaluatorStorage = new AsyncLocalStorage();
10
+
11
+ /**
12
+ * Runs fn inside an isolated evaluator context.
13
+ * Concurrent transpile() calls each get their own stack — no cross-contamination.
14
+ */
15
+ export function withEvaluator(fn) {
16
+ return evaluatorStorage.run([], fn);
17
+ }
18
+
7
19
  // Global tracker to ensure deep recursive Smark compilation never exceeds safe boundaries
8
20
  let globalCompilationDepth = 0;
9
21
 
@@ -730,8 +742,10 @@ class EvaluatorState {
730
742
  this.expose(vars);
731
743
  }
732
744
 
733
- async execute(code) {
745
+ async execute(code, baseDir = null) {
734
746
  if (!this.context) throw new Error("Evaluator not initialized");
747
+ const prevBaseDir = this.baseDir;
748
+ if (baseDir) this.baseDir = baseDir;
735
749
 
736
750
  const timeout = this.security?.timeout ?? 5000;
737
751
  this.deadline = Date.now() + timeout;
@@ -965,6 +979,7 @@ class EvaluatorState {
965
979
  } finally {
966
980
  this.deadline = 0;
967
981
  clearInterval(interval);
982
+ if (baseDir) this.baseDir = prevBaseDir;
968
983
  }
969
984
  }
970
985
 
@@ -1001,30 +1016,42 @@ class EvaluatorState {
1001
1016
 
1002
1017
  class Evaluator {
1003
1018
  constructor() {
1004
- this.instances = [];
1019
+ // Fallback stack for callers that use init() outside withEvaluator() (e.g. tests).
1020
+ this._fallbackStack = [];
1005
1021
  }
1006
1022
 
1023
+ _getStack() {
1024
+ return evaluatorStorage.getStore() ?? this._fallbackStack;
1025
+ }
1026
+
1027
+ // Expose the active stack so tests can check .instances.length
1028
+ get instances() { return this._getStack(); }
1029
+
1007
1030
  setDefaultFs(fs) {
1008
1031
  defaultFs = fs;
1009
1032
  }
1010
1033
 
1011
1034
  get active() {
1012
- if (this.instances.length === 0) {
1035
+ const stack = this._getStack();
1036
+ if (stack.length === 0) {
1013
1037
  throw new Error("No active EvaluatorState instance. Did you call init()?");
1014
1038
  }
1015
- return this.instances[this.instances.length - 1];
1039
+ return stack[stack.length - 1];
1016
1040
  }
1017
1041
 
1042
+ // Forward .runtime to the active state so tests can assert on it
1043
+ get runtime() { return this.active?.runtime ?? null; }
1044
+
1018
1045
  async init(baseDir = null, security = {}, settings = {}, mapperFile = null) {
1019
1046
  const state = new EvaluatorState();
1020
1047
  await state.init(baseDir, security, settings, mapperFile);
1021
- this.instances.push(state);
1048
+ this._getStack().push(state);
1022
1049
  }
1023
1050
 
1024
1051
  destroy() {
1025
- if (this.instances.length > 0) {
1026
- const state = this.instances.pop();
1027
- state.destroy();
1052
+ const stack = this._getStack();
1053
+ if (stack.length > 0) {
1054
+ stack.pop().destroy();
1028
1055
  }
1029
1056
  }
1030
1057
 
@@ -1040,8 +1067,8 @@ class Evaluator {
1040
1067
  this.active.inject(vars);
1041
1068
  }
1042
1069
 
1043
- async execute(code) {
1044
- return await this.active.execute(code);
1070
+ async execute(code, baseDir = null) {
1071
+ return await this.active.execute(code, baseDir);
1045
1072
  }
1046
1073
 
1047
1074
  hasDynamicTag(id) {
@@ -6,6 +6,7 @@ const LITE_ERROR =
6
6
  "Use the full SomMark bundle to enable JS evaluation.";
7
7
 
8
8
  export function setCompilerClass(_cls) {}
9
+ export function withEvaluator(fn) { return fn(); }
9
10
 
10
11
  class EvaluatorStub {
11
12
  setDefaultFs(_fs) {}
@@ -18,7 +18,7 @@ export function registerHostSettings(settings) {
18
18
  hostSettings = settings || {};
19
19
  }
20
20
 
21
- const version = "5.0.2";
21
+ const version = "5.0.4";
22
22
 
23
23
  const SomMark = {
24
24
  version,
package/core/lexer.js CHANGED
@@ -346,13 +346,6 @@ function lexer(src, filename = "anonymous") {
346
346
  // LOGIC BLOCKS (${ ... }$) — explicit: static/runtime ${ }$ shorthand: ${ }$ = static ${ }$
347
347
  if (char === "$" && next === "{") {
348
348
  {
349
- const hasExplicitKeyword = last_non_junk_type === TOKEN_TYPES.STATIC_KEYWORD || last_non_junk_type === TOKEN_TYPES.RUNTIME_KEYWORD;
350
- if (!hasExplicitKeyword) {
351
- // Zero-width: synthetic token has no source presence, must not shift position
352
- tokens.push({ type: TOKEN_TYPES.STATIC_KEYWORD, value: "static", source: filename, range: { start: { line, character }, end: { line, character } } });
353
- prev_type = TOKEN_TYPES.STATIC_KEYWORD;
354
- last_non_junk_type = TOKEN_TYPES.STATIC_KEYWORD;
355
- }
356
349
  addToken(TOKEN_TYPES.LOGIC_OPEN, "${");
357
350
  i += 2;
358
351
 
package/core/modules.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from "pathe";
2
2
  import { fileURLToPath } from "./helpers/url.js";
3
3
  import { runtimeError } from "./errors.js";
4
- import { IMPORT, USE_MODULE, TEXT, BLOCK, COMMENT, SLOT } from "./labels.js";
4
+ import { IMPORT, USE_MODULE, TEXT, BLOCK, COMMENT, SLOT, STATIC_LOGIC, RUNTIME_LOGIC } from "./labels.js";
5
5
 
6
6
  /**
7
7
  * Resolves a module path relative to a base directory.
@@ -52,7 +52,10 @@ const resolveAstVariables = (nodes, variables) => {
52
52
  for (const node of nodes) {
53
53
  if (node.type === TEXT) {
54
54
  if (node.text.includes(VAR_PREFIX)) {
55
- node.text = node.text.replace(VAR_PATTERN, (match, key) => {
55
+ node.text = node.text.replace(VAR_PATTERN, (match, keyAndFallback) => {
56
+ const pipeIdx = keyAndFallback.indexOf('|');
57
+ const key = pipeIdx >= 0 ? keyAndFallback.slice(0, pipeIdx) : keyAndFallback;
58
+ const fallback = pipeIdx >= 0 ? keyAndFallback.slice(pipeIdx + 1) : undefined;
56
59
  if (variables[key] !== undefined) {
57
60
  if (!variables.__consumed__) {
58
61
  Object.defineProperty(variables, "__consumed__", {
@@ -65,14 +68,21 @@ const resolveAstVariables = (nodes, variables) => {
65
68
  variables.__consumed__.add(key);
66
69
  return String(variables[key]);
67
70
  }
71
+ if (fallback !== undefined) return fallback;
68
72
  return match;
69
73
  });
70
74
  }
71
75
  } else if (node.type === BLOCK) {
72
76
  // Resolve any unresolved variables in block arguments
73
77
  for (const [argKey, argVal] of Object.entries(node.props)) {
74
- if (typeof argVal === "string" && argVal.startsWith(VAR_PREFIX) && argVal.endsWith(VAR_SUFFIX)) {
75
- const varKey = argVal.slice(VAR_PREFIX.length, -VAR_SUFFIX.length);
78
+ if (typeof argVal !== "string" || !argVal.includes(VAR_PREFIX)) continue;
79
+
80
+ if (argVal.startsWith(VAR_PREFIX) && argVal.endsWith(VAR_SUFFIX)) {
81
+ // Entire value is an envelope — resolve to scalar or fallback
82
+ const keyAndFallback = argVal.slice(VAR_PREFIX.length, -VAR_SUFFIX.length);
83
+ const pipeIdx = keyAndFallback.indexOf('|');
84
+ const varKey = pipeIdx >= 0 ? keyAndFallback.slice(0, pipeIdx) : keyAndFallback;
85
+ const fallback = pipeIdx >= 0 ? keyAndFallback.slice(pipeIdx + 1) : undefined;
76
86
  if (variables[varKey] !== undefined) {
77
87
  node.props[argKey] = variables[varKey];
78
88
  if (!variables.__consumed__) {
@@ -84,7 +94,31 @@ const resolveAstVariables = (nodes, variables) => {
84
94
  });
85
95
  }
86
96
  variables.__consumed__.add(varKey);
97
+ } else if (fallback !== undefined) {
98
+ node.props[argKey] = fallback;
87
99
  }
100
+ } else {
101
+ // Envelope embedded inside a larger string — replace in-place.
102
+ // Unresolved envelopes become "" so they don't pollute class names etc.
103
+ node.props[argKey] = argVal.replace(VAR_PATTERN, (match, keyAndFallback) => {
104
+ const pipeIdx = keyAndFallback.indexOf('|');
105
+ const key = pipeIdx >= 0 ? keyAndFallback.slice(0, pipeIdx) : keyAndFallback;
106
+ const fallback = pipeIdx >= 0 ? keyAndFallback.slice(pipeIdx + 1) : undefined;
107
+ if (variables[key] !== undefined) {
108
+ if (!variables.__consumed__) {
109
+ Object.defineProperty(variables, "__consumed__", {
110
+ value: new Set(),
111
+ writable: true,
112
+ enumerable: false,
113
+ configurable: true
114
+ });
115
+ }
116
+ variables.__consumed__.add(key);
117
+ return String(variables[key]);
118
+ }
119
+ if (fallback !== undefined) return fallback;
120
+ return "";
121
+ });
88
122
  }
89
123
  }
90
124
  if (node.body) {
@@ -114,6 +148,7 @@ const cloneAst = (nodes) => {
114
148
  if (node.id !== undefined) nodeCopy.id = node.id;
115
149
  if (node.code !== undefined) nodeCopy.code = node.code;
116
150
  if (node.isSelfClosing !== undefined) nodeCopy.isSelfClosing = node.isSelfClosing;
151
+ if (node.baseDir !== undefined) nodeCopy.baseDir = node.baseDir;
117
152
  if (node.props !== undefined) {
118
153
  nodeCopy.props = { ...node.props };
119
154
  }
@@ -125,6 +160,20 @@ const cloneAst = (nodes) => {
125
160
  return copy;
126
161
  };
127
162
 
163
+ /**
164
+ * Tags all STATIC_LOGIC and RUNTIME_LOGIC nodes in a subtree with their
165
+ * source module's baseDir so the evaluator can resolve imports correctly.
166
+ */
167
+ const tagLogicNodes = (nodes, baseDir) => {
168
+ if (!nodes) return;
169
+ for (const node of nodes) {
170
+ if ((node.type === STATIC_LOGIC || node.type === RUNTIME_LOGIC) && !node.baseDir) {
171
+ node.baseDir = baseDir;
172
+ }
173
+ if (node.body) tagLogicNodes(node.body, baseDir);
174
+ }
175
+ };
176
+
128
177
  /**
129
178
  * Handles all [import] and [$use-module] blocks in your code.
130
179
  * It loads the requested files, checks for errors, and puts the content into the main document.
@@ -298,6 +347,7 @@ export async function resolveModules(ast, context) {
298
347
  format: context.format,
299
348
  filename: mod.path,
300
349
  baseDir: path.dirname(mod.localPath),
350
+ fs: context.instance.fs,
301
351
  mapperFile: context.instance.mapperFile,
302
352
  placeholders: context.instance.placeholders,
303
353
  variables: {},
@@ -313,6 +363,7 @@ export async function resolveModules(ast, context) {
313
363
  });
314
364
 
315
365
  const subAst = await subSmark.parse();
366
+ tagLogicNodes(subAst, path.dirname(mod.localPath));
316
367
  context.instance.moduleCache.set(mod.localPath, subAst);
317
368
  expandedNodes = trimAst(subAst);
318
369
  }
@@ -356,6 +407,7 @@ export async function resolveModules(ast, context) {
356
407
  format: context.format,
357
408
  filename: mod.path,
358
409
  baseDir: path.dirname(mod.localPath),
410
+ fs: context.instance.fs,
359
411
  mapperFile: context.instance.mapperFile,
360
412
  placeholders: context.instance.placeholders,
361
413
  variables: {}, // Parse without variables to keep the cached AST pure
@@ -371,6 +423,7 @@ export async function resolveModules(ast, context) {
371
423
  });
372
424
 
373
425
  subAst = await subSmark.parse();
426
+ tagLogicNodes(subAst, path.dirname(mod.localPath));
374
427
  context.instance.moduleCache.set(mod.localPath, subAst);
375
428
  subAst = cloneAst(subAst);
376
429
  }
@@ -441,3 +494,10 @@ export async function resolveModules(ast, context) {
441
494
 
442
495
  return ast;
443
496
  }
497
+
498
+ /**
499
+ * After full transpilation of the top-level file, apply any v{} fallbacks that
500
+ * remain unresolved. Envelopes with no fallback are kept as-is (debugging signal).
501
+ * Must NOT be called on sub-module ASTs — only on the final top-level AST.
502
+ */
503
+ export const applyVariableFallbacks = (ast) => resolveAstVariables(ast, {});
package/core/parser.js CHANGED
@@ -389,6 +389,22 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
389
389
  nextI++;
390
390
  }
391
391
 
392
+ return [node, nextI, false];
393
+ } else if (current_token(tokens, i).type === TOKEN_TYPES.LOGIC_OPEN) {
394
+ if (!allowLogic) {
395
+ parserError(errorMessage(tokens, i, "literal value", "", "Logic blocks are not allowed in this context."));
396
+ }
397
+ let nextI = i + 1;
398
+ const logicToken = current_token(tokens, nextI);
399
+ const node = makeLogicNode(STATIC_LOGIC);
400
+ node.code = logicToken ? logicToken.value : "";
401
+ node.range = logicToken ? logicToken.range : current_token(tokens, i).range;
402
+ nextI++;
403
+
404
+ if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
405
+ nextI++;
406
+ }
407
+
392
408
  return [node, nextI, false];
393
409
  } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_V) {
394
410
  i++; // consume PREFIX_V keyword
@@ -405,7 +421,9 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
405
421
  }
406
422
  variables.__consumed__.add(vKey);
407
423
  } else {
408
- val = vFallback !== undefined ? vFallback : getPrefixValue('v', vKey);
424
+ // Encode fallback in the envelope key so resolveAstVariables can apply it
425
+ // at instantiation time instead of baking it in now.
426
+ val = getPrefixValue('v', vFallback !== undefined ? `${vKey}|${vFallback}` : vKey);
409
427
  }
410
428
  return [val, i, false];
411
429
  } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P) {
@@ -838,7 +856,8 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
838
856
  }
839
857
  variables.__consumed__.add(tvKey);
840
858
  } else {
841
- textNode.text += tvFallback !== undefined ? tvFallback : getPrefixValue('v', tvKey);
859
+ // Encode fallback in envelope so resolveAstVariables can apply it later.
860
+ textNode.text += getPrefixValue('v', tvFallback !== undefined ? `${tvKey}|${tvFallback}` : tvKey);
842
861
  }
843
862
  } else {
844
863
  break;
@@ -938,6 +957,28 @@ function parseNode(tokens, i, filename = null, placeholders = {}, variables = {}
938
957
  return [node, nextI];
939
958
  }
940
959
  // ========================================================================== //
960
+ // Bare Logic Block (${ }$ without explicit static/runtime — defaults to static)
961
+ // ========================================================================== //
962
+ else if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.LOGIC_OPEN) {
963
+ global_static_logic_count++;
964
+ let nextI = i + 1;
965
+ const logicToken = current_token(tokens, nextI);
966
+ const node = makeLogicNode(STATIC_LOGIC);
967
+ node.code = logicToken ? logicToken.value : "";
968
+ node.depth = depth;
969
+ node.range = {
970
+ start: current_token(tokens, i).range.start,
971
+ end: logicToken ? logicToken.range.end : current_token(tokens, i).range.end
972
+ };
973
+ nextI++;
974
+
975
+ if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
976
+ nextI++;
977
+ }
978
+
979
+ return [node, nextI];
980
+ }
981
+ // ========================================================================== //
941
982
  // Text or Placeholder //
942
983
  // ========================================================================== //
943
984
  else if (
@@ -1,6 +1,6 @@
1
1
  import { BLOCK, TEXT, COMMENT, COMMENT_BLOCK, STATIC_LOGIC, RUNTIME_LOGIC, FOR_EACH } from "./labels.js";
2
2
  import { transpilerError } from "./errors.js";
3
- import evaluator from "./evaluator.js";
3
+ import evaluator, { withEvaluator } from "./evaluator.js";
4
4
  import { matchedValue } from "../helpers/utils.js";
5
5
  import { dedentBy } from "../helpers/dedent.js";
6
6
  import { preprocessRuntimeLogic } from "./helpers/preprocessor.js";
@@ -107,7 +107,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
107
107
 
108
108
  if (node.type === STATIC_LOGIC) {
109
109
  try {
110
- const result = await evaluator.execute(node.code);
110
+ const result = await evaluator.execute(node.code, node.baseDir || null);
111
111
  if (generateRuntimeOutput) return "";
112
112
  if (result && typeof result === "object" && result.__raw !== undefined) {
113
113
  if (security?.allowRaw === false) {
@@ -313,7 +313,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
313
313
  }
314
314
  } else if (child.type === STATIC_LOGIC) {
315
315
  try {
316
- const val = await evaluator.execute(child.code);
316
+ const val = await evaluator.execute(child.code, child.baseDir || null);
317
317
  if (val !== undefined && typeof val !== "object") richText += String(val);
318
318
  } catch (err) {
319
319
  transpilerError([
@@ -431,7 +431,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
431
431
 
432
432
  case STATIC_LOGIC:
433
433
  try {
434
- const result = await evaluator.execute(body_node.code);
434
+ const result = await evaluator.execute(body_node.code, body_node.baseDir || null);
435
435
  if (result && typeof result === "object" && result.__raw !== undefined) {
436
436
  if (security?.allowRaw === false) {
437
437
  bodyOutput = mapper_file ? mapper_file.text(String(result.__raw)) : String(result.__raw);
@@ -545,109 +545,108 @@ export async function transpiler(optionsOrAst, format, mapperFile) {
545
545
  })();
546
546
 
547
547
  const dualOutput = optionsOrAst?.dualOutput || false;
548
-
549
- // Initialize Logic Sandbox
550
- await evaluator.init(fileBaseDir, security, settings, targetMapper);
551
- // Inject global data
552
548
  const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
553
549
  const variables = optionsOrAst?.variables || settings?.variables || {};
554
550
  warnDroppedVariables(variables);
555
- evaluator.inject(placeholders);
556
- evaluator.inject(variables);
557
551
 
558
- let output = "";
559
- let prev_body_node = null;
560
- let prev_was_silent = false;
552
+ return withEvaluator(async () => {
553
+ // Initialize Logic Sandbox inside isolated async context
554
+ await evaluator.init(fileBaseDir, security, settings, targetMapper);
555
+ evaluator.inject(placeholders);
556
+ evaluator.inject(variables);
561
557
 
562
- if (dualOutput) {
563
- const idState = { mode: 'record', ids: [], idx: 0 };
558
+ let output = "";
559
+ let prev_body_node = null;
560
+ let prev_was_silent = false;
564
561
 
565
- // HTML pass — generate HTML, record element IDs for runtime blocks
566
- let htmlOutput = "";
567
- try {
568
- for (let i = 0; i < body.length; i++) {
569
- const node = body[i];
570
- const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, true, instance, idState);
571
- let finalBlockOutput = blockOutput;
572
- if (prev_was_silent && node.type === TEXT) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
573
- if (finalBlockOutput) {
574
- htmlOutput += finalBlockOutput;
575
- prev_was_silent = false;
576
- } else {
577
- prev_was_silent = true;
578
- if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
579
- const nextNode = body[i + 1];
580
- if (nextNode && nextNode.type === TEXT && (nextNode.text === "\n" || nextNode.text === "\r\n")) i++;
562
+ if (dualOutput) {
563
+ const idState = { mode: 'record', ids: [], idx: 0 };
564
+
565
+ // HTML pass generate HTML, record element IDs for runtime blocks
566
+ let htmlOutput = "";
567
+ try {
568
+ for (let i = 0; i < body.length; i++) {
569
+ const node = body[i];
570
+ const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, true, instance, idState);
571
+ let finalBlockOutput = blockOutput;
572
+ if (prev_was_silent && node.type === TEXT) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
573
+ if (finalBlockOutput) {
574
+ htmlOutput += finalBlockOutput;
575
+ prev_was_silent = false;
576
+ } else {
577
+ prev_was_silent = true;
578
+ if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
579
+ const nextNode = body[i + 1];
580
+ if (nextNode && nextNode.type === TEXT && (nextNode.text === "\n" || nextNode.text === "\r\n")) i++;
581
+ }
581
582
  }
582
583
  }
584
+ } finally {
585
+ evaluator.destroy();
583
586
  }
584
- } finally {
585
- evaluator.destroy();
586
- }
587
587
 
588
- // JS pass — replay the same IDs so querySelector targets match HTML
589
- idState.mode = 'replay';
590
- idState.idx = 0;
591
- prev_was_silent = false;
588
+ // JS pass — replay the same IDs so querySelector targets match HTML
589
+ idState.mode = 'replay';
590
+ idState.idx = 0;
591
+ prev_was_silent = false;
592
592
 
593
- await evaluator.init(fileBaseDir, security, settings, targetMapper);
594
- evaluator.inject(placeholders);
595
- evaluator.inject(variables);
593
+ await evaluator.init(fileBaseDir, security, settings, targetMapper);
594
+ evaluator.inject(placeholders);
595
+ evaluator.inject(variables);
596
+
597
+ let jsOutput = "";
598
+ try {
599
+ for (let i = 0; i < body.length; i++) {
600
+ const node = body[i];
601
+ const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, true, false, instance, idState);
602
+ let finalBlockOutput = blockOutput;
603
+ if (prev_was_silent && node.type === TEXT) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
604
+ if (finalBlockOutput) {
605
+ jsOutput += finalBlockOutput;
606
+ prev_was_silent = false;
607
+ } else {
608
+ prev_was_silent = true;
609
+ }
610
+ }
611
+ } finally {
612
+ evaluator.destroy();
613
+ }
614
+
615
+ return [htmlOutput.trim(), jsOutput.trim()];
616
+ }
596
617
 
597
- let jsOutput = "";
598
618
  try {
599
619
  for (let i = 0; i < body.length; i++) {
600
620
  const node = body[i];
601
- const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, true, false, instance, idState);
621
+ const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, false, instance);
622
+
602
623
  let finalBlockOutput = blockOutput;
603
- if (prev_was_silent && node.type === TEXT) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
624
+ if (prev_was_silent && node.type === TEXT) {
625
+ finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
626
+ }
627
+
604
628
  if (finalBlockOutput) {
605
- jsOutput += finalBlockOutput;
629
+ output += finalBlockOutput;
606
630
  prev_was_silent = false;
631
+ if (node.type !== TEXT || node.text.trim().length > 0) {
632
+ prev_body_node = node;
633
+ }
607
634
  } else {
608
635
  prev_was_silent = true;
636
+ if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
637
+ const nextNode = body[i + 1];
638
+ if (nextNode && nextNode.type === TEXT && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
639
+ i++;
640
+ }
641
+ }
609
642
  }
610
643
  }
611
644
  } finally {
612
645
  evaluator.destroy();
613
646
  }
614
647
 
615
- return [htmlOutput.trim(), jsOutput.trim()];
616
- }
617
-
618
- try {
619
- for (let i = 0; i < body.length; i++) {
620
- const node = body[i];
621
- const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, false, instance);
622
-
623
- let finalBlockOutput = blockOutput;
624
- if (prev_was_silent && node.type === TEXT) {
625
- finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
626
- }
627
-
628
- if (finalBlockOutput) {
629
- output += finalBlockOutput;
630
- prev_was_silent = false;
631
- if (node.type !== TEXT || node.text.trim().length > 0) {
632
- prev_body_node = node;
633
- }
634
- } else {
635
- prev_was_silent = true;
636
- if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
637
- // If a comment is removed, check the next node.
638
- // If it's just a blank line, skip it so we don't have extra gaps in the output.
639
- const nextNode = body[i + 1];
640
- if (nextNode && nextNode.type === TEXT && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
641
- i++; // Skip the next newline node
642
- }
643
- }
644
- }
645
- }
646
- } finally {
647
- evaluator.destroy();
648
- }
649
-
650
- return output.trim();
648
+ return output.trim();
649
+ });
651
650
  }
652
651
 
653
652
  /**
@@ -670,7 +669,7 @@ async function transpileArgs(props) {
670
669
  result[key] = "";
671
670
  } else if (value.type === STATIC_LOGIC) {
672
671
  try {
673
- result[key] = await evaluator.execute(value.code);
672
+ result[key] = await evaluator.execute(value.code, value.baseDir || null);
674
673
  } catch (err) {
675
674
  transpilerError([
676
675
  `<$red:Logic Error (Argument):$> ${err.message}{line}`,