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.
@@ -801,13 +801,6 @@ function lexer(src, filename = "anonymous") {
801
801
  // LOGIC BLOCKS (${ ... }$) — explicit: static/runtime ${ }$ shorthand: ${ }$ = static ${ }$
802
802
  if (char === "$" && next === "{") {
803
803
  {
804
- const hasExplicitKeyword = last_non_junk_type === TOKEN_TYPES.STATIC_KEYWORD || last_non_junk_type === TOKEN_TYPES.RUNTIME_KEYWORD;
805
- if (!hasExplicitKeyword) {
806
- // Zero-width: synthetic token has no source presence, must not shift position
807
- tokens.push({ type: TOKEN_TYPES.STATIC_KEYWORD, value: "static", source: filename, range: { start: { line, character }, end: { line, character } } });
808
- TOKEN_TYPES.STATIC_KEYWORD;
809
- last_non_junk_type = TOKEN_TYPES.STATIC_KEYWORD;
810
- }
811
804
  addToken(TOKEN_TYPES.LOGIC_OPEN, "${");
812
805
  i += 2;
813
806
 
@@ -1761,6 +1754,22 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
1761
1754
  nextI++;
1762
1755
  }
1763
1756
 
1757
+ return [node, nextI, false];
1758
+ } else if (current_token(tokens, i).type === TOKEN_TYPES.LOGIC_OPEN) {
1759
+ if (!allowLogic) {
1760
+ parserError(errorMessage(tokens, i, "literal value", "", "Logic blocks are not allowed in this context."));
1761
+ }
1762
+ let nextI = i + 1;
1763
+ const logicToken = current_token(tokens, nextI);
1764
+ const node = makeLogicNode(STATIC_LOGIC);
1765
+ node.code = logicToken ? logicToken.value : "";
1766
+ node.range = logicToken ? logicToken.range : current_token(tokens, i).range;
1767
+ nextI++;
1768
+
1769
+ if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
1770
+ nextI++;
1771
+ }
1772
+
1764
1773
  return [node, nextI, false];
1765
1774
  } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_V) {
1766
1775
  i++; // consume PREFIX_V keyword
@@ -1777,7 +1786,9 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
1777
1786
  }
1778
1787
  variables.__consumed__.add(vKey);
1779
1788
  } else {
1780
- 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);
1781
1792
  }
1782
1793
  return [val, i, false];
1783
1794
  } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P) {
@@ -2171,7 +2182,8 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
2171
2182
  }
2172
2183
  variables.__consumed__.add(tvKey);
2173
2184
  } else {
2174
- 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);
2175
2187
  }
2176
2188
  } else {
2177
2189
  break;
@@ -2269,6 +2281,27 @@ function parseNode(tokens, i, filename = null, placeholders = {}, variables = {}
2269
2281
  return [node, nextI];
2270
2282
  }
2271
2283
  // ========================================================================== //
2284
+ // Bare Logic Block (${ }$ without explicit static/runtime — defaults to static)
2285
+ // ========================================================================== //
2286
+ else if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.LOGIC_OPEN) {
2287
+ let nextI = i + 1;
2288
+ const logicToken = current_token(tokens, nextI);
2289
+ const node = makeLogicNode(STATIC_LOGIC);
2290
+ node.code = logicToken ? logicToken.value : "";
2291
+ node.depth = depth;
2292
+ node.range = {
2293
+ start: current_token(tokens, i).range.start,
2294
+ end: logicToken ? logicToken.range.end : current_token(tokens, i).range.end
2295
+ };
2296
+ nextI++;
2297
+
2298
+ if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
2299
+ nextI++;
2300
+ }
2301
+
2302
+ return [node, nextI];
2303
+ }
2304
+ // ========================================================================== //
2272
2305
  // Text or Placeholder //
2273
2306
  // ========================================================================== //
