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/lexer.js CHANGED
@@ -27,9 +27,9 @@ function lexer(src, filename = "anonymous") {
27
27
  let isInAtBlockBody = false;
28
28
  let isInQuote = false;
29
29
  let isInHeader = false; // Tracks if we are in a structural header context
30
+ let isInAtBlockHeader = false; // Specific for At-Block headers (@_ ... _@)
30
31
  let isInInlineHead = false; // Specific for (key:val) after ->
31
32
  let parenDepth = 0; // To track balanced parentheses in inlines
32
- let delimiterStack = []; // To track block nesting for body mode
33
33
 
34
34
  /**
35
35
  * Adds a token to the stream and updates the scanner's position tracking.
@@ -54,8 +54,7 @@ function lexer(src, filename = "anonymous") {
54
54
  type,
55
55
  value,
56
56
  source: filename,
57
- range: { start, end },
58
- depth: delimiterStack.length
57
+ range: { start, end }
59
58
  });
60
59
 
61
60
  prev_type = type;
@@ -140,17 +139,18 @@ function lexer(src, filename = "anonymous") {
140
139
  i += 2;
141
140
  continue;
142
141
  }
143
-
142
+
144
143
  // Support Prefix Layers inside quotes!
145
- if ((src[i] === "j" && src[i+1] === "s" && src[i+2] === "{") || (src[i] === "p" && src[i+1] === "{")) {
144
+ if ((src[i] === "j" && src[i + 1] === "s" && src[i + 2] === "{") || (src[i] === "p" && src[i + 1] === "{") || (src[i] === "v" && src[i + 1] === "{")) {
146
145
  const isJS = (src[i] === "j");
146
+ const isV = (src[i] === "v");
147
147
  if (quoteValue.length > 0) {
148
148
  addToken(TOKEN_TYPES.VALUE, quoteValue);
149
149
  quoteValue = "";
150
150
  }
151
-
151
+
152
152
  let braceDepth = 1;
153
- let prefixValue = isJS ? "js{" : "p{";
153
+ let prefixValue = isJS ? "js{" : (isV ? "v{" : "p{");
154
154
  i += isJS ? 3 : 2;
155
155
 
156
156
  let internalString = null;
@@ -172,7 +172,8 @@ function lexer(src, filename = "anonymous") {
172
172
  prefixValue += c;
173
173
  i++;
174
174
  }
175
- addToken(isJS ? TOKEN_TYPES.PREFIX_JS : TOKEN_TYPES.PREFIX_P, prefixValue);
175
+ let tokenType = isJS ? TOKEN_TYPES.PREFIX_JS : (isV ? TOKEN_TYPES.PREFIX_V : TOKEN_TYPES.PREFIX_P);
176
+ addToken(tokenType, prefixValue);
176
177
  continue;
177
178
  }
178
179
 
@@ -197,7 +198,7 @@ function lexer(src, filename = "anonymous") {
197
198
 
198
199
  // --- PHASE 3: STRUCTURAL PARSING ---
199
200
  // Handles markers, whitespace, and structural symbols.
200
-
201
+
201
202
  // WHITESPACE
202
203
  if (char === "\n") {
203
204
  addToken(TOKEN_TYPES.WHITESPACE, char);
@@ -218,11 +219,31 @@ function lexer(src, filename = "anonymous") {
218
219
  // COMMENTS
219
220
  if (char === "#") {
220
221
  let comm = "";
221
- while (i < src.length && src[i] !== "\n") {
222
- comm += src[i];
223
- i++;
222
+ // Check for Multiline Comment ### (must have no spaces)
223
+ if (src[i + 1] === "#" && src[i + 2] === "#") {
224
+ const startPos = i;
225
+ comm = "###";
226
+ i += 3;
227
+ let closed = false;
228
+ while (i < src.length) {
229
+ if (src[i] === "#" && src[i + 1] === "#" && src[i + 2] === "#") {
230
+ comm += "###";
231
+ i += 3;
232
+ closed = true;
233
+ break;
234
+ }
235
+ comm += src[i];
236
+ i++;
237
+ }
238
+ addToken(TOKEN_TYPES.COMMENT_BLOCK, comm);
239
+ } else {
240
+ // Single line comment
241
+ while (i < src.length && src[i] !== "\n") {
242
+ comm += src[i];
243
+ i++;
244
+ }
245
+ addToken(TOKEN_TYPES.COMMENT, comm);
224
246
  }
225
- addToken(TOKEN_TYPES.COMMENT, comm);
226
247
  continue;
227
248
  }
228
249
 
@@ -234,23 +255,24 @@ function lexer(src, filename = "anonymous") {
234
255
  continue;
235
256
  }
236
257
 
237
- // PREFIX LAYERS (js{...} or p{...})
238
- if ((char === "j" && next === "s" && src[i+2] === "{") || (char === "p" && next === "{")) {
258
+ // PREFIX LAYERS (js{...} or p{...} or v{...})
259
+ if ((char === "j" && next === "s" && src[i + 2] === "{") || (char === "p" && next === "{") || (char === "v" && next === "{")) {
239
260
  const isJS = (char === "j");
240
261
  const isP = (char === "p");
241
-
262
+ const isV = (char === "v");
263
+
242
264
  // Context Check
243
- const top = (delimiterStack.length > 0) ? delimiterStack[delimiterStack.length - 1] : null;
244
- const isInBlockHeader = isInHeader && top === "[";
245
- const isInNormalText = !isInHeader && !isInInlineHead && !isInAtBlockBody && parenDepth === 0;
246
-
265
+ const isBlockHeader = isInHeader && !isInAtBlockHeader;
266
+ const isNormalText = !isInHeader && !isInInlineHead && !isInAtBlockBody && parenDepth === 0;
267
+
247
268
  let allowed = false;
248
- if (isJS && isInBlockHeader) allowed = true;
249
- if (isP && (isInBlockHeader || isInNormalText)) allowed = true;
269
+ if (isJS && isBlockHeader) allowed = true;
270
+ if (isP && (isBlockHeader || isNormalText)) allowed = true;
271
+ if (isV && (isBlockHeader || isNormalText)) allowed = true;
250
272
 
251
273
  if (allowed) {
252
274
  let braceDepth = 1;
253
- let prefixValue = isJS ? "js{" : "p{";
275
+ let prefixValue = isJS ? "js{" : (isV ? "v{" : "p{");
254
276
  i += isJS ? 3 : 2;
255
277
 
256
278
  let inString = null; // Track if we are inside " " or ' '
@@ -273,7 +295,8 @@ function lexer(src, filename = "anonymous") {
273
295
  prefixValue += c;
274
296
  i++;
275
297
  }
276
- addToken(isJS ? TOKEN_TYPES.PREFIX_JS : TOKEN_TYPES.PREFIX_P, prefixValue);
298
+ let tokenType = isJS ? TOKEN_TYPES.PREFIX_JS : (isV ? TOKEN_TYPES.PREFIX_V : TOKEN_TYPES.PREFIX_P);
299
+ addToken(tokenType, prefixValue);
277
300
  continue;
278
301
  }
279
302
  // If not allowed, it will fall through to normal word scanning
@@ -283,8 +306,8 @@ function lexer(src, filename = "anonymous") {
283
306
  if (char === "@" && next === "_") {
284
307
  addToken(TOKEN_TYPES.OPEN_AT, "@_");
285
308
  i += 2;
286
- if (!isInAtBlockBody) delimiterStack.push("@");
287
309
  isInHeader = true; // At-Blocks start with a header part
310
+ isInAtBlockHeader = true;
288
311
  continue;
289
312
  }
290
313
  if (char === "-" && next === ">") {
@@ -299,13 +322,128 @@ function lexer(src, filename = "anonymous") {
299
322
  continue;
300
323
  }
301
324
 
325
+ // STATIC KEYWORD
326
+ if (char === "s" && src.slice(i, i + 6) === "static") {
327
+ const afterStatic = src.slice(i + 6);
328
+ const hasSpace = afterStatic.startsWith(" ");
329
+ const hasLogic = hasSpace ? afterStatic.slice(1).startsWith("${") : afterStatic.startsWith("${");
330
+
331
+ const isMainIdentifier = (
332
+ last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET ||
333
+ last_non_junk_type === TOKEN_TYPES.OPEN_AT ||
334
+ (last_non_junk_type === TOKEN_TYPES.OPEN_PAREN && isInInlineHead)
335
+ );
336
+
337
+ if ((hasLogic || isInHeader) && !isMainIdentifier) {
338
+ addToken(TOKEN_TYPES.STATIC_KEYWORD, hasSpace ? "static " : "static");
339
+ i += hasSpace ? 7 : 6;
340
+ continue;
341
+ }
342
+ }
343
+
344
+ // RUNTIME KEYWORD
345
+ if (char === "r" && src.slice(i, i + 7) === "runtime") {
346
+ const afterRuntime = src.slice(i + 7);
347
+ const hasSpace = afterRuntime.startsWith(" ");
348
+ const hasLogic = hasSpace ? afterRuntime.slice(1).startsWith("${") : afterRuntime.startsWith("${");
349
+
350
+ const isMainIdentifier = (
351
+ last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET ||
352
+ last_non_junk_type === TOKEN_TYPES.OPEN_AT ||
353
+ (last_non_junk_type === TOKEN_TYPES.OPEN_PAREN && isInInlineHead)
354
+ );
355
+
356
+ if ((hasLogic || isInHeader) && !isMainIdentifier) {
357
+ addToken(TOKEN_TYPES.RUNTIME_KEYWORD, hasSpace ? "runtime " : "runtime");
358
+ i += hasSpace ? 8 : 7;
359
+ continue;
360
+ }
361
+ }
362
+
363
+ // LOGIC BLOCKS (${ ... }$)
364
+ if (char === "$" && next === "{" && (last_non_junk_type === TOKEN_TYPES.STATIC_KEYWORD || last_non_junk_type === TOKEN_TYPES.RUNTIME_KEYWORD)) {
365
+ const startLine = line;
366
+ const startCharacter = character;
367
+ i += 2;
368
+ let logicCode = "";
369
+ let braceDepth = 1;
370
+ let internalString = null;
371
+ let foundClosing = false;
372
+
373
+ while (i < src.length) {
374
+ const c = src[i];
375
+ const n = src[i + 1];
376
+
377
+ // Stop condition: }$ (only if not inside a JS string and at top-level brace depth)
378
+ if (c === "}" && n === "$" && !internalString && braceDepth === 1) {
379
+ i += 2;
380
+ braceDepth = 0;
381
+ foundClosing = true;
382
+ break;
383
+ }
384
+
385
+ if (internalString) {
386
+ if (c === "\\" && (n === internalString || n === "\\")) {
387
+ logicCode += c + n;
388
+ i += 2;
389
+ continue;
390
+ }
391
+ if (c === internalString) internalString = null;
392
+ } else {
393
+ if (c === "/" && n === "/") {
394
+ logicCode += c + n;
395
+ i += 2;
396
+ while (i < src.length && src[i] !== "\n" && src[i] !== "\r") {
397
+ logicCode += src[i];
398
+ i++;
399
+ }
400
+ continue;
401
+ }
402
+ if (c === "/" && n === "*") {
403
+ logicCode += c + n;
404
+ i += 2;
405
+ while (i < src.length) {
406
+ if (src[i] === "*" && src[i + 1] === "/") {
407
+ logicCode += "*/";
408
+ i += 2;
409
+ break;
410
+ }
411
+ logicCode += src[i];
412
+ i++;
413
+ }
414
+ continue;
415
+ }
416
+
417
+ if (c === "\"" || c === "'" || c === "`") internalString = c;
418
+ else if (c === "{") braceDepth++;
419
+ else if (c === "}") braceDepth--;
420
+ }
421
+
422
+ logicCode += c;
423
+ i++;
424
+ }
425
+
426
+ if (!foundClosing) {
427
+ lexerError("Unclosed logic block. Expected '}$' to close the block starting with '${'.", {
428
+ src,
429
+ filename,
430
+ range: {
431
+ start: { line: startLine, character: startCharacter },
432
+ end: { line: startLine, character: startCharacter + 2 }
433
+ }
434
+ });
435
+ }
436
+
437
+ addToken(TOKEN_TYPES.LOGIC, logicCode);
438
+ continue;
439
+ }
440
+
302
441
  // SINGLE-CHAR MARKERS
