sommark 5.0.3 → 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.
@@ -1786,7 +1786,9 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
1786
1786
  }
1787
1787
  variables.__consumed__.add(vKey);
1788
1788
  } else {
1789
- val = vFallback !== undefined ? vFallback : getPrefixValue('v', vKey);
1789
+ // Encode fallback in the envelope key so resolveAstVariables can apply it
1790
+ // at instantiation time instead of baking it in now.
1791
+ val = getPrefixValue('v', vFallback !== undefined ? `${vKey}|${vFallback}` : vKey);
1790
1792
  }
1791
1793
  return [val, i, false];
1792
1794
  } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P) {
@@ -2180,7 +2182,8 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
2180
2182
  }
2181
2183
  variables.__consumed__.add(tvKey);
2182
2184
  } else {
2183
- textNode.text += tvFallback !== undefined ? tvFallback : getPrefixValue('v', tvKey);
2185
+ // Encode fallback in envelope so resolveAstVariables can apply it later.
2186
+ textNode.text += getPrefixValue('v', tvFallback !== undefined ? `${tvKey}|${tvFallback}` : tvKey);
2184
2187
  }
2185
2188
  } else {
2186
2189
  break;
@@ -2383,6 +2386,7 @@ function parser(tokens, filename = null, placeholders = {}, variables = {}) {
2383
2386
  const LITE_ERROR =
2384
2387
  "[SomMark lite] static ${}$ and runtime ${}$ blocks are not supported in lite mode. " +
2385
2388
  "Use the full SomMark bundle to enable JS evaluation.";
2389
+ function withEvaluator(fn) { return fn(); }
2386
2390
 
2387
2391
  class EvaluatorStub {
2388
2392
  setDefaultFs(_fs) {}
@@ -8984,7 +8988,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
8984
8988
 
8985
8989
  if (node.type === STATIC_LOGIC) {
8986
8990
  try {
8987
- const result = await Evaluator.execute(node.code);
8991
+ const result = await Evaluator.execute(node.code, node.baseDir || null);
8988
8992
  if (generateRuntimeOutput) return "";
8989
8993
  if (result && typeof result === "object" && result.__raw !== undefined) {
8990
8994
  if (security?.allowRaw === false) {
@@ -9190,7 +9194,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9190
9194
  }
9191
9195
  } else if (child.type === STATIC_LOGIC) {
9192
9196
  try {
9193
- const val = await Evaluator.execute(child.code);
9197
+ const val = await Evaluator.execute(child.code, child.baseDir || null);
9194
9198
  if (val !== undefined && typeof val !== "object") richText += String(val);
9195
9199
  } catch (err) {
9196
9200
  transpilerError([
@@ -9307,7 +9311,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9307
9311
 
9308
9312
  case STATIC_LOGIC:
9309
9313
  try {
9310
- const result = await Evaluator.execute(body_node.code);
9314
+ const result = await Evaluator.execute(body_node.code, body_node.baseDir || null);
9311
9315
  if (result && typeof result === "object" && result.__raw !== undefined) {
9312
9316
  if (security?.allowRaw === false) {
9313
9317
  bodyOutput = mapper_file ? mapper_file.text(String(result.__raw)) : String(result.__raw);
@@ -9421,109 +9425,108 @@ async function transpiler(optionsOrAst, format, mapperFile) {
9421
9425
  })();
9422
9426
 
9423
9427
  const dualOutput = optionsOrAst?.dualOutput || false;
9424
-
9425
- // Initialize Logic Sandbox
9426
- await Evaluator.init(fileBaseDir, security, settings, targetMapper);
9427
- // Inject global data
9428
9428
  const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
9429
9429
  const variables = optionsOrAst?.variables || settings?.variables || {};
9430
9430
  warnDroppedVariables(variables);
9431
- Evaluator.inject(placeholders);
9432
- Evaluator.inject(variables);
9433
9431
 
9434
- let output = "";
9435
- let prev_body_node = null;
9436
- let prev_was_silent = false;
9432
+ return withEvaluator(async () => {
9433
+ // Initialize Logic Sandbox inside isolated async context
9434
+ await Evaluator.init(fileBaseDir, security, settings, targetMapper);
9435
+ Evaluator.inject(placeholders);
9436
+ Evaluator.inject(variables);
9437
9437
 
9438
- if (dualOutput) {
9439
- const idState = { mode: 'record', ids: [], idx: 0 };
9438
+ let output = "";
9439
+ let prev_body_node = null;
9440
+ let prev_was_silent = false;
9440
9441
 
9441
- // HTML pass — generate HTML, record element IDs for runtime blocks
9442
- let htmlOutput = "";
9443
- try {
9444
- for (let i = 0; i < body.length; i++) {
9445
- const node = body[i];
9446
- const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, true, instance, idState);
9447
- let finalBlockOutput = blockOutput;
9448
- if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
9449
- if (finalBlockOutput) {
9450
- htmlOutput += finalBlockOutput;
9451
- prev_was_silent = false;
9452
- } else {
9453
- prev_was_silent = true;
9454
- if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
9455
- const nextNode = body[i + 1];
9456
- if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) i++;
9442
+ if (dualOutput) {
9443
+ const idState = { mode: 'record', ids: [], idx: 0 };
9444
+
9445
+ // HTML pass generate HTML, record element IDs for runtime blocks
9446
+ let htmlOutput = "";
9447
+ try {
9448
+ for (let i = 0; i < body.length; i++) {
9449
+ const node = body[i];
9450
+ const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, true, instance, idState);
9451
+ let finalBlockOutput = blockOutput;
9452
+ if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
9453
+ if (finalBlockOutput) {
9454
+ htmlOutput += finalBlockOutput;
9455
+ prev_was_silent = false;
9456
+ } else {
9457
+ prev_was_silent = true;
9458
+ if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
9459
+ const nextNode = body[i + 1];
9460
+ if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) i++;
9461
+ }
9457
9462
  }
9458
9463
  }
9464
+ } finally {
9465
+ Evaluator.destroy();
9459
9466
  }
9460
- } finally {
9461
- Evaluator.destroy();
9462
- }
9463
9467
 
9464
- // JS pass — replay the same IDs so querySelector targets match HTML
9465
- idState.mode = 'replay';
9466
- idState.idx = 0;
9467
- prev_was_silent = false;
9468
+ // JS pass — replay the same IDs so querySelector targets match HTML
9469
+ idState.mode = 'replay';
9470
+ idState.idx = 0;
9471
+ prev_was_silent = false;
9468
9472
 
9469
- await Evaluator.init(fileBaseDir, security, settings, targetMapper);
9470
- Evaluator.inject(placeholders);
9471
- Evaluator.inject(variables);
9473
+ await Evaluator.init(fileBaseDir, security, settings, targetMapper);
9474
+ Evaluator.inject(placeholders);
9475
+ Evaluator.inject(variables);
9476
+
9477
+ let jsOutput = "";
9478
+ try {
9479
+ for (let i = 0; i < body.length; i++) {
9480
+ const node = body[i];
9481
+ const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, true, false, instance, idState);
9482
+ let finalBlockOutput = blockOutput;
9483
+ if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
9484
+ if (finalBlockOutput) {
9485
+ jsOutput += finalBlockOutput;
9486
+ prev_was_silent = false;
9487
+ } else {
9488
+ prev_was_silent = true;
9489
+ }
9490
+ }
9491
+ } finally {
9492
+ Evaluator.destroy();
9493
+ }
9494
+
9495
+ return [htmlOutput.trim(), jsOutput.trim()];
9496
+ }
9472
9497
 
9473
- let jsOutput = "";
9474
9498
  try {
9475
9499
  for (let i = 0; i < body.length; i++) {
9476
9500
  const node = body[i];
9477
- const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, true, false, instance, idState);
9501
+ const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, false, instance);
9502
+
9478
9503
  let finalBlockOutput = blockOutput;
9479
- if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
9504
+ if (prev_was_silent && node.type === TEXT$1) {
9505
+ finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
9506
+ }
9507
+
9480
9508
  if (finalBlockOutput) {
9481
- jsOutput += finalBlockOutput;
9509
+ output += finalBlockOutput;
9482
9510
  prev_was_silent = false;
9511
+ if (node.type !== TEXT$1 || node.text.trim().length > 0) {
9512
+ prev_body_node = node;
9513
+ }
9483
9514
  } else {
9484
9515
  prev_was_silent = true;
9516
+ if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
9517
+ const nextNode = body[i + 1];
9518
+ if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
9519
+ i++;
9520
+ }
9521
+ }
9485
9522
  }
9486
9523
  }
9487
9524
  } finally {
9488
9525
  Evaluator.destroy();
9489
9526
  }
9490
9527
 
9491
- return [htmlOutput.trim(), jsOutput.trim()];
9492
- }
9493
-
9494
- try {
9495
- for (let i = 0; i < body.length; i++) {
9496
- const node = body[i];
9497
- const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, false, instance);
9498
-
9499
- let finalBlockOutput = blockOutput;
9500
- if (prev_was_silent && node.type === TEXT$1) {
9501
- finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
9502
- }
9503
-
9504
- if (finalBlockOutput) {
9505
- output += finalBlockOutput;
9506
- prev_was_silent = false;
9507
- if (node.type !== TEXT$1 || node.text.trim().length > 0) {
9508
- prev_body_node = node;
9509
- }
9510
- } else {
9511
- prev_was_silent = true;
9512
- if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
9513
- // If a comment is removed, check the next node.
9514
- // If it's just a blank line, skip it so we don't have extra gaps in the output.
9515
- const nextNode = body[i + 1];
9516
- if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
9517
- i++; // Skip the next newline node
9518
- }
9519
- }
9520
- }
9521
- }
9522
- } finally {
9523
- Evaluator.destroy();
9524
- }
9525
-
9526
- return output.trim();
9528
+ return output.trim();
9529
+ });
9527
9530
  }