2274
2307
  else if (
@@ -2353,6 +2386,7 @@ function parser(tokens, filename = null, placeholders = {}, variables = {}) {
2353
2386
  const LITE_ERROR =
2354
2387
  "[SomMark lite] static ${}$ and runtime ${}$ blocks are not supported in lite mode. " +
2355
2388
  "Use the full SomMark bundle to enable JS evaluation.";
2389
+ function withEvaluator(fn) { return fn(); }
2356
2390
 
2357
2391
  class EvaluatorStub {
2358
2392
  setDefaultFs(_fs) {}
@@ -8954,7 +8988,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
8954
8988
 
8955
8989
  if (node.type === STATIC_LOGIC) {
8956
8990
  try {
8957
- const result = await Evaluator.execute(node.code);
8991
+ const result = await Evaluator.execute(node.code, node.baseDir || null);
8958
8992
  if (generateRuntimeOutput) return "";
8959
8993
  if (result && typeof result === "object" && result.__raw !== undefined) {
8960
8994
  if (security?.allowRaw === false) {
@@ -9160,7 +9194,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9160
9194
  }
9161
9195
  } else if (child.type === STATIC_LOGIC) {
9162
9196
  try {
9163
- const val = await Evaluator.execute(child.code);
9197
+ const val = await Evaluator.execute(child.code, child.baseDir || null);
9164
9198
  if (val !== undefined && typeof val !== "object") richText += String(val);
9165
9199
  } catch (err) {
9166
9200
  transpilerError([
@@ -9277,7 +9311,7 @@ async function generateOutput(ast, i, format, mapper_file, security = {}, parent
9277
9311
 
9278
9312
  case STATIC_LOGIC:
9279
9313
  try {
9280
- const result = await Evaluator.execute(body_node.code);
9314
+ const result = await Evaluator.execute(body_node.code, body_node.baseDir || null);
9281
9315
  if (result && typeof result === "object" && result.__raw !== undefined) {
9282
9316
  if (security?.allowRaw === false) {
9283
9317
  bodyOutput = mapper_file ? mapper_file.text(String(result.__raw)) : String(result.__raw);
@@ -9391,109 +9425,108 @@ async function transpiler(optionsOrAst, format, mapperFile) {
9391
9425
  })();
9392
9426
 
9393
9427
  const dualOutput = optionsOrAst?.dualOutput || false;
9394
-
9395
- // Initialize Logic Sandbox
9396
- await Evaluator.init(fileBaseDir, security, settings, targetMapper);
9397
- // Inject global data
9398
9428
  const placeholders = optionsOrAst?.placeholders || settings?.placeholders || {};
9399
9429
  const variables = optionsOrAst?.variables || settings?.variables || {};
9400
9430
  warnDroppedVariables(variables);
9401
- Evaluator.inject(placeholders);
9402
- Evaluator.inject(variables);
9403
9431
 
9404
- let output = "";
9405
- let prev_body_node = null;
9406
- 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);
9407
9437
 
9408
- if (dualOutput) {
9409
- const idState = { mode: 'record', ids: [], idx: 0 };
9438
+ let output = "";
9439
+ let prev_body_node = null;
9440
+ let prev_was_silent = false;
9410
9441
 
9411
- // HTML pass — generate HTML, record element IDs for runtime blocks
9412
- let htmlOutput = "";
9413
- try {
9414
- for (let i = 0; i < body.length; i++) {
9415
- const node = body[i];
9416
- const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, true, instance, idState);
9417
- let finalBlockOutput = blockOutput;
9418
- if (prev_was_silent && node.type === TEXT$1) finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
9419
- if (finalBlockOutput) {
9420
- htmlOutput += finalBlockOutput;
9421
- prev_was_silent = false;
9422
- } else {
9423
- prev_was_silent = true;
9424
- if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
9425
- const nextNode = body[i + 1];
9426
- 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
+ }
9427
9462
  }
9428
9463
  }
9464
+ } finally {
9465
+ Evaluator.destroy();
9429
9466
  }
9430
- } finally {
9431
- Evaluator.destroy();
9432
- }
9433
9467
 
9434
- // JS pass — replay the same IDs so querySelector targets match HTML
9435
- idState.mode = 'replay';
9436
- idState.idx = 0;
9437
- 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;
9438
9472
 
9439
- await Evaluator.init(fileBaseDir, security, settings, targetMapper);
9440
- Evaluator.inject(placeholders);
9441
- 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
+ }
9442
9497
 
9443
- let jsOutput = "";
9444
9498
  try {
9445
9499
  for (let i = 0; i < body.length; i++) {
9446
9500
  const node = body[i];
9447
- 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
+
9448
9503
  let finalBlockOutput = blockOutput;
9449
- 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
+
9450
9508
  if (finalBlockOutput) {
9451
- jsOutput += finalBlockOutput;
9509
+ output += finalBlockOutput;
9452
9510
  prev_was_silent = false;
9511
+ if (node.type !== TEXT$1 || node.text.trim().length > 0) {
9512
+ prev_body_node = node;
9513
+ }
9453
9514
  } else {
9454
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
+ }
9455
9522
  }
9456
9523
  }
9457
9524
  } finally {
9458
9525
  Evaluator.destroy();
9459
9526
  }
9460
9527
 
9461
- return [htmlOutput.trim(), jsOutput.trim()];
9462
- }
9463
-
9464
- try {
9465
- for (let i = 0; i < body.length; i++) {
9466
- const node = body[i];
9467
- const blockOutput = await generateOutput(body, i, targetFormat, targetMapper, security, null, false, false, instance);
9468
-
9469
- let finalBlockOutput = blockOutput;
9470
- if (prev_was_silent && node.type === TEXT$1) {
9471
- finalBlockOutput = finalBlockOutput.replace(/^\n/, "");
9472
- }
9473
-
9474
- if (finalBlockOutput) {
9475
- output += finalBlockOutput;
9476
- prev_was_silent = false;
9477
- if (node.type !== TEXT$1 || node.text.trim().length > 0) {
9478
- prev_body_node = node;
9479
- }
9480
- } else {
9481
- prev_was_silent = true;
9482
- if ((node.type === COMMENT || node.type === COMMENT_BLOCK) && targetMapper?.options?.removeComments) {
9483
- // If a comment is removed, check the next node.
9484
- // If it's just a blank line, skip it so we don't have extra gaps in the output.
9485
- const nextNode = body[i + 1];
9486
- if (nextNode && nextNode.type === TEXT$1 && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
9487
- i++; // Skip the next newline node
9488
- }
9489
- }
9490
- }
9491
- }
9492
- } finally {
9493
- Evaluator.destroy();
9494
- }
9495
-
9496
- return output.trim();
9528
+ return output.trim();
9529
+ });
9497
9530
  }
9498
9531
 
9499
9532
  /**
@@ -9516,7 +9549,7 @@ async function transpileArgs(props) {
9516
9549
  result[key] = "";
9517
9550
  } else if (value.type === STATIC_LOGIC) {
9518
9551
  try {
9519
- result[key] = await Evaluator.execute(value.code);
9552
+ result[key] = await Evaluator.execute(value.code, value.baseDir || null);
9520
9553
  } catch (err) {
9521
9554
  transpilerError([
9522
9555
  `<$red:Logic Error (Argument):$> ${err.message}{line}`,
@@ -12154,7 +12187,10 @@ const resolveAstVariables = (nodes, variables) => {
12154
12187
  for (const node of nodes) {
12155
12188
  if (node.type === TEXT$1) {
12156
12189
  if (node.text.includes(VAR_PREFIX)) {
12157
- 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;
12158
12194
  if (variables[key] !== undefined) {
12159
12195
  if (!variables.__consumed__) {
12160
12196
  Object.defineProperty(variables, "__consumed__", {
@@ -12167,14 +12203,21 @@ const resolveAstVariables = (nodes, variables) => {
12167
12203
  variables.__consumed__.add(key);
12168
12204
  return String(variables[key]);
12169
12205
  }
12206
+ if (fallback !== undefined) return fallback;
12170
12207
  return match;
12171
12208
  });
12172
12209
  }
12173
12210
  } else if (node.type === BLOCK) {
12174
12211
  // Resolve any unresolved variables in block arguments
12175
12212
  for (const [argKey, argVal] of Object.entries(node.props)) {
12176
- if (typeof argVal === "string" && argVal.startsWith(VAR_PREFIX) && argVal.endsWith(VAR_SUFFIX)) {
12177
- 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;
12178
12221
  if (variables[varKey] !== undefined) {
12179
12222
  node.props[argKey] = variables[varKey];
12180
12223
  if (!variables.__consumed__) {
@@ -12186,7 +12229,31 @@ const resolveAstVariables = (nodes, variables) => {
12186
12229
  });
12187
12230
  }
12188
12231
  variables.__consumed__.add(varKey);
12232
+ } else if (fallback !== undefined) {
12233
+ node.props[argKey] = fallback;
12189
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
+ });
12190
12257
  }
12191
12258
  }
12192
12259
  if (node.body) {
@@ -12216,6 +12283,7 @@ const cloneAst = (nodes) => {
12216
12283
  if (node.id !== undefined) nodeCopy.id = node.id;
12217
12284
  if (node.code !== undefined) nodeCopy.code = node.code;
12218
12285
  if (node.isSelfClosing !== undefined) nodeCopy.isSelfClosing = node.isSelfClosing;
12286
+ if (node.baseDir !== undefined) nodeCopy.baseDir = node.baseDir;
12219
12287
  if (node.props !== undefined) {
12220
12288
  nodeCopy.props = { ...node.props };
12221
12289
  }
@@ -12227,6 +12295,20 @@ const cloneAst = (nodes) => {
12227
12295
  return copy;
12228
12296
  };
12229
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
+
12230
12312
  /**
12231
12313
  * Handles all [import] and [$use-module] blocks in your code.
12232
12314
  * It loads the requested files, checks for errors, and puts the content into the main document.
@@ -12400,6 +12482,7 @@ async function resolveModules(ast, context) {
12400
12482
  format: context.format,
12401
12483
  filename: mod.path,
12402
12484
  baseDir: posix.dirname(mod.localPath),
12485
+ fs: context.instance.fs,
12403
12486
  mapperFile: context.instance.mapperFile,
12404
12487
  placeholders: context.instance.placeholders,
12405
12488
  variables: {},
@@ -12415,6 +12498,7 @@ async function resolveModules(ast, context) {
12415
12498
  });
12416
12499
 
12417
12500
  const subAst = await subSmark.parse();
12501
+ tagLogicNodes(subAst, posix.dirname(mod.localPath));
12418
12502
  context.instance.moduleCache.set(mod.localPath, subAst);
12419
12503
  expandedNodes = trimAst(subAst);
12420
12504
  }
@@ -12458,6 +12542,7 @@ async function resolveModules(ast, context) {
12458
12542
  format: context.format,
12459
12543
  filename: mod.path,
12460
12544
  baseDir: posix.dirname(mod.localPath),
12545
+ fs: context.instance.fs,
12461
12546
  mapperFile: context.instance.mapperFile,
12462
12547
  placeholders: context.instance.placeholders,
12463
12548
  variables: {}, // Parse without variables to keep the cached AST pure
@@ -12473,6 +12558,7 @@ async function resolveModules(ast, context) {
12473
12558
  });
12474
12559
 
12475
12560
  subAst = await subSmark.parse();
12561
+ tagLogicNodes(subAst, posix.dirname(mod.localPath));
12476
12562
  context.instance.moduleCache.set(mod.localPath, subAst);
12477
12563
  subAst = cloneAst(subAst);
12478
12564
  }
@@ -12542,6 +12628,13 @@ async function resolveModules(ast, context) {
12542
12628
  return ast;
12543
12629
  }
12544
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
+
12545
12638
  /**
12546
12639
  * SomMark Rules Validator
12547
12640
  *
@@ -13036,6 +13129,7 @@ class SomMark {
13036
13129
  if (this.showSpinner) startSpinner();
13037
13130
  try {
13038
13131
  const ast = this.ast || await this.parse(src);
13132
+ applyVariableFallbacks(ast);
13039
13133
  let result = await transpiler({
13040
13134
  ast,
13041
13135
  format: this.targetFormat,
@@ -451,13 +451,6 @@ function lexer(src, filename = "anonymous") {
451
451
  // LOGIC BLOCKS (${ ... }$) — explicit: static/runtime ${ }$ shorthand: ${ }$ = static ${ }$
452
452
  if (char === "$" && next === "{") {
453
453
  {
454
- const hasExplicitKeyword = last_non_junk_type === TOKEN_TYPES.STATIC_KEYWORD || last_non_junk_type === TOKEN_TYPES.RUNTIME_KEYWORD;
455
- if (!hasExplicitKeyword) {
456
- // Zero-width: synthetic token has no source presence, must not shift position
457
- tokens.push({ type: TOKEN_TYPES.STATIC_KEYWORD, value: "static", source: filename, range: { start: { line, character }, end: { line, character } } });
458
- TOKEN_TYPES.STATIC_KEYWORD;
459
- last_non_junk_type = TOKEN_TYPES.STATIC_KEYWORD;
460
- }
461
454
  addToken(TOKEN_TYPES.LOGIC_OPEN, "${");
462
455
  i += 2;
463
456
 
@@ -472,13 +472,6 @@ function lexer(src, filename = "anonymous") {
472
472
  // LOGIC BLOCKS (${ ... }$) — explicit: static/runtime ${ }$ shorthand: ${ }$ = static ${ }$
473
473
  if (char === "$" && next === "{") {
474
474
  {
475
- const hasExplicitKeyword = last_non_junk_type === TOKEN_TYPES.STATIC_KEYWORD || last_non_junk_type === TOKEN_TYPES.RUNTIME_KEYWORD;
476
- if (!hasExplicitKeyword) {
477
- // Zero-width: synthetic token has no source presence, must not shift position
478
- tokens.push({ type: TOKEN_TYPES.STATIC_KEYWORD, value: "static", source: filename, range: { start: { line, character }, end: { line, character } } });
479
- TOKEN_TYPES.STATIC_KEYWORD;
480
- last_non_junk_type = TOKEN_TYPES.STATIC_KEYWORD;
481
- }
482
475
  addToken(TOKEN_TYPES.LOGIC_OPEN, "${");
483
476
  i += 2;
484
477
 
@@ -1316,6 +1309,22 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
1316
1309
  nextI++;
1317
1310
  }
1318
1311
 
1312
+ return [node, nextI, false];
1313
+ } else if (current_token(tokens, i).type === TOKEN_TYPES.LOGIC_OPEN) {
1314
+ if (!allowLogic) {
1315
+ parserError(errorMessage(tokens, i, "literal value", "", "Logic blocks are not allowed in this context."));
1316
+ }
1317
+ let nextI = i + 1;
1318
+ const logicToken = current_token(tokens, nextI);
1319
+ const node = makeLogicNode(STATIC_LOGIC);
1320
+ node.code = logicToken ? logicToken.value : "";
1321
+ node.range = logicToken ? logicToken.range : current_token(tokens, i).range;
1322
+ nextI++;
1323
+
1324
+ if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
1325
+ nextI++;
1326
+ }
1327
+
1319
1328
  return [node, nextI, false];
1320
1329
  } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_V) {
1321
1330
  i++; // consume PREFIX_V keyword
@@ -1332,7 +1341,9 @@ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = t
1332
1341
  }
1333
1342
  variables.__consumed__.add(vKey);
1334
1343
  } else {
1335
- 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);
1336
1347
  }
1337
1348
  return [val, i, false];
1338
1349
  } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P) {
@@ -1726,7 +1737,8 @@ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, opti
1726
1737
  }
1727
1738
  variables.__consumed__.add(tvKey);
1728
1739
  } else {
1729
- 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);
1730
1742
  }
1731
1743
  } else {
1732
1744
  break;
@@ -1824,6 +1836,27 @@ function parseNode(tokens, i, filename = null, placeholders = {}, variables = {}
1824
1836
  return [node, nextI];
1825
1837
  }
1826
1838
  // ========================================================================== //
1839
+ // Bare Logic Block (${ }$ without explicit static/runtime — defaults to static)
1840
+ // ========================================================================== //
1841
+ else if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.LOGIC_OPEN) {
1842
+ let nextI = i + 1;
1843
+ const logicToken = current_token(tokens, nextI);
1844
+ const node = makeLogicNode(STATIC_LOGIC);
1845
+ node.code = logicToken ? logicToken.value : "";
1846
+ node.depth = depth;
1847
+ node.range = {
1848
+ start: current_token(tokens, i).range.start,
1849
+ end: logicToken ? logicToken.range.end : current_token(tokens, i).range.end
1850
+ };
1851
+ nextI++;
1852
+
1853
+ if (current_token(tokens, nextI) && current_token(tokens, nextI).type === TOKEN_TYPES.LOGIC_CLOSE) {
1854
+ nextI++;
1855
+ }
1856
+
1857
+ return [node, nextI];
1858
+ }
1859
+ // ========================================================================== //
1827
1860
  // Text or Placeholder //
1828
1861
  // ========================================================================== //
1829
1862
  else if (
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.2",
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": [