303
442
  if (char === "[") {
304
443
  if (isInAtBlockBody || (parenDepth > 0 && !isInInlineHead)) {
305
444
  addToken(TOKEN_TYPES.TEXT, "[");
306
445
  } else {
307
446
  addToken(TOKEN_TYPES.OPEN_BRACKET, "[");
308
- delimiterStack.push("[");
309
447
  isInHeader = true;
310
448
  }
311
449
  i++;
@@ -317,13 +455,11 @@ function lexer(src, filename = "anonymous") {
317
455
  } else {
318
456
  const lastRealType = last_non_junk_type;
319
457
  addToken(TOKEN_TYPES.CLOSE_AT, "_@");
320
- const top = delimiterStack[delimiterStack.length - 1];
321
- if (top === "@") {
322
- if (lastRealType === TOKEN_TYPES.END_KEYWORD) {
323
- delimiterStack.pop();
324
- isInAtBlockBody = false;
325
- isInHeader = false;
326
- }
458
+ // Removed delimiter stack check
459
+ if (lastRealType === TOKEN_TYPES.END_KEYWORD) {
460
+ isInAtBlockBody = false;
461
+ isInHeader = false;
462
+ isInAtBlockHeader = false;
327
463
  }
328
464
  }
329
465
  i += 2;
@@ -359,7 +495,10 @@ function lexer(src, filename = "anonymous") {
359
495
  parenDepth--;
360
496
  if (parenDepth === 0) {
361
497
  addToken(TOKEN_TYPES.CLOSE_PAREN, ")");
362
- if (isInInlineHead) isInInlineHead = false;
498
+ if (isInInlineHead) {
499
+ isInInlineHead = false;
500
+ isInHeader = false;
501
+ }
363
502
  } else {
364
503
  addToken(TOKEN_TYPES.TEXT, ")");
365
504
  }
@@ -373,7 +512,7 @@ function lexer(src, filename = "anonymous") {
373
512
  if (isInAtBlockBody || (parenDepth > 0 && !isInInlineHead)) {
374
513
  addToken(TOKEN_TYPES.TEXT, ":");
375
514
  } else {
376
- const allowed = [TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.KEY, TOKEN_TYPES.CLOSE_AT, TOKEN_TYPES.VALUE, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.QUOTE, TOKEN_TYPES.PREFIX_JS, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT];
515
+ const allowed = [TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.KEY, TOKEN_TYPES.CLOSE_AT, TOKEN_TYPES.VALUE, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.QUOTE, TOKEN_TYPES.PREFIX_JS, TOKEN_TYPES.PREFIX_V, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT, TOKEN_TYPES.LOGIC, TOKEN_TYPES.STATIC_KEYWORD, TOKEN_TYPES.RUNTIME_KEYWORD, TOKEN_TYPES.FOR_EACH];
377
516
  if (allowed.includes(last_non_junk_type)) {
378
517
  addToken(TOKEN_TYPES.COLON, ":");
379
518
  isInHeader = true;
@@ -388,7 +527,7 @@ function lexer(src, filename = "anonymous") {
388
527
  if (isInAtBlockBody || (parenDepth > 0 && !isInInlineHead)) {
389
528
  addToken(TOKEN_TYPES.TEXT, "=");
390
529
  } else {
391
- const allowed = [TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.KEY, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.QUOTE, TOKEN_TYPES.PREFIX_JS, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT];
530
+ const allowed = [TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.KEY, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.QUOTE, TOKEN_TYPES.PREFIX_JS, TOKEN_TYPES.PREFIX_V, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT, TOKEN_TYPES.LOGIC, TOKEN_TYPES.STATIC_KEYWORD, TOKEN_TYPES.RUNTIME_KEYWORD, TOKEN_TYPES.FOR_EACH];
392
531
  if (allowed.includes(last_non_junk_type)) {
393
532
  addToken(TOKEN_TYPES.EQUAL, "=");
394
533
  } else {
@@ -402,7 +541,7 @@ function lexer(src, filename = "anonymous") {
402
541
  if (isInAtBlockBody || (parenDepth > 0 && !isInInlineHead)) {
403
542
  addToken(TOKEN_TYPES.TEXT, ",");
404
543
  } else {
405
- const allowed = [TOKEN_TYPES.VALUE, TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.QUOTE, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.PREFIX_JS, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT];
544
+ const allowed = [TOKEN_TYPES.VALUE, TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.QUOTE, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.PREFIX_JS, TOKEN_TYPES.PREFIX_V, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT, TOKEN_TYPES.LOGIC, TOKEN_TYPES.STATIC_KEYWORD, TOKEN_TYPES.RUNTIME_KEYWORD, TOKEN_TYPES.FOR_EACH];
406
545
  if (allowed.includes(last_non_junk_type)) {
407
546
  addToken(TOKEN_TYPES.COMMA, ",");
408
547
  } else {
@@ -416,16 +555,14 @@ function lexer(src, filename = "anonymous") {
416
555
  if (isInAtBlockBody || (parenDepth > 0 && !isInInlineHead)) {
417
556
  addToken(TOKEN_TYPES.TEXT, ";");
418
557
  } else {
419
- const allowed = [TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.VALUE, TOKEN_TYPES.CLOSE_AT, TOKEN_TYPES.CLOSE_PAREN, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.QUOTE, TOKEN_TYPES.PREFIX_JS, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT];
558
+ const allowed = [TOKEN_TYPES.IDENTIFIER, TOKEN_TYPES.VALUE, TOKEN_TYPES.CLOSE_AT, TOKEN_TYPES.CLOSE_PAREN, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.QUOTE, TOKEN_TYPES.PREFIX_JS, TOKEN_TYPES.PREFIX_V, TOKEN_TYPES.PREFIX_P, TOKEN_TYPES.IMPORT, TOKEN_TYPES.USE_MODULE, TOKEN_TYPES.END_KEYWORD, TOKEN_TYPES.TEXT, TOKEN_TYPES.LOGIC, TOKEN_TYPES.STATIC_KEYWORD, TOKEN_TYPES.RUNTIME_KEYWORD, TOKEN_TYPES.FOR_EACH];
420
559
  if (allowed.includes(last_non_junk_type)) {
421
560
  addToken(TOKEN_TYPES.SEMICOLON, ";");
422
- isInHeader = false; // Semicolon ends the At-Block header
423
- // Trigger body mode for At-Blocks
424
- if (delimiterStack.length > 0) {
425
- const top = delimiterStack[delimiterStack.length - 1];
426
- if (top === "@") {
427
- isInAtBlockBody = true;
428
- }
561
+ // ONLY trigger body mode if we were actually in an At-Block header
562
+ if (isInAtBlockHeader) {
563
+ isInHeader = false;
564
+ isInAtBlockHeader = false;
565
+ isInAtBlockBody = true;
429
566
  }
430
567
  } else {
431
568
  addToken(TOKEN_TYPES.TEXT, ";");
@@ -434,6 +571,13 @@ function lexer(src, filename = "anonymous") {
434
571
  i++;
435
572
  continue;
436
573
  }
574
+ if (char === "!") {
575
+ if (isInHeader) {
576
+ addToken(TOKEN_TYPES.EXCLAMATION_MARK, "!");
577
+ i++;
578
+ continue;
579
+ }
580
+ }
437
581
  if (char === "\"" || char === "'") {
438
582
  const valTriggers = [TOKEN_TYPES.COLON, TOKEN_TYPES.EQUAL, TOKEN_TYPES.COMMA, TOKEN_TYPES.ESCAPE, TOKEN_TYPES.OPEN_BRACKET, TOKEN_TYPES.OPEN_AT];
439
583
  const wasValueTrigger = valTriggers.includes(last_non_junk_type);
@@ -455,28 +599,40 @@ function lexer(src, filename = "anonymous") {
455
599
  // At-Blocks (@_) and Inlines (->( )) do NOT allow ':' in the ID.
456
600
  const isStartOfBlockId = (last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET);
457
601
 
458
- let stopChars = "[](){}:=;,@_>\"'#\\ \t\n\r";
602
+ let stopChars = "[](){}:=;,@>\"'#\\ \t\n\r!";
459
603
  if (isStartOfBlockId || (parenDepth > 0 && !isInInlineHead)) {
460
604
  stopChars = stopChars.replace(":", "");
461
605
  }
462
- if (!isInHeader && !isInInlineHead) {
463
- stopChars = "[]@_()\\#\n\r"; // In normal text, stop at markers, comments and newlines
606
+ const isInNormalText = !isInHeader && !isInInlineHead && !isInAtBlockBody;
607
+ if (isInNormalText) {
608
+ stopChars = "[]@()>_()\\#\n\r"; // In normal text, stop at markers, comments and newlines
464
609
  }
465
610
 
466
- while (i < src.length && !stopChars.includes(src[i])) {
611
+ while (i < src.length && !stopChars.includes(src[i])) {
612
+ // Stop ONLY if $ is followed by { (Logic block start)
613
+ if (src[i] === "$" && src[i + 1] === "{") break;
614
+
615
+ // Lookahead for At-Block markers (_@ or @_)
616
+ if (src[i] === "_" && src[i + 1] === "@") break;
617
+ if (src[i] === "@" && src[i + 1] === "_") break;
618
+
619
+ // Lookahead for 'static ${' or 'runtime ${' (only if we're not at the very start of the word scanning)
620
+ if (word.length > 0) {
621
+ if (src[i] === "s" && src.slice(i, i + 7) === "static " && src[i + 7] === "$" && src[i + 8] === "{") break;
622
+ if (src[i] === "s" && src.slice(i, i + 6) === "static" && src[i + 6] === "$" && src[i + 7] === "{") break;
623
+ if (src[i] === "r" && src.slice(i, i + 8) === "runtime " && src[i + 8] === "$" && src[i + 9] === "{") break;
624
+ if (src[i] === "r" && src.slice(i, i + 7) === "runtime" && src[i + 7] === "$" && src[i + 8] === "{") break;
625
+ }
626
+
467
627
  // Lookahead for -> marker in normal text
468
- if (!isInHeader && src[i] === "-" && src[i+1] === ">") break;
628
+ if (!isInHeader && src[i] === "-" && src[i + 1] === ">") break;
469
629
 
470
630
  // Stop if we hit an ALLOWED prefix trigger
471
- if ((src[i] === "p" && src[i+1] === "{")) {
472
- const top = (delimiterStack.length > 0) ? delimiterStack[delimiterStack.length - 1] : null;
473
- const isInBlockHeader = isInHeader && top === "[";
474
- const isInNormalText = !isInHeader && !isInInlineHead && !isInAtBlockBody && parenDepth === 0;
475
- if (isInBlockHeader || isInNormalText) break;
631
+ if ((src[i] === "p" && src[i + 1] === "{") || (src[i] === "v" && src[i + 1] === "{")) {
632
+ if (isInHeader || isInNormalText) break;
476
633
  }
477
- if (src[i] === "j" && src[i+1] === "s" && src[i+2] === "{") {
478
- const top = (delimiterStack.length > 0) ? delimiterStack[delimiterStack.length - 1] : null;
479
- if (isInHeader && top === "[") break;
634
+ if (src[i] === "j" && src[i + 1] === "s" && src[i + 2] === "{") {
635
+ if (isInHeader) break;
480
636
  }
481
637
  word += src[i];
482
638
  i++;
@@ -490,7 +646,7 @@ function lexer(src, filename = "anonymous") {
490
646
  } else if (isInHeader || isInInlineHead) {
491
647
  // Inside a structural header context
492
648
  const isMainIdentifier = (
493
- last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET ||
649
+ last_non_junk_type === TOKEN_TYPES.OPEN_BRACKET ||
494
650
  last_non_junk_type === TOKEN_TYPES.OPEN_AT ||
495
651
  (last_non_junk_type === TOKEN_TYPES.OPEN_PAREN && isInInlineHead)
496
652
  );
@@ -498,23 +654,34 @@ function lexer(src, filename = "anonymous") {
498
654
  if (isMainIdentifier) {
499
655
  if (word === end_keyword) {
500
656
  addToken(TOKEN_TYPES.END_KEYWORD, word);
501
- if (delimiterStack[delimiterStack.length - 1] === "[") delimiterStack.pop();
502
657
  }
503
658
  else if (word === "import") addToken(TOKEN_TYPES.IMPORT, word);
504
659
  else if (word === "$use-module") addToken(TOKEN_TYPES.USE_MODULE, word);
660
+ else if (word === "slot") addToken(TOKEN_TYPES.SLOT_KEYWORD, word);
661
+ else if (word === "for-each") addToken(TOKEN_TYPES.FOR_EACH, word);
505
662
  else addToken(TOKEN_TYPES.IDENTIFIER, word);
506
663
  } else {
507
664
  // Use lookahead to distinguish KEY from VALUE
508
665
  const p = peekStructural(i);
509
666
  if (p === ":") {
510
667
  addToken(TOKEN_TYPES.KEY, word);
668
+ } else if (word === "static") {
669
+ addToken(TOKEN_TYPES.STATIC_KEYWORD, word);
670
+ } else if (word === "runtime") {
671
+ addToken(TOKEN_TYPES.RUNTIME_KEYWORD, word);
511
672
  } else {
512
673
  addToken(TOKEN_TYPES.VALUE, word);
513
674
  }
514
675
  }
515
676
  } else {
516
677
  // Normal text
517
- addToken(TOKEN_TYPES.TEXT, word);
678
+ if (word.trim() === "static") {
679
+ addToken(TOKEN_TYPES.STATIC_KEYWORD, word);
680
+ } else if (word.trim() === "runtime") {
681
+ addToken(TOKEN_TYPES.RUNTIME_KEYWORD, word);
682
+ } else {
683
+ addToken(TOKEN_TYPES.TEXT, word);
684
+ }
518
685
  }
519
686
  } else {
520
687
  // Fallback for any unhandled characters