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.
- package/README.md +304 -73
- package/cli/cli.mjs +1 -1
- package/cli/commands/build.js +3 -1
- package/cli/commands/help.js +2 -0
- package/cli/commands/init.js +25 -6
- package/cli/constants.js +2 -1
- package/cli/helpers/transpile.js +5 -2
- package/constants/html_props.js +1 -0
- package/core/evaluator.js +1061 -0
- package/core/formats.js +15 -7
- package/core/helpers/config-loader.js +16 -8
- package/core/helpers/lib.js +72 -0
- package/core/helpers/preprocessor.js +202 -0
- package/core/helpers/runtimeOutput.js +28 -0
- package/core/helpers/url.js +12 -0
- package/core/labels.js +9 -2
- package/core/lexer.js +228 -61
- package/core/modules.js +338 -60
- package/core/parser.js +275 -55
- package/core/tokenTypes.js +11 -0
- package/core/transpiler.js +352 -66
- package/core/validator.js +70 -7
- package/formatter/tag.js +31 -7
- package/grammar.ebnf +21 -10
- package/helpers/fetch-fs.js +37 -0
- package/helpers/safeDataParser.js +3 -3
- package/helpers/spinner.js +97 -0
- package/helpers/utils.js +46 -0
- package/helpers/virtual-fs.js +29 -0
- package/index.browser.js +87 -0
- package/index.js +23 -332
- package/index.shared.js +443 -0
- package/mappers/languages/html.js +50 -9
- package/mappers/languages/json.js +81 -38
- package/mappers/languages/jsonc.js +82 -0
- package/mappers/languages/markdown.js +88 -48
- package/mappers/languages/mdx.js +50 -15
- package/mappers/languages/text.js +67 -0
- package/mappers/languages/xml.js +6 -6
- package/mappers/mapper.js +36 -4
- package/mappers/shared/index.js +12 -13
- package/package.json +11 -2
- 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] :
|
|
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.
|
|
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
|
-
|
|
517
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
679
|
-
i
|
|
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
|
|
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,
|
|
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 -
|
|
750
|
-
* @param {Object}
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
947
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
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
|
}
|