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
package/core/parser.js CHANGED
@@ -11,6 +11,9 @@ import {
11
11
  INLINE,
12
12
  ATBLOCK,
13
13
  COMMENT,
14
+ COMMENT_BLOCK,
15
+ STATIC_LOGIC,
16
+ RUNTIME_LOGIC,
14
17
  IMPORT,
15
18
  USE_MODULE,
16
19
  block_id,
@@ -21,9 +24,16 @@ import {
21
24
  at_id,
22
25
  atblock_key,
23
26
  at_value,
24
- end_keyword
27
+ end_keyword,
28
+ SLOT,
29
+ slot_keyword,
30
+ FOR_EACH,
31
+ for_each_keyword
25
32
  } from "./labels.js";
26
- import { levenshtein } from "../helpers/utils.js";
33
+ import { levenshtein, getPrefixValue } from "../helpers/utils.js";
34
+
35
+ const MAX_ITERATIONS = 10000;
36
+
27
37
 
28
38
  // ========================================================================== //
29
39
  // Helper Functions //
@@ -51,7 +61,7 @@ function skipJunk(tokens, i) {
51
61
  while (i < tokens.length) {
52
62
  const t = tokens[i];
53
63
  const type = t.type;
54
- if (type === TOKEN_TYPES.WHITESPACE || type === TOKEN_TYPES.COMMENT) {
64
+ if (type === TOKEN_TYPES.WHITESPACE || type === TOKEN_TYPES.COMMENT || type === TOKEN_TYPES.COMMENT_BLOCK) {
55
65
  i++;
56
66
  } else if (type === TOKEN_TYPES.TEXT && t.value.trim() === "") {
57
67
  i++;
@@ -91,6 +101,7 @@ function validateName(
91
101
  function makeBlockNode() {
92
102
  return {
93
103
  type: BLOCK,
104
+ structure: "Block",
94
105
  id: "",
95
106
  args: {},
96
107
  body: [],
@@ -105,6 +116,7 @@ function makeBlockNode() {
105
116
  function makeTextNode() {
106
117
  return {
107
118
  type: TEXT,
119
+ structure: "Text",
108
120
  text: "",
109
121
  depth: 0,
110
122
  range: {
@@ -117,6 +129,7 @@ function makeTextNode() {
117
129
  function makeCommentNode() {
118
130
  return {
119
131
  type: COMMENT,
132
+ structure: "Comment",
120
133
  text: "",
121
134
  depth: 0,
122
135
  range: {
@@ -129,6 +142,7 @@ function makeCommentNode() {
129
142
  function makeInlineNode() {
130
143
  return {
131
144
  type: INLINE,
145
+ structure: "Inline",
132
146
  value: "",
133
147
  id: "",
134
148
  args: {},
@@ -147,6 +161,7 @@ function makeInlineNode() {
147
161
  function makeAtBlockNode() {
148
162
  return {
149
163
  type: ATBLOCK,
164
+ structure: "AtBlock",
150
165
  id: "",
151
166
  args: {},
152
167
  content: "",
@@ -158,10 +173,25 @@ function makeAtBlockNode() {
158
173
  };
159
174
  }
160
175
 
176
+ /** Creates a new empty Logic node. */
177
+ function makeLogicNode(type = RUNTIME_LOGIC) {
178
+ return {
179
+ type: type,
180
+ structure: "Block",
181
+ code: "",
182
+ depth: 0,
183
+ range: {
184
+ start: { line: 0, character: 0 },
185
+ end: { line: 0, character: 0 }
186
+ }
187
+ };
188
+ }
189
+
161
190
  // ========================================================================== //
162
191
  // Parser State and Error Tracking //
163
192
  // ========================================================================== //
164
193
 
194
+ let global_static_logic_count = 0;
165
195
  let end_stack = [];
166
196
  let tokens_stack = [];
167
197
  let range = {
@@ -258,7 +288,7 @@ function parseKey(tokens, i) {
258
288
  // ========================================================================== //
259
289
  // Parse Value //
260
290
  // ========================================================================== //
261
- function parseValue(tokens, i, placeholders = {}) {
291
+ function parseValue(tokens, i, placeholders = {}, variables = {}, allowLogic = true) {
262
292
  let val = current_token(tokens, i).value;
263
293
  // consume Value
264
294
  if (current_token(tokens, i).type === TOKEN_TYPES.QUOTE) {
@@ -266,8 +296,8 @@ function parseValue(tokens, i, placeholders = {}) {
266
296
  val = "";
267
297
  while (i < tokens.length && current_token(tokens, i).type !== TOKEN_TYPES.QUOTE) {
268
298
  const token = current_token(tokens, i);
269
- if (token.type === TOKEN_TYPES.PREFIX_P || token.type === TOKEN_TYPES.PREFIX_JS) {
270
- const [resolvedVal, nextI] = parseValue(tokens, i, placeholders);
299
+ if (token.type === TOKEN_TYPES.PREFIX_P || token.type === TOKEN_TYPES.PREFIX_JS || token.type === TOKEN_TYPES.PREFIX_V) {
300
+ const [resolvedVal, nextI] = parseValue(tokens, i, placeholders, variables, allowLogic);
271
301
  val += resolvedVal;
272
302
  i = nextI;
273
303
  } else {
@@ -291,12 +321,61 @@ function parseValue(tokens, i, placeholders = {}) {
291
321
  }
292
322
  i++;
293
323
  return [val, i, false];
324
+ } else if (current_token(tokens, i).type === TOKEN_TYPES.LOGIC || current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD || current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD) {
325
+ if (!allowLogic) {
326
+ parserError(errorMessage(tokens, i, "literal value", "", "Logic blocks are not allowed in this context."));
327
+ }
328
+ let isStatic = current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD;
329
+ let isRuntimeKeyword = current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD;
330
+ let nextI = i;
331
+
332
+ if (isStatic || isRuntimeKeyword) {
333
+ nextI = skipJunk(tokens, i + 1);
334
+ if (!current_token(tokens, nextI) || current_token(tokens, nextI).type !== TOKEN_TYPES.LOGIC) {
335
+ // Treat as literal text if keyword is not followed by a logic block
336
+ return [current_token(tokens, i).value, i + 1, false];
337
+ }
338
+ i = nextI;
339
+ }
340
+
341
+ const logicToken = current_token(tokens, i);
342
+ const node = makeLogicNode(isStatic ? STATIC_LOGIC : RUNTIME_LOGIC);
343
+ node.code = logicToken.value;
344
+ node.range = logicToken.range;
345
+
346
+ return [node, i + 1, false];
347
+ } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_V) {
348
+ val = current_token(tokens, i).value;
349
+ // V4.1.0 VARIABLE: Strip v{ } and resolve from local variables
350
+ if (val.startsWith("v{") && val.endsWith("}")) {
351
+ const key = val.slice(2, -1).trim();
352
+ if (variables[key] !== undefined) {
353
+ val = variables[key];
354
+ if (!variables.__consumed__) {
355
+ Object.defineProperty(variables, "__consumed__", {
356
+ value: new Set(),
357
+ enumerable: false,
358
+ configurable: true
359
+ });
360
+ }
361
+ variables.__consumed__.add(key);
362
+ } else {
363
+ val = getPrefixValue('v', key);
364
+ }
365
+ }
366
+ i++;
367
+ return [val, i, false];
368
+ } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_C) {
369
+ val = current_token(tokens, i).value;
370
+ // PREFIX_C is preserved for the resolveModules expansion phase
371
+ i++;
372
+ return [val, i, false];
294
373
  } else if (current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P) {
295
374
  val = current_token(tokens, i).value;
296
375
  // V4 PLACEHOLDER: Strip p{ } and resolve from config
297
376
  if (val.startsWith("p{") && val.endsWith("}")) {
298
377
  const key = val.slice(2, -1).trim();
299
- val = placeholders[key] !== undefined ? placeholders[key] : val;
378
+ val = placeholders[key] !== undefined ? placeholders[key] : getPrefixValue('p', key);
300
379
  }
301
380
  i++;
302
381
  return [val, i, false];
@@ -312,6 +391,7 @@ function parseValue(tokens, i, placeholders = {}) {
312
391
  token.type === TOKEN_TYPES.CLOSE_BRACKET ||
313
392
  token.type === TOKEN_TYPES.COLON ||
314
393
  token.type === TOKEN_TYPES.SEMICOLON ||
394
+ token.type === TOKEN_TYPES.EXCLAMATION_MARK ||
315
395
  token.type === TOKEN_TYPES.CLOSE_PAREN) break;
316
396
 
317
397
  if (token.type === TOKEN_TYPES.ESCAPE) {
@@ -403,8 +483,9 @@ function parseSemiColon(tokens, i, afterChar = "") {
403
483
  * @param {Object} placeholders - Dynamic public API data.
404
484
  * @returns {[Object, number]} The parsed Block node and new index.
405
485
  */
406
- function parseBlock(tokens, i, filename = null, placeholders = {}) {
486
+ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {}, depth = 0) {
407
487
  const blockNode = makeBlockNode();
488
+ blockNode.depth = depth;
408
489
  const openBracketToken = current_token(tokens, i);
409
490
  // ========================================================================== //
410
491
  // consume '[' //
@@ -429,11 +510,18 @@ function parseBlock(tokens, i, filename = null, placeholders = {}) {
429
510
  blockNode.type = IMPORT;
430
511
  } else if (blockNode.id === "$use-module") {
431
512
  blockNode.type = USE_MODULE;
513
+ } else if (idToken.type === TOKEN_TYPES.SLOT_KEYWORD) {
514
+ blockNode.type = SLOT;
515
+ // Prevent nested slots
516
+ if (end_stack.some(e => e.id === "slot")) {
517
+ parserError(errorMessage(tokens, i, "slot", "", "Nested slots are not allowed. A [slot] cannot be placed inside another [slot]."));
518
+ }
519
+ } else if (idToken.type === TOKEN_TYPES.FOR_EACH || blockNode.id === "for-each") {
520
+ blockNode.type = FOR_EACH;
432
521
  }
433
522
  validateName(blockNode.id, true);
434
- blockNode.depth = idToken.depth;
435
523
  blockNode.range.start = openBracketToken.range.start;
436
- end_stack.push(id);
524
+ end_stack.push({ id, line: openBracketToken.range.start.line + 1, col: openBracketToken.range.start.character });
437
525
  // ========================================================================== //
438
526
  // consume Block Identifier //
439
527
  // ========================================================================== //
@@ -460,7 +548,11 @@ function parseBlock(tokens, i, filename = null, placeholders = {}) {
460
548
  current_token(tokens, i).type !== TOKEN_TYPES.KEY &&
461
549
  current_token(tokens, i).type !== TOKEN_TYPES.QUOTE &&
462
550
  current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_JS &&
463
- current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_P)
551
+ current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_V &&
552
+ current_token(tokens, i).type !== TOKEN_TYPES.PREFIX_P &&
553
+ current_token(tokens, i).type !== TOKEN_TYPES.LOGIC &&
554
+ current_token(tokens, i).type !== TOKEN_TYPES.STATIC_KEYWORD &&
555
+ current_token(tokens, i).type !== TOKEN_TYPES.RUNTIME_KEYWORD)
464
556
  ) {
465
557
  parserError(errorMessage(tokens, i, block_value, "="));
466
558
  }
@@ -499,7 +591,7 @@ function parseBlock(tokens, i, filename = null, placeholders = {}) {
499
591
  }
500
592
 
501
593
  // Parse Value (handles both quoted, unquoted, and prefixes)
502
- let [value, valueIndex, isQuoted] = parseValue(tokens, i, placeholders);
594
+ let [value, valueIndex, isQuoted] = parseValue(tokens, i, placeholders, variables);
503
595
  v = value;
504
596
  vIsQuoted = isQuoted;
505
597
  i = valueIndex;
@@ -513,10 +605,19 @@ function parseBlock(tokens, i, filename = null, placeholders = {}) {
513
605
  v = "";
514
606
 
515
607
  i = skipJunk(tokens, i);
516
- if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COMMA) {
517
- i = parseComma(tokens, i, block_value);
608
+ const separatorToken = current_token(tokens, i);
609
+ if (separatorToken && (separatorToken.type === TOKEN_TYPES.COMMA || separatorToken.type === TOKEN_TYPES.COLON)) {
610
+ i++; // consume , or :
611
+ i = skipJunk(tokens, i);
612
+ updateData(tokens, i);
613
+
614
+ // Ensure next token is NOT the closing bracket (trailing separator)
615
+ const afterSeparator = current_token(tokens, i);
616
+ if (!afterSeparator || afterSeparator.type === TOKEN_TYPES.CLOSE_BRACKET) {
617
+ parserError(errorMessage(tokens, i, "value", "", "Unexpected trailing separator"));
618
+ }
518
619
  } else {
519
- // No comma, must be end of arguments or ]
620
+ // No separator, must be end of arguments or ]
520
621
  break;
521
622
  }
522
623
  }
@@ -531,6 +632,13 @@ function parseBlock(tokens, i, filename = null, placeholders = {}) {
531
632
  }
532
633
 
533
634
  i = skipJunk(tokens, i);
635
+
636
+ if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.EXCLAMATION_MARK) {
637
+ blockNode.isSelfClosing = true;
638
+ i++;
639
+ i = skipJunk(tokens, i);
640
+ }
641
+
534
642
  // ========================================================================== //
535
643
  // Close Bracket //
536
644
  // ========================================================================== //
@@ -542,6 +650,13 @@ function parseBlock(tokens, i, filename = null, placeholders = {}) {
542
650
  // ========================================================================== //
543
651
  i++;
544
652
  updateData(tokens, i);
653
+
654
+ if (blockNode.isSelfClosing) {
655
+ end_stack.pop();
656
+ blockNode.range.end = current_token(tokens, i - 1).range.end;
657
+ return [blockNode, i];
658
+ }
659
+
545
660
  tokens_stack.length = 0;
546
661
  while (i < tokens.length) {
547
662
  const nextIdx = skipJunk(tokens, i + 1);
@@ -553,11 +668,9 @@ function parseBlock(tokens, i, filename = null, placeholders = {}) {
553
668
  nextToken.type !== TOKEN_TYPES.END_KEYWORD &&
554
669
  nextToken.value.trim() !== end_keyword
555
670
  ) {
556
- const [childNode, nextIndex] = parseBlock(tokens, i, filename, placeholders);
671
+ const [childNode, nextIndex] = parseBlock(tokens, i, filename, placeholders, variables, depth + 1);
672
+
557
673
  blockNode.body.push(childNode);
558
- // ========================================================================== //
559
- // consume child node //
560
- // ========================================================================== //
561
674
  i = nextIndex;
562
675
  } else if (
563
676
  current_token(tokens, i) &&
@@ -602,8 +715,15 @@ function parseBlock(tokens, i, filename = null, placeholders = {}) {
602
715
  updateData(tokens, i);
603
716
  blockNode.range.end = closeBracketToken.range.end;
604
717
  break;
718
+ } else if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.WHITESPACE) {
719
+ blockNode.body.push({
720
+ type: TEXT,
721
+ text: current_token(tokens, i).value,
722
+ range: current_token(tokens, i).range
723
+ });
724
+ i++;
605
725
  } else {
606
- const [childNode, nextIndex] = parseNode(tokens, i, filename, placeholders);
726
+ const [childNode, nextIndex] = parseNode(tokens, i, filename, placeholders, variables, depth + 1);
607
727
  if (childNode) {
608
728
  blockNode.body.push(childNode);
609
729
  i = nextIndex;
@@ -623,8 +743,9 @@ function parseBlock(tokens, i, filename = null, placeholders = {}) {
623
743
  * @param {Object} placeholders - Dynamic public API data.
624
744
  * @returns {[Object, number]} The parsed Inline node and new index.
625
745
  */
626
- function parseInline(tokens, i, placeholders = {}) {
746
+ function parseInline(tokens, i, placeholders = {}, depth = 0) {
627
747
  const inlineNode = makeInlineNode();
748
+ inlineNode.depth = depth;
628
749
  const openParenToken = current_token(tokens, i);
629
750
  inlineNode.range.start = openParenToken.range.start;
630
751
 
@@ -639,7 +760,7 @@ function parseInline(tokens, i, placeholders = {}) {
639
760
 
640
761
  if (token.type === TOKEN_TYPES.ESCAPE) {
641
762
  inlineNode.value += token.value.slice(1);
642
- } else {
763
+ } else if (token.type !== TOKEN_TYPES.COMMENT) {
643
764
  inlineNode.value += token.value;
644
765
  }
645
766
  i++;
@@ -666,7 +787,15 @@ function parseInline(tokens, i, placeholders = {}) {
666
787
  i++; // consume '('
667
788
  i = skipJunk(tokens, i);
668
789
  const idToken = current_token(tokens, i);
669
- if (!idToken || (idToken.type !== TOKEN_TYPES.IDENTIFIER && idToken.type !== TOKEN_TYPES.KEY)) {
790
+ const allowedInlineIdTypes = new Set([
791
+ TOKEN_TYPES.IDENTIFIER,
792
+ TOKEN_TYPES.KEY,
793
+ TOKEN_TYPES.IMPORT,
794
+ TOKEN_TYPES.USE_MODULE,
795
+ TOKEN_TYPES.SLOT_KEYWORD,
796
+ TOKEN_TYPES.FOR_EACH
797
+ ]);
798
+ if (!idToken || !allowedInlineIdTypes.has(idToken.type)) {
670
799
  parserError(errorMessage(tokens, i, inline_id, "("));
671
800
  }
672
801
  inlineNode.id = idToken.value.trim();
@@ -675,14 +804,20 @@ function parseInline(tokens, i, placeholders = {}) {
675
804
  i++; // consume ID
676
805
  i = skipJunk(tokens, i);
677
806
 
678
- if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COLON) {
679
- i++; // consume ':'
807
+ const hasArgsTrigger = current_token(tokens, i) && (
808
+ current_token(tokens, i).type === TOKEN_TYPES.COLON ||
809
+ current_token(tokens, i).type === TOKEN_TYPES.EQUAL
810
+ );
811
+
812
+ if (hasArgsTrigger) {
813
+ const separator = current_token(tokens, i).value;
814
+ i++; // consume ':' or '='
680
815
  i = skipJunk(tokens, i);
681
816
 
682
- // Ensure there is a value after the colon
817
+ // Ensure there is a value after the separator
683
818
  const nextToken = current_token(tokens, i);
684
819
  if (!nextToken || nextToken.type === TOKEN_TYPES.CLOSE_PAREN || nextToken.type === TOKEN_TYPES.COMMA) {
685
- parserError(errorMessage(tokens, i, inline_value, ":", "Missing value after colon"));
820
+ parserError(errorMessage(tokens, i, inline_value, separator, `Missing value after ${separator === "=" ? "equals" : "colon"}`));
686
821
  }
687
822
 
688
823
  let k = "";
@@ -710,7 +845,7 @@ function parseInline(tokens, i, placeholders = {}) {
710
845
  validateName(k);
711
846
  }
712
847
 
713
- let [value, valueIndex, isQuoted] = parseValue(tokens, i, placeholders);
848
+ let [value, valueIndex, isQuoted] = parseValue(tokens, i, placeholders, {}, false);
714
849
  v = value;
715
850
  i = valueIndex;
716
851
 
@@ -746,15 +881,16 @@ function parseInline(tokens, i, placeholders = {}) {
746
881
  *
747
882
  * @param {Object[]} tokens - Token stream.
748
883
  * @param {number} i - Initial index.
749
- * @param {Object} placeholders - Dynamic public API data.
750
- * @param {Object} options - Formatting options.
884
+ * @param {Object} [placeholders={}] - Global data for p{keyword} resolution.
885
+ * @param {Object} [variables={}] - Local data for v{keyword} resolution.
886
+ * @param {Object} [options={}] - Formatting options.
751
887
  * @returns {[Object, number]} The Text node and new index.
752
888
  */
753
- function parseText(tokens, i, placeholders = {}, options = {}) {
889
+ function parseText(tokens, i, placeholders = {}, variables = {}, depth = 0, options = {}) {
754
890
  const textNode = makeTextNode();
891
+ textNode.depth = depth;
755
892
  const startToken = current_token(tokens, i);
756
893
  textNode.range.start = startToken.range.start;
757
- textNode.depth = startToken.depth;
758
894
  const { selectiveUnescape = false } = options;
759
895
 
760
896
  while (i < tokens.length) {
@@ -764,6 +900,14 @@ function parseText(tokens, i, placeholders = {}, options = {}) {
764
900
  if (token.type === TOKEN_TYPES.TEXT || token.type === TOKEN_TYPES.WHITESPACE || token.type === TOKEN_TYPES.VALUE) {
765
901
  textNode.text += token.value;
766
902
  i++;
903
+ } else if (token.type === TOKEN_TYPES.STATIC_KEYWORD || token.type === TOKEN_TYPES.RUNTIME_KEYWORD) {
904
+ const nextIdx = skipJunk(tokens, i + 1);
905
+ if (tokens[nextIdx] && tokens[nextIdx].type === TOKEN_TYPES.LOGIC) {
906
+ // Stop consuming text; this is the start of a logic block
907
+ break;
908
+ }
909
+ textNode.text += token.value;
910
+ i++;
767
911
  } else if (token.type === TOKEN_TYPES.ESCAPE) {
768
912
  if (selectiveUnescape) {
769
913
  const char = token.value.slice(1);
@@ -779,8 +923,38 @@ function parseText(tokens, i, placeholders = {}, options = {}) {
779
923
  } else if (token.type === TOKEN_TYPES.PREFIX_P) {
780
924
  const val = token.value;
781
925
  if (val.startsWith("p{") && val.endsWith("}")) {
926
+ const match = [val.slice(2, -1).trim(), val, 'p'];
927
+ const key = match[0];
928
+ const layer = match[2]; // 'p' or 'v'
929
+
930
+ if (placeholders[key] !== undefined) {
931
+ textNode.text += String(placeholders[key]);
932
+ } else {
933
+ // Use the unique 'Unresolved Envelope' format via helper
934
+ textNode.text += getPrefixValue(layer, key);
935
+ }
936
+ } else {
937
+ textNode.text += val;
938
+ }
939
+ i++;
940
+ } else if (token.type === TOKEN_TYPES.PREFIX_V) {
941
+ const val = token.value;
942
+ if (val.startsWith("v{") && val.endsWith("}")) {
782
943
  const key = val.slice(2, -1).trim();
783
- textNode.text += placeholders[key] !== undefined ? String(placeholders[key]) : val;
944
+ if (variables[key] !== undefined) {
945
+ textNode.text += String(variables[key]);
946
+ if (!variables.__consumed__) {
947
+ Object.defineProperty(variables, "__consumed__", {
948
+ value: new Set(),
949
+ enumerable: false,
950
+ configurable: true
951
+ });
952
+ }
953
+ variables.__consumed__.add(key);
954
+ } else {
955
+ // Use the unique 'Unresolved Envelope' format via helper
956
+ textNode.text += getPrefixValue('v', key);
957
+ }
784
958
  } else {
785
959
  textNode.text += val;
786
960
  }
@@ -804,8 +978,9 @@ function parseText(tokens, i, placeholders = {}, options = {}) {
804
978
  * @param {Object} placeholders - Dynamic public API data.
805
979
  * @returns {[Object, number]} The At-Block node and new index.
806
980
  */
807
- function parseAtBlock(tokens, i, filename = null, placeholders = {}) {
981
+ function parseAtBlock(tokens, i, filename = null, placeholders = {}, depth = 0) {
808
982
  const atBlockNode = makeAtBlockNode();
983
+ atBlockNode.depth = depth;
809
984
  const openAtToken = current_token(tokens, i);
810
985
  atBlockNode.range.start = openAtToken.range.start;
811
986
 
@@ -826,7 +1001,6 @@ function parseAtBlock(tokens, i, filename = null, placeholders = {}) {
826
1001
 
827
1002
  atBlockNode.id = id.trim();
828
1003
  validateName(atBlockNode.id);
829
- atBlockNode.depth = idToken.depth;
830
1004
 
831
1005
  // consume ID
832
1006
  i++;
@@ -882,7 +1056,7 @@ function parseAtBlock(tokens, i, filename = null, placeholders = {}) {
882
1056
  }
883
1057
  }
884
1058
 
885
- let [value, valueIndex, isQuoted] = parseValue(tokens, i, placeholders);
1059
+ let [value, valueIndex, isQuoted] = parseValue(tokens, i, placeholders, {}, false);
886
1060
  v = value;
887
1061
  i = valueIndex;
888
1062
 
@@ -923,7 +1097,14 @@ function parseAtBlock(tokens, i, filename = null, placeholders = {}) {
923
1097
  i = skipJunk(tokens, i);
924
1098
  const endToken = current_token(tokens, i);
925
1099
  if (!endToken || (endToken.type !== TOKEN_TYPES.END_KEYWORD && endToken.value.trim() !== end_keyword)) {
926
- parserError(errorMessage(tokens, i, "end", "@_"));
1100
+ let extraInfo = "";
1101
+ if (endToken && endToken.value) {
1102
+ const dist = levenshtein(endToken.value.trim().toLowerCase(), "end");
1103
+ if (dist > 0 && dist <= 2) {
1104
+ extraInfo = ` (Did you mean '@_end_@'?)`;
1105
+ }
1106
+ }
1107
+ parserError(errorMessage(tokens, i, "end", "AtBlock Body", extraInfo));
927
1108
  }
928
1109
  i++; // consume 'end'
929
1110
  i = skipJunk(tokens, i);
@@ -939,12 +1120,18 @@ function parseAtBlock(tokens, i, filename = null, placeholders = {}) {
939
1120
  // ========================================================================== //
940
1121
  // Parse Comments //
941
1122
  // ========================================================================== //
942
- function parseCommentNode(tokens, i) {
1123
+ function parseCommentNode(tokens, i, depth = 0) {
943
1124
  const commentNode = makeCommentNode();
944
1125
  const token = current_token(tokens, i);
945
- if (token && token.type === TOKEN_TYPES.COMMENT) {
946
- commentNode.text = token.value;
947
- commentNode.depth = token.depth;
1126
+ if (token && (token.type === TOKEN_TYPES.COMMENT || token.type === TOKEN_TYPES.COMMENT_BLOCK)) {
1127
+ commentNode.type = token.type === TOKEN_TYPES.COMMENT ? COMMENT : COMMENT_BLOCK;
1128
+ // Clean the text here instead of the transpiler
1129
+ const raw = token.value;
1130
+ commentNode.text = token.type === TOKEN_TYPES.COMMENT
1131
+ ? raw.replace(/^#/, "").trim()
1132
+ : raw.replace(/^###[\r\n]*/, "").replace(/[\r\n]*###$/, "").trim();
1133
+
1134
+ commentNode.depth = depth;
948
1135
  commentNode.range = token.range;
949
1136
  }
950
1137
  // ========================================================================== //
@@ -968,21 +1155,21 @@ function parseCommentNode(tokens, i) {
968
1155
  * @param {Object} placeholders - Dynamic public API data.
969
1156
  * @returns {[Object, number]} The parsed node and new index.
970
1157
  */
971
- function parseNode(tokens, i, filename = null, placeholders = {}) {
1158
+ function parseNode(tokens, i, filename = null, placeholders = {}, variables = {}, depth = 0) {
972
1159
  if (!current_token(tokens, i) || (current_token(tokens, i) && !current_token(tokens, i).value)) {
973
1160
  return [null, i];
974
1161
  }
975
1162
  // ========================================================================== //
976
1163
  // Comment //
977
1164
  // ========================================================================== //
978
- if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COMMENT) {
979
- return parseCommentNode(tokens, i);
1165
+ if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.COMMENT || current_token(tokens, i).type === TOKEN_TYPES.COMMENT_BLOCK)) {
1166
+ return parseCommentNode(tokens, i, depth);
980
1167
  }
981
1168
  // ========================================================================== //
982
1169
  // Block or Reserved Keyword //
983
1170
  // ========================================================================== //
984
1171
  else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.OPEN_BRACKET)) {
985
- return parseBlock(tokens, i, filename, placeholders);
1172
+ return parseBlock(tokens, i, filename, placeholders, variables, depth);
986
1173
  }
987
1174
  // ========================================================================== //
988
1175
  // Inline Statement or Text //
@@ -1013,18 +1200,46 @@ function parseNode(tokens, i, filename = null, placeholders = {}) {
1013
1200
  }
1014
1201
 
1015
1202
  if (foundArrow) {
1016
- return parseInline(tokens, i, placeholders);
1203
+ return parseInline(tokens, i, placeholders, depth);
1017
1204
  }
1018
1205
 
1019
1206
  // Treat as text if not an inline
1020
1207
  const textNode = makeTextNode();
1021
1208
  textNode.text = current_token(tokens, i).value;
1209
+ textNode.depth = depth;
1022
1210
  textNode.range = current_token(tokens, i).range;
1023
1211
  return [textNode, i + 1];
1024
1212
  }
1025
1213
  // ========================================================================== //
1026
- // Text or Placeholder //
1214
+ // Logic Block //
1027
1215
  // ========================================================================== //
1216
+ else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD || current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD || current_token(tokens, i).type === TOKEN_TYPES.LOGIC)) {
1217
+ let isStatic = current_token(tokens, i).type === TOKEN_TYPES.STATIC_KEYWORD;
1218
+ let isRuntimeKeyword = current_token(tokens, i).type === TOKEN_TYPES.RUNTIME_KEYWORD;
1219
+ let startRange = current_token(tokens, i).range;
1220
+ let nextI = i;
1221
+
1222
+ if (isStatic || isRuntimeKeyword) {
1223
+ if (isStatic) global_static_logic_count++;
1224
+ nextI = skipJunk(tokens, i + 1);
1225
+ if (!current_token(tokens, nextI) || current_token(tokens, nextI).type !== TOKEN_TYPES.LOGIC) {
1226
+ // Treat as normal text if keyword is not followed by a logic block
1227
+ return parseText(tokens, i, placeholders, variables, depth);
1228
+ }
1229
+ i = nextI;
1230
+ }
1231
+
1232
+ const logicToken = current_token(tokens, i);
1233
+ const node = makeLogicNode(isStatic ? STATIC_LOGIC : RUNTIME_LOGIC);
1234
+ node.code = logicToken.value;
1235
+ node.depth = depth;
1236
+ node.range = {
1237
+ start: (isStatic || isRuntimeKeyword) ? startRange.start : logicToken.range.start,
1238
+ end: logicToken.range.end
1239
+ };
1240
+
1241
+ return [node, i + 1];
1242
+ }
1028
1243
  // ========================================================================== //
1029
1244
  // Text or Placeholder //
1030
1245
  // ========================================================================== //
@@ -1034,19 +1249,21 @@ function parseNode(tokens, i, filename = null, placeholders = {}) {
1034
1249
  current_token(tokens, i).type === TOKEN_TYPES.WHITESPACE ||
1035
1250
  current_token(tokens, i).type === TOKEN_TYPES.ESCAPE ||
1036
1251
  current_token(tokens, i).type === TOKEN_TYPES.VALUE ||
1252
+ current_token(tokens, i).type === TOKEN_TYPES.PREFIX_V ||
1037
1253
  current_token(tokens, i).type === TOKEN_TYPES.PREFIX_P)
1038
1254
  ) {
1039
- return parseText(tokens, i, placeholders);
1255
+ return parseText(tokens, i, placeholders, variables, depth);
1040
1256
  }
1041
1257
  // ========================================================================== //
1042
1258
  // Atblock //
1043
1259
  // ========================================================================== //
1044
1260
  else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.OPEN_AT)) {
1045
- return parseAtBlock(tokens, i, filename, placeholders);
1261
+ return parseAtBlock(tokens, i, filename, placeholders, depth);
1046
1262
  } else {
1047
1263
  // FALLBACK: Treat any other token as TEXT to avoid infinite loops and allow literal content
1048
1264
  const textNode = makeTextNode();
1049
1265
  textNode.text = current_token(tokens, i).value;
1266
+ textNode.depth = depth;
1050
1267
  textNode.range = current_token(tokens, i).range;
1051
1268
  return [textNode, i + 1];
1052
1269
  }
@@ -1065,20 +1282,22 @@ function parseNode(tokens, i, filename = null, placeholders = {}) {
1065
1282
  * @param {Object[]} tokens - The stream of tokens from the Lexer.
1066
1283
  * @param {string|null} [filename=null] - Source filename for error context.
1067
1284
  * @param {Object} [placeholders={}] - Global data for p{keyword} resolution.
1285
+ * @param {Object} [variables={}] - Local data for v{keyword} resolution.
1068
1286
  * @returns {Array<Object>} The final Abstract Syntax Tree.
1069
1287
  */
1070
- function parser(tokens, filename = null, placeholders = {}) {
1288
+ function parser(tokens, filename = null, placeholders = {}, variables = {}) {
1071
1289
  end_stack = [];
1072
- tokens_stack = [];
1073
- range = {
1290
+ global_static_logic_count = 0;
1291
+ let tokens_stack = [];
1292
+ let range = {
1074
1293
  start: { line: 0, character: 0 },
1075
1294
  end: { line: 0, character: 0 }
1076
1295
  };
1077
- value = "";
1296
+ let value = "";
1078
1297
  let ast = [];
1079
1298
  let i = 0;
1080
1299
  while (i < tokens.length) {
1081
- let [node, nextIndex] = parseNode(tokens, i, filename, placeholders);
1300
+ let [node, nextIndex] = parseNode(tokens, i, filename, placeholders, variables, 1);
1082
1301
  if (node) {
1083
1302
  ast.push(node);
1084
1303
  i = nextIndex;
@@ -1107,7 +1326,8 @@ function parser(tokens, filename = null, placeholders = {}) {
1107
1326
  if (extraInfo) break;
1108
1327
  }
1109
1328
 
1110
- parserError(errorMessage(tokens, tokens.length - 1, "[end]", "", extraInfo ? `Missing '[end]'${extraInfo}` : "Missing '[end]'", filename));
1329
+ const lastOpen = end_stack[end_stack.length - 1];
1330
+ parserError(errorMessage(tokens, tokens.length - 1, "[end]", "", extraInfo ? `Missing '[end]' for block '${lastOpen.id}' (opened at line ${lastOpen.line}, col ${lastOpen.col})${extraInfo}` : `Missing '[end]' for block '${lastOpen.id}' (opened at line ${lastOpen.line}, col ${lastOpen.col})`, filename));
1111
1331
  }
1112
1332
  return ast;
1113
1333
  }