9528
9531
 
9529
9532
  /**
@@ -9546,7 +9549,7 @@ async function transpileArgs(props) {
9546
9549
  result[key] = "";
9547
9550
  } else if (value.type === STATIC_LOGIC) {
9548
9551
  try {
9549
- result[key] = await Evaluator.execute(value.code);
9552
+ result[key] = await Evaluator.execute(value.code, value.baseDir || null);
9550
9553
  } catch (err) {
9551
9554
  transpilerError([
9552
9555
  `<$red:Logic Error (Argument):$> ${err.message}{line}`,
@@ -12184,7 +12187,10 @@ const resolveAstVariables = (nodes, variables) => {
12184
12187
  for (const node of nodes) {
12185
12188
  if (node.type === TEXT$1) {
12186
12189
  if (node.text.includes(VAR_PREFIX)) {
12187
- node.text = node.text.replace(VAR_PATTERN, (match, key) => {
12190
+ node.text = node.text.replace(VAR_PATTERN, (match, keyAndFallback) => {
12191
+ const pipeIdx = keyAndFallback.indexOf('|');
12192
+ const key = pipeIdx >= 0 ? keyAndFallback.slice(0, pipeIdx) : keyAndFallback;
12193
+ const fallback = pipeIdx >= 0 ? keyAndFallback.slice(pipeIdx + 1) : undefined;
12188
12194
  if (variables[key] !== undefined) {
12189
12195
  if (!variables.__consumed__) {
12190
12196
  Object.defineProperty(variables, "__consumed__", {
@@ -12197,14 +12203,21 @@ const resolveAstVariables = (nodes, variables) => {
12197
12203
  variables.__consumed__.add(key);
12198
12204
  return String(variables[key]);
12199
12205
  }
12206
+ if (fallback !== undefined) return fallback;
12200
12207
  return match;
12201
12208
  });
12202
12209
  }
12203
12210
  } else if (node.type === BLOCK) {
12204
12211
  // Resolve any unresolved variables in block arguments
12205
12212
  for (const [argKey, argVal] of Object.entries(node.props)) {
12206
- if (typeof argVal === "string" && argVal.startsWith(VAR_PREFIX) && argVal.endsWith(VAR_SUFFIX)) {
12207
- const varKey = argVal.slice(VAR_PREFIX.length, -VAR_SUFFIX.length);
12213
+ if (typeof argVal !== "string" || !argVal.includes(VAR_PREFIX)) continue;
12214
+
12215
+ if (argVal.startsWith(VAR_PREFIX) && argVal.endsWith(VAR_SUFFIX)) {
12216
+ // Entire value is an envelope — resolve to scalar or fallback
12217
+ const keyAndFallback = argVal.slice(VAR_PREFIX.length, -VAR_SUFFIX.length);
12218
+ const pipeIdx = keyAndFallback.indexOf('|');
12219
+ const varKey = pipeIdx >= 0 ? keyAndFallback.slice(0, pipeIdx) : keyAndFallback;
12220
+ const fallback = pipeIdx >= 0 ? keyAndFallback.slice(pipeIdx + 1) : undefined;
12208
12221
  if (variables[varKey] !== undefined) {
12209
12222
  node.props[argKey] = variables[varKey];
12210
12223
  if (!variables.__consumed__) {
@@ -12216,7 +12229,31 @@ const resolveAstVariables = (nodes, variables) => {
12216
12229
  });
12217
12230
  }
12218
12231
  variables.__consumed__.add(varKey);
12232
+ } else if (fallback !== undefined) {
12233
+ node.props[argKey] = fallback;
12219
12234
  }
12235
+ } else {
12236
+ // Envelope embedded inside a larger string — replace in-place.
12237
+ // Unresolved envelopes become "" so they don't pollute class names etc.
12238
+ node.props[argKey] = argVal.replace(VAR_PATTERN, (match, keyAndFallback) => {
12239
+ const pipeIdx = keyAndFallback.indexOf('|');
12240
+ const key = pipeIdx >= 0 ? keyAndFallback.slice(0, pipeIdx) : keyAndFallback;
12241
+ const fallback = pipeIdx >= 0 ? keyAndFallback.slice(pipeIdx + 1) : undefined;
12242
+ if (variables[key] !== undefined) {
12243
+ if (!variables.__consumed__) {
12244
+ Object.defineProperty(variables, "__consumed__", {
12245
+ value: new Set(),
12246
+ writable: true,
12247
+ enumerable: false,
12248
+ configurable: true
12249
+ });
12250
+ }
12251
+ variables.__consumed__.add(key);
12252
+ return String(variables[key]);
12253
+ }
12254
+ if (fallback !== undefined) return fallback;
12255
+ return "";
12256
+ });
12220
12257
  }
12221
12258
  }
12222
12259
  if (node.body) {
@@ -12246,6 +12283,7 @@ const cloneAst = (nodes) => {
12246
12283
  if (node.id !== undefined) nodeCopy.id = node.id;
12247
12284
  if (node.code !== undefined) nodeCopy.code = node.code;
12248
12285
  if (node.isSelfClosing !== undefined) nodeCopy.isSelfClosing = node.isSelfClosing;
12286
+ if (node.baseDir !== undefined) nodeCopy.baseDir = node.baseDir;
12249
12287
  if (node.props !== undefined) {
12250
12288
  nodeCopy.props = { ...node.props };
12251
12289
  }
@@ -12257,6 +12295,20 @@ const cloneAst = (nodes) => {
12257
12295
  return copy;
12258
12296
  };
12259
12297
 
12298
+ /**
12299
+ * Tags all STATIC_LOGIC and RUNTIME_LOGIC nodes in a subtree with their
12300
+ * source module's baseDir so the evaluator can resolve imports correctly.
12301
+ */
12302
+ const tagLogicNodes = (nodes, baseDir) => {
12303
+ if (!nodes) return;
12304
+ for (const node of nodes) {
12305
+ if ((node.type === STATIC_LOGIC || node.type === RUNTIME_LOGIC) && !node.baseDir) {
12306
+ node.baseDir = baseDir;
12307
+ }
12308
+ if (node.body) tagLogicNodes(node.body, baseDir);
12309
+ }
12310
+ };
12311
+
12260
12312
  /**
12261
12313
  * Handles all [import] and [$use-module] blocks in your code.
12262
12314
  * It loads the requested files, checks for errors, and puts the content into the main document.
@@ -12430,6 +12482,7 @@ async function resolveModules(ast, context) {
12430
12482
  format: context.format,
12431
12483
  filename: mod.path,
12432
12484
  baseDir: posix.dirname(mod.localPath),
12485
+ fs: context.instance.fs,
12433
12486
  mapperFile: context.instance.mapperFile,
12434
12487
  placeholders: context.instance.placeholders,
12435
12488
  variables: {},
@@ -12445,6 +12498,7 @@ async function resolveModules(ast, context) {
12445
12498
  });
12446
12499
 
12447
12500
  const subAst = await subSmark.parse();
12501
+ tagLogicNodes(subAst, posix.dirname(mod.localPath));
12448
12502
  context.instance.moduleCache.set(mod.localPath, subAst);
12449
12503
  expandedNodes = trimAst(subAst);
12450
12504
  }
@@ -12488,6 +12542,7 @@ async function resolveModules(ast, context) {
12488
12542
  format: context.format,
12489
12543
  filename: mod.path,
12490
12544
  baseDir: posix.dirname(mod.localPath),
12545
+ fs: context.instance.fs,
12491
12546
  mapperFile: context.instance.mapperFile,
12492
12547
  placeholders: context.instance.placeholders,
12493
12548
  variables: {}, // Parse without variables to keep the cached AST pure
@@ -12503,6 +12558,7 @@ async function resolveModules(ast, context) {
12503
12558
  });
12504
12559
 
12505
12560
  subAst = await subSmark.parse();
12561
+ tagLogicNodes(subAst, posix.dirname(mod.localPath));
12506
12562
  context.instance.moduleCache.set(mod.localPath, subAst);
12507
12563
  subAst = cloneAst(subAst);
12508
12564
  }
@@ -12572,6 +12628,13 @@ async function resolveModules(ast, context) {
12572
12628
  return ast;
12573
12629
  }
12574
12630
 
12631
+ /**
12632
+ * After full transpilation of the top-level file, apply any v{} fallbacks that
12633
+ * remain unresolved. Envelopes with no fallback are kept as-is (debugging signal).
12634
+ * Must NOT be called on sub-module ASTs — only on the final top-level AST.
12635
+ */
12636
+ const applyVariableFallbacks = (ast) => resolveAstVariables(ast, {});
12637
+
12575
12638
  /**
12576
12639
  * SomMark Rules Validator
12577
12640
  *
@@ -13066,6 +13129,7 @@ class SomMark {
13066
13129
  if (this.showSpinner) startSpinner();
13067
13130
  try {
13068
13131
  const ast = this.ast || await this.parse(src);
13132
+ applyVariableFallbacks(ast);
13069
13133
  let result = await transpiler({
13070
13134
  ast,
13071
13135
  format: this.targetFormat,
@@ -1341,7 +1341,9 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
1341
1341
  }
1342
1342
  variables.__consumed__.add(vKey);
1343
1343
  } else {
1344
- val = vFallback !== undefined ? vFallback : getPrefixValue('v', vKey);
1344
+ // Encode fallback in the envelope key so resolveAstVariables can apply it
1345
+ // at instantiation time instead of baking it in now.
1346
+ val = getPrefixValue('v', vFallback !== undefined ? `${vKey}|${vFallback}` : vKey);
1345
1347
  }
1346
1348
  return [val, i, false];
1347
1349
  } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P) {
@@ -1735,7 +1737,8 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
1735
1737
  }
1736
1738
  variables.__consumed__.add(tvKey);
1737
1739
  } else {
1738
- textNode.text += tvFallback !== undefined ? tvFallback : getPrefixValue('v', tvKey);
1740
+ // Encode fallback in envelope so resolveAstVariables can apply it later.
1741
+ textNode.text += getPrefixValue('v', tvFallback !== undefined ? `${tvKey}|${tvFallback}` : tvKey);
1739
1742
  }
1740
1743
  } else {
1741
1744
  break;
package/index.shared.js CHANGED
@@ -19,7 +19,7 @@ import { runtimeError } from "./core/errors.js";
19
19
  import FORMATS, { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat, jsoncFormat, xmlFormat, csvFormat, tomlFormat, yamlFormat } from "./core/formats.js";
20
20
  import TOKEN_TYPES from "./core/tokenTypes.js";
21
21
  import * as labels from "./core/labels.js";
22
- import { resolveModules } from "./core/modules.js";
22
+ import { resolveModules, applyVariableFallbacks } from "./core/modules.js";
23
23
  import { validateAST } from "./core/validator.js";
24
24
  import { enableColor } from "./helpers/colorize.js";
25
25
  import { safeArg } from "./helpers/utils.js";
@@ -292,6 +292,7 @@ class SomMark {
292
292
  if (this.showSpinner) startSpinner();
293
293
  try {
294
294
  const ast = this.ast || await this.parse(src);
295
+ applyVariableFallbacks(ast);
295
296
  let result = await transpiler({
296
297
  ast,
297
298
  format: this.targetFormat,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sommark",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "description": "SomMark is a template language that compiles to multiple output formats — HTML, JSON, YAML, TOML, CSV, Markdown, XML, and more.",
5
5
  "main": "index.js",
6
6
  "files": [