sommark 2.3.2 → 3.0.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 +47 -42
- package/SOMMARK-SPEC.md +483 -0
- package/cli/cli.mjs +42 -2
- package/cli/commands/color.js +36 -0
- package/cli/commands/help.js +7 -0
- package/cli/commands/init.js +2 -0
- package/cli/commands/list.js +119 -0
- package/cli/commands/print.js +61 -11
- package/cli/commands/show.js +24 -27
- package/cli/constants.js +1 -1
- package/cli/helpers/config.js +14 -4
- package/cli/helpers/transpile.js +27 -32
- package/constants/html_props.js +100 -0
- package/constants/html_tags.js +146 -0
- package/constants/void_elements.js +26 -0
- package/core/lexer.js +70 -39
- package/core/parser.js +100 -84
- package/core/pluginManager.js +139 -0
- package/core/plugins/comment-remover.js +47 -0
- package/core/plugins/module-system.js +137 -0
- package/core/plugins/quote-escaper.js +37 -0
- package/core/plugins/raw-content-plugin.js +72 -0
- package/core/plugins/rules-validation-plugin.js +197 -0
- package/core/plugins/sommark-format.js +211 -0
- package/core/transpiler.js +65 -198
- package/debug.js +9 -4
- package/format.js +23 -0
- package/formatter/mark.js +3 -3
- package/formatter/tag.js +6 -2
- package/grammar.ebnf +5 -5
- package/helpers/camelize.js +2 -0
- package/helpers/colorize.js +20 -14
- package/helpers/kebabize.js +2 -0
- package/helpers/utils.js +161 -0
- package/index.js +243 -44
- package/mappers/languages/html.js +200 -105
- package/mappers/languages/json.js +23 -4
- package/mappers/languages/markdown.js +88 -67
- package/mappers/languages/mdx.js +130 -2
- package/mappers/mapper.js +77 -246
- package/package.json +7 -5
- package/unformatted.smark +90 -0
- package/v3-todo.smark +75 -0
- package/CHANGELOG.md +0 -119
- package/helpers/loadCss.js +0 -46
package/core/parser.js
CHANGED
|
@@ -22,10 +22,10 @@ function current_token(tokens, i) {
|
|
|
22
22
|
|
|
23
23
|
function validateName(
|
|
24
24
|
id,
|
|
25
|
-
keyRegex = /^[a-zA-Z0-9]+$/,
|
|
25
|
+
keyRegex = /^[a-zA-Z0-9\-_$]+$/,
|
|
26
26
|
name = "Identifier",
|
|
27
|
-
rule = "(A–Z, a–z, 0–9)",
|
|
28
|
-
ruleMessage = "must contain only letters
|
|
27
|
+
rule = "(A–Z, a–z, 0–9, -, _, $)",
|
|
28
|
+
ruleMessage = "must contain only letters, numbers, hyphens, underscores, or dollar signs ($)"
|
|
29
29
|
) {
|
|
30
30
|
if (!keyRegex.test(id)) {
|
|
31
31
|
parserError([`{line}<$red:Invalid ${name}:$><$blue: '${id}'$>{N}<$yellow:${name} ${ruleMessage}$> <$cyan: ${rule}.$>{line}`]);
|
|
@@ -103,12 +103,33 @@ const updateData = (tokens, i) => {
|
|
|
103
103
|
};
|
|
104
104
|
|
|
105
105
|
const errorMessage = (tokens, i, expectedValue, behindValue, frontText) => {
|
|
106
|
-
const tokensUntilError = tokens.slice(0, i);
|
|
107
|
-
const contextText = tokensUntilError.map(t => t.value).join("");
|
|
108
|
-
const pointerPadding = " ".repeat(contextText.length);
|
|
109
106
|
const current = tokens[i] ?? fallback;
|
|
107
|
+
const errorLineNumber = current.line;
|
|
108
|
+
|
|
109
|
+
// Find starting index of the error line
|
|
110
|
+
let lineStartIndex = i;
|
|
111
|
+
while (lineStartIndex > 0 && tokens[lineStartIndex - 1].line === errorLineNumber) {
|
|
112
|
+
lineStartIndex--;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Find ending index of the error line
|
|
116
|
+
let lineEndIndex = i;
|
|
117
|
+
while (lineEndIndex < tokens.length - 1 && tokens[lineEndIndex + 1].line === errorLineNumber) {
|
|
118
|
+
lineEndIndex++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Get all tokens on the error line
|
|
122
|
+
const lineTokens = tokens.slice(lineStartIndex, lineEndIndex + 1);
|
|
123
|
+
const lineContent = lineTokens.map(t => t.value).join('');
|
|
124
|
+
|
|
125
|
+
// Get content on the line before the error token
|
|
126
|
+
const tokensBeforeErrorOnLine = tokens.slice(lineStartIndex, i);
|
|
127
|
+
const contentBeforeErrorOnLine = tokensBeforeErrorOnLine.map(t => t.value).join('');
|
|
128
|
+
|
|
129
|
+
const pointerPadding = " ".repeat(contentBeforeErrorOnLine.length);
|
|
130
|
+
|
|
110
131
|
return [
|
|
111
|
-
`<$blue:{line}$><$red:Here where error occurred:$>{N}${
|
|
132
|
+
`<$blue:{line}$><$red:Here where error occurred:$>{N}${lineContent}{N}${pointerPadding}<$yellow:^$>{N}{N}`,
|
|
112
133
|
`<$red:${frontText ? frontText : "Expected token"}$> <$blue:'${expectedValue}'$> ${behindValue ? "after <$blue:'" + behindValue + "'$>" : ""} at line <$yellow:${line}$>,`,
|
|
113
134
|
` from column <$yellow: ${start}$> to <$yellow: ${end}$>`,
|
|
114
135
|
`{N}<$yellow:Received:$> <$blue:'${value === "\n" ? "\\n' (newline)" : value}'$>`,
|
|
@@ -133,12 +154,11 @@ function parseKey(tokens, i) {
|
|
|
133
154
|
// Parse Value //
|
|
134
155
|
// ========================================================================== //
|
|
135
156
|
function parseValue(tokens, i) {
|
|
136
|
-
let
|
|
137
|
-
//
|
|
138
|
-
// consume Value //
|
|
139
|
-
// ========================================================================== //
|
|
157
|
+
let val = current_token(tokens, i).value;
|
|
158
|
+
// consume Value
|
|
140
159
|
i++;
|
|
141
|
-
|
|
160
|
+
updateData(tokens, i);
|
|
161
|
+
return [val, i];
|
|
142
162
|
}
|
|
143
163
|
// ========================================================================== //
|
|
144
164
|
// Parse ',' //
|
|
@@ -222,8 +242,8 @@ function parseBlock(tokens, i) {
|
|
|
222
242
|
parserError(errorMessage(tokens, i, block_id, "["));
|
|
223
243
|
}
|
|
224
244
|
const id = current_token(tokens, i).value;
|
|
225
|
-
if (id === end_keyword) {
|
|
226
|
-
parserError(errorMessage(tokens, i, id, "", `'${id}' is a reserved keyword and cannot be used as an identifier.`));
|
|
245
|
+
if (id.trim() === end_keyword) {
|
|
246
|
+
parserError(errorMessage(tokens, i, id, "", `'${id.trim()}' is a reserved keyword and cannot be used as an identifier.`));
|
|
227
247
|
}
|
|
228
248
|
blockNode.id = id.trim();
|
|
229
249
|
validateName(blockNode.id);
|
|
@@ -297,6 +317,10 @@ function parseBlock(tokens, i) {
|
|
|
297
317
|
}
|
|
298
318
|
|
|
299
319
|
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COMMA) {
|
|
320
|
+
v = v.trim();
|
|
321
|
+
if (v.startsWith('"') && v.endsWith('"')) {
|
|
322
|
+
v = v.slice(1, -1);
|
|
323
|
+
}
|
|
300
324
|
blockNode.args.push(v);
|
|
301
325
|
if (k) {
|
|
302
326
|
blockNode.args[k] = v;
|
|
@@ -312,6 +336,10 @@ function parseBlock(tokens, i) {
|
|
|
312
336
|
}
|
|
313
337
|
}
|
|
314
338
|
if (v !== "") {
|
|
339
|
+
v = v.trim();
|
|
340
|
+
if (v.startsWith('"') && v.endsWith('"')) {
|
|
341
|
+
v = v.slice(1, -1);
|
|
342
|
+
}
|
|
315
343
|
blockNode.args.push(v);
|
|
316
344
|
if (k) {
|
|
317
345
|
blockNode.args[k] = v;
|
|
@@ -347,18 +375,17 @@ function parseBlock(tokens, i) {
|
|
|
347
375
|
// consume child node //
|
|
348
376
|
// ========================================================================== //
|
|
349
377
|
i = nextIndex;
|
|
350
|
-
updateData(tokens, i);
|
|
351
378
|
} else if (
|
|
352
379
|
current_token(tokens, i) &&
|
|
353
380
|
current_token(tokens, i).type === TOKEN_TYPES.OPEN_BRACKET &&
|
|
354
381
|
peek(tokens, i, 1) &&
|
|
355
|
-
peek(tokens, i, 1).type === TOKEN_TYPES.END_KEYWORD
|
|
382
|
+
(peek(tokens, i, 1).type === TOKEN_TYPES.END_KEYWORD || peek(tokens, i, 1).value.trim() === end_keyword)
|
|
356
383
|
) {
|
|
357
384
|
// ========================================================================== //
|
|
358
385
|
// consume '[' //
|
|
359
386
|
// ========================================================================== //
|
|
360
387
|
i++;
|
|
361
|
-
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.END_KEYWORD)) {
|
|
388
|
+
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.END_KEYWORD && current_token(tokens, i).value.trim() !== end_keyword)) {
|
|
362
389
|
parserError(errorMessage(tokens, i, "end", "["));
|
|
363
390
|
}
|
|
364
391
|
// ========================================================================== //
|
|
@@ -387,7 +414,6 @@ function parseBlock(tokens, i) {
|
|
|
387
414
|
}
|
|
388
415
|
blockNode.body.push(childNode);
|
|
389
416
|
i = nextIndex;
|
|
390
|
-
updateData(tokens, i);
|
|
391
417
|
}
|
|
392
418
|
}
|
|
393
419
|
return [blockNode, i];
|
|
@@ -402,34 +428,21 @@ function parseInline(tokens, i) {
|
|
|
402
428
|
// ========================================================================== //
|
|
403
429
|
i++;
|
|
404
430
|
updateData(tokens, i);
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
(current_token(tokens, i) &&
|
|
408
|
-
current_token(tokens, i).type !== TOKEN_TYPES.VALUE &&
|
|
409
|
-
current_token(tokens, i).type !== TOKEN_TYPES.ESCAPE)
|
|
410
|
-
) {
|
|
411
|
-
parserError(errorMessage(tokens, i, inline_value, "("));
|
|
431
|
+
if (current_token(tokens, i)) {
|
|
432
|
+
inlineNode.depth = current_token(tokens, i).depth;
|
|
412
433
|
}
|
|
413
|
-
inlineNode.depth = current_token(tokens, i).depth;
|
|
414
|
-
updateData(tokens, i);
|
|
415
434
|
while (i < tokens.length) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
// ========================================================================== //
|
|
419
|
-
// consume Inline Value //
|
|
420
|
-
// ========================================================================== //
|
|
421
|
-
i++;
|
|
422
|
-
continue;
|
|
423
|
-
} else if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.ESCAPE) {
|
|
424
|
-
inlineNode.value += current_token(tokens, i).value.slice(1);
|
|
425
|
-
// ========================================================================== //
|
|
426
|
-
// consume Escape Character '\' //
|
|
427
|
-
// ========================================================================== //
|
|
428
|
-
i++;
|
|
429
|
-
continue;
|
|
430
|
-
} else {
|
|
435
|
+
const token = current_token(tokens, i);
|
|
436
|
+
if (!token || token.type === TOKEN_TYPES.CLOSE_PAREN) {
|
|
431
437
|
break;
|
|
432
438
|
}
|
|
439
|
+
if (token.type === TOKEN_TYPES.ESCAPE) {
|
|
440
|
+
inlineNode.value += token.value.slice(1);
|
|
441
|
+
} else {
|
|
442
|
+
inlineNode.value += token.value;
|
|
443
|
+
}
|
|
444
|
+
i++;
|
|
445
|
+
updateData(tokens, i);
|
|
433
446
|
}
|
|
434
447
|
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.CLOSE_PAREN)) {
|
|
435
448
|
parserError(errorMessage(tokens, i, ")", inline_value));
|
|
@@ -459,6 +472,9 @@ function parseInline(tokens, i) {
|
|
|
459
472
|
parserError(errorMessage(tokens, i, inline_id, "("));
|
|
460
473
|
}
|
|
461
474
|
inlineNode.id = current_token(tokens, i).value.trim();
|
|
475
|
+
if (inlineNode.id === end_keyword) {
|
|
476
|
+
parserError(errorMessage(tokens, i, inlineNode.id, "", `'${inlineNode.id}' is a reserved keyword and cannot be used as an identifier.`));
|
|
477
|
+
}
|
|
462
478
|
validateName(inlineNode.id);
|
|
463
479
|
// ========================================================================== //
|
|
464
480
|
// consume Inline Identifier //
|
|
@@ -478,10 +494,11 @@ function parseInline(tokens, i) {
|
|
|
478
494
|
let v = "";
|
|
479
495
|
const pushArg = () => {
|
|
480
496
|
if (v !== "") {
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
|
|
497
|
+
v = v.trim();
|
|
498
|
+
if (v.startsWith('"') && v.endsWith('"')) {
|
|
499
|
+
v = v.slice(1, -1);
|
|
484
500
|
}
|
|
501
|
+
inlineNode.args.push(v);
|
|
485
502
|
v = "";
|
|
486
503
|
}
|
|
487
504
|
};
|
|
@@ -560,22 +577,28 @@ function parseInline(tokens, i) {
|
|
|
560
577
|
// ========================================================================== //
|
|
561
578
|
// Parse Text //
|
|
562
579
|
// ========================================================================== //
|
|
563
|
-
function parseText(tokens, i) {
|
|
580
|
+
function parseText(tokens, i, options = {}) {
|
|
564
581
|
const textNode = makeTextNode();
|
|
565
582
|
textNode.depth = current_token(tokens, i).depth;
|
|
583
|
+
const { selectiveUnescape = false } = options;
|
|
584
|
+
|
|
566
585
|
while (i < tokens.length) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
// consume Text Node //
|
|
571
|
-
// ========================================================================== //
|
|
586
|
+
const token = current_token(tokens, i);
|
|
587
|
+
if (token && token.type === TOKEN_TYPES.TEXT) {
|
|
588
|
+
textNode.text += token.value;
|
|
572
589
|
i++;
|
|
573
590
|
updateData(tokens, i);
|
|
574
|
-
} else if (
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
591
|
+
} else if (token && token.type === TOKEN_TYPES.ESCAPE) {
|
|
592
|
+
if (selectiveUnescape) {
|
|
593
|
+
const char = token.value.slice(1);
|
|
594
|
+
if (char === "@" || char === "_") {
|
|
595
|
+
textNode.text += char;
|
|
596
|
+
} else {
|
|
597
|
+
textNode.text += token.value;
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
textNode.text += token.value.slice(1); // Standard behavior: unescape all
|
|
601
|
+
}
|
|
579
602
|
i++;
|
|
580
603
|
updateData(tokens, i);
|
|
581
604
|
} else {
|
|
@@ -595,8 +618,8 @@ function parseAtBlock(tokens, i) {
|
|
|
595
618
|
i++;
|
|
596
619
|
updateData(tokens, i);
|
|
597
620
|
const id = current_token(tokens, i).value;
|
|
598
|
-
if (id === end_keyword) {
|
|
599
|
-
parserError(errorMessage(tokens, i, id, "", `'${id}' is a reserved keyword and cannot be used as an identifier.`));
|
|
621
|
+
if (id.trim() === end_keyword) {
|
|
622
|
+
parserError(errorMessage(tokens, i, id, "", `'${id.trim()}' is a reserved keyword and cannot be used as an identifier.`));
|
|
600
623
|
}
|
|
601
624
|
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.IDENTIFIER) {
|
|
602
625
|
atBlockNode.id = id.trim();
|
|
@@ -618,21 +641,6 @@ function parseAtBlock(tokens, i) {
|
|
|
618
641
|
// ========================================================================== //
|
|
619
642
|
i++;
|
|
620
643
|
updateData(tokens, i);
|
|
621
|
-
if (
|
|
622
|
-
current_token(tokens, i) &&
|
|
623
|
-
current_token(tokens, i).type === TOKEN_TYPES.TEXT &&
|
|
624
|
-
(current_token(tokens, i).value.includes("[") || current_token(tokens, i).value.includes("]"))
|
|
625
|
-
) {
|
|
626
|
-
parserError(
|
|
627
|
-
errorMessage(
|
|
628
|
-
tokens,
|
|
629
|
-
i,
|
|
630
|
-
current_token(tokens, i).value,
|
|
631
|
-
"",
|
|
632
|
-
`SomMark uses a scope-based state system to control tokenizing.When @_ is encountered, tokenizing is turned off.Tokenizing is turned back on after the lexer encounters @_end_@. If the At-Block syntax is not completed, all remaining characters are concatenated and treated as plain text until the end of input.`
|
|
633
|
-
)
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
644
|
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COLON) {
|
|
637
645
|
// ========================================================================== //
|
|
638
646
|
// consume ':' //
|
|
@@ -683,6 +691,10 @@ function parseAtBlock(tokens, i) {
|
|
|
683
691
|
break;
|
|
684
692
|
}
|
|
685
693
|
}
|
|
694
|
+
v = v.trim();
|
|
695
|
+
if (v.startsWith('"') && v.endsWith('"')) {
|
|
696
|
+
v = v.slice(1, -1);
|
|
697
|
+
}
|
|
686
698
|
atBlockNode.args.push(v);
|
|
687
699
|
if (k) {
|
|
688
700
|
atBlockNode.args[k] = v;
|
|
@@ -698,11 +710,11 @@ function parseAtBlock(tokens, i) {
|
|
|
698
710
|
break;
|
|
699
711
|
}
|
|
700
712
|
}
|
|
701
|
-
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.SEMICOLON)) {
|
|
702
|
-
parserError(errorMessage(tokens, i, ";", at_value));
|
|
703
|
-
}
|
|
704
|
-
i = parseSemiColon(tokens, i, at_value);
|
|
705
713
|
}
|
|
714
|
+
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.SEMICOLON)) {
|
|
715
|
+
parserError(errorMessage(tokens, i, ";", at_value));
|
|
716
|
+
}
|
|
717
|
+
i = parseSemiColon(tokens, i, at_value);
|
|
706
718
|
if (
|
|
707
719
|
!current_token(tokens, i) ||
|
|
708
720
|
(current_token(tokens, i) &&
|
|
@@ -715,7 +727,7 @@ function parseAtBlock(tokens, i) {
|
|
|
715
727
|
current_token(tokens, i) &&
|
|
716
728
|
(current_token(tokens, i).type === TOKEN_TYPES.TEXT || current_token(tokens, i).type === TOKEN_TYPES.ESCAPE)
|
|
717
729
|
) {
|
|
718
|
-
const [childNode, nextIndex] = parseText(tokens, i);
|
|
730
|
+
const [childNode, nextIndex] = parseText(tokens, i, { selectiveUnescape: true });
|
|
719
731
|
atBlockNode.content = childNode.text;
|
|
720
732
|
i = nextIndex;
|
|
721
733
|
updateData(tokens, i);
|
|
@@ -728,7 +740,7 @@ function parseAtBlock(tokens, i) {
|
|
|
728
740
|
// ========================================================================== //
|
|
729
741
|
i++;
|
|
730
742
|
updateData(tokens, i);
|
|
731
|
-
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.END_KEYWORD)) {
|
|
743
|
+
if (!current_token(tokens, i) || (current_token(tokens, i) && current_token(tokens, i).type !== TOKEN_TYPES.END_KEYWORD && current_token(tokens, i).value.trim() !== end_keyword)) {
|
|
732
744
|
parserError(errorMessage(tokens, i, end_keyword, "@_"));
|
|
733
745
|
}
|
|
734
746
|
// ========================================================================== //
|
|
@@ -775,9 +787,13 @@ function parseNode(tokens, i) {
|
|
|
775
787
|
return parseCommentNode(tokens, i);
|
|
776
788
|
}
|
|
777
789
|
// ========================================================================== //
|
|
778
|
-
// Block
|
|
790
|
+
// Block or Reserved Keyword //
|
|
779
791
|
// ========================================================================== //
|
|
780
|
-
else if (current_token(tokens, i)
|
|
792
|
+
else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.OPEN_BRACKET)) {
|
|
793
|
+
const next = peek(tokens, i, 1);
|
|
794
|
+
if (next && (next.type === TOKEN_TYPES.END_KEYWORD || next.value.trim() === end_keyword)) {
|
|
795
|
+
parserError(errorMessage(tokens, i + 1, "Block ID", "[", `'${next.value.trim()}' is a reserved keyword and cannot be used as a start identifier.`));
|
|
796
|
+
}
|
|
781
797
|
return parseBlock(tokens, i);
|
|
782
798
|
}
|
|
783
799
|
// ========================================================================== //
|
|
@@ -798,7 +814,7 @@ function parseNode(tokens, i) {
|
|
|
798
814
|
// ========================================================================== //
|
|
799
815
|
// Atblock //
|
|
800
816
|
// ========================================================================== //
|
|
801
|
-
else if (current_token(tokens, i).
|
|
817
|
+
else if (current_token(tokens, i) && (current_token(tokens, i).type === TOKEN_TYPES.OPEN_AT)) {
|
|
802
818
|
return parseAtBlock(tokens, i);
|
|
803
819
|
} else {
|
|
804
820
|
parserError(errorMessage(tokens, i, current_token(tokens, i).value, "", "Syntax Error:"));
|
|
@@ -817,8 +833,8 @@ function parser(tokens) {
|
|
|
817
833
|
let i = 0;
|
|
818
834
|
while (i < tokens.length) {
|
|
819
835
|
let [nodes, nextIndex] = parseNode(tokens, i);
|
|
820
|
-
if (
|
|
821
|
-
parserError(errorMessage(tokens, i, "
|
|
836
|
+
if (nodes && nodes.type !== "Comment" && nodes.depth === 0) {
|
|
837
|
+
parserError(errorMessage(tokens, i, "Top-level Block", "", "Top-level content must be wrapped in a block. Found:"));
|
|
822
838
|
}
|
|
823
839
|
if (nodes) {
|
|
824
840
|
ast.push(nodes);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export default class PluginManager {
|
|
2
|
+
constructor(plugins = [], priority = []) {
|
|
3
|
+
this.plugins = [...plugins].sort((a, b) => {
|
|
4
|
+
const getIndex = (p) => {
|
|
5
|
+
// ========================================================================== //
|
|
6
|
+
// Priority array can contain plugin objects or plugin names (for built-ins)
|
|
7
|
+
// ========================================================================== //
|
|
8
|
+
const index = priority.findIndex(item => {
|
|
9
|
+
if (typeof item === "string") {
|
|
10
|
+
return item === p.name;
|
|
11
|
+
}
|
|
12
|
+
return item === p;
|
|
13
|
+
});
|
|
14
|
+
return index;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const indexA = getIndex(a);
|
|
18
|
+
const indexB = getIndex(b);
|
|
19
|
+
|
|
20
|
+
// ========================================================================== //
|
|
21
|
+
// 1. Both have a priority index
|
|
22
|
+
// ========================================================================== //
|
|
23
|
+
if (indexA !== -1 && indexB !== -1) {
|
|
24
|
+
return indexA - indexB;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ========================================================================== //
|
|
28
|
+
// 2. Only A has a priority
|
|
29
|
+
// ========================================================================== //
|
|
30
|
+
if (indexA !== -1) return -1;
|
|
31
|
+
|
|
32
|
+
// ========================================================================== //
|
|
33
|
+
// 3. Only B has a priority
|
|
34
|
+
// ========================================================================== //
|
|
35
|
+
if (indexB !== -1) return 1;
|
|
36
|
+
|
|
37
|
+
// ========================================================================== //
|
|
38
|
+
// 4. Neither have a priority
|
|
39
|
+
// Default rule: built-ins first, then external/user-defined
|
|
40
|
+
// ========================================================================== //
|
|
41
|
+
const isBuiltInA = ["module-system", "quote-escaper", "raw-content", "comment-remover", "rules-validation", "sommark-format"].includes(a.name); // Hardcoded for now based on current project
|
|
42
|
+
const isBuiltInB = ["module-system", "quote-escaper", "raw-content", "comment-remover", "rules-validation", "sommark-format"].includes(b.name);
|
|
43
|
+
|
|
44
|
+
if (isBuiltInA && !isBuiltInB) return -1;
|
|
45
|
+
if (!isBuiltInA && isBuiltInB) return 1;
|
|
46
|
+
|
|
47
|
+
return 0;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async runPreprocessor(src, scope) {
|
|
52
|
+
let processedSrc = src;
|
|
53
|
+
const preprocessors = this.plugins.filter(p => {
|
|
54
|
+
const types = Array.isArray(p.type) ? p.type : [p.type];
|
|
55
|
+
return types.includes("preprocessor") && (p.scope === scope || !p.scope);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
for (const plugin of preprocessors) {
|
|
59
|
+
if (typeof plugin.beforeLex === "function") {
|
|
60
|
+
processedSrc = await plugin.beforeLex.call(plugin, processedSrc);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return processedSrc;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async runAfterLex(tokens) {
|
|
67
|
+
let processedTokens = tokens;
|
|
68
|
+
const afterLexers = this.plugins.filter(p => {
|
|
69
|
+
const types = Array.isArray(p.type) ? p.type : [p.type];
|
|
70
|
+
return (types.includes("lexer") || types.includes("after-lexer")) && typeof p.afterLex === "function";
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
for (const plugin of afterLexers) {
|
|
74
|
+
processedTokens = await plugin.afterLex.call(plugin, processedTokens);
|
|
75
|
+
}
|
|
76
|
+
return processedTokens;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async runOnAst(ast, context = {}) {
|
|
80
|
+
let processedAst = ast;
|
|
81
|
+
const astPlugins = this.plugins.filter(p => {
|
|
82
|
+
const types = Array.isArray(p.type) ? p.type : [p.type];
|
|
83
|
+
return (types.includes("parser") || types.includes("on-ast")) && typeof p.onAst === "function";
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
for (const plugin of astPlugins) {
|
|
87
|
+
processedAst = await plugin.onAst.call(plugin, processedAst, context);
|
|
88
|
+
}
|
|
89
|
+
return processedAst;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getMapperExtensions() {
|
|
93
|
+
const extensions = { outputs: [], rules: {} };
|
|
94
|
+
const mapperPlugins = this.plugins.filter(p => {
|
|
95
|
+
const types = Array.isArray(p.type) ? p.type : [p.type];
|
|
96
|
+
return types.includes("mapper");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
for (const plugin of mapperPlugins) {
|
|
100
|
+
if (plugin.outputs) {
|
|
101
|
+
extensions.outputs.push(...plugin.outputs);
|
|
102
|
+
}
|
|
103
|
+
if (plugin.rules) {
|
|
104
|
+
extensions.rules = { ...extensions.rules, ...plugin.rules };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return extensions;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
runRegisterHooks(sm) {
|
|
111
|
+
for (const plugin of this.plugins) {
|
|
112
|
+
const registerFn = plugin.registerOutput || plugin.register;
|
|
113
|
+
if (typeof registerFn === "function") {
|
|
114
|
+
registerFn.call(plugin, sm);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async runTransformers(output) {
|
|
120
|
+
let processedOutput = output;
|
|
121
|
+
const transformers = this.plugins.filter(p => {
|
|
122
|
+
const types = Array.isArray(p.type) ? p.type : [p.type];
|
|
123
|
+
return types.includes("transform") || types.includes("postprocessor");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
for (const plugin of transformers) {
|
|
127
|
+
const transformFn = plugin.transform || plugin.afterTranspile;
|
|
128
|
+
if (typeof transformFn === "function") {
|
|
129
|
+
processedOutput = await transformFn.call(plugin, processedOutput);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return processedOutput;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getFormatMapper(formatName) {
|
|
136
|
+
const plugin = this.plugins.find(p => p.format === formatName && p.mapper);
|
|
137
|
+
return plugin ? plugin.mapper : null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { COMMENT } from "../labels.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in plugin to remove all comments from the AST.
|
|
5
|
+
* Active by default.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
name: "comment-remover",
|
|
9
|
+
type: "on-ast",
|
|
10
|
+
author: "Adam-Elmi",
|
|
11
|
+
description: "Removes all comment nodes from the AST during the parsing phase.",
|
|
12
|
+
onAst: function (ast) {
|
|
13
|
+
// ========================================================================== //
|
|
14
|
+
// Recursive function to filter out comments //
|
|
15
|
+
// ========================================================================== //
|
|
16
|
+
const cleanNodes = (nodes) => {
|
|
17
|
+
if (!Array.isArray(nodes)) return nodes;
|
|
18
|
+
|
|
19
|
+
return nodes
|
|
20
|
+
.filter(node => node.type !== COMMENT)
|
|
21
|
+
.map(node => {
|
|
22
|
+
if (node.body && Array.isArray(node.body)) {
|
|
23
|
+
return {
|
|
24
|
+
...node,
|
|
25
|
+
body: cleanNodes(node.body)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return node;
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
// ========================================================================== //
|
|
32
|
+
// Handle both root array and individual node objects //
|
|
33
|
+
// ========================================================================== //
|
|
34
|
+
if (Array.isArray(ast)) {
|
|
35
|
+
return cleanNodes(ast);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (ast && ast.body) {
|
|
39
|
+
return {
|
|
40
|
+
...ast,
|
|
41
|
+
body: cleanNodes(ast.body)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return ast;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import fsPromises from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { runtimeError } from "../errors.js";
|
|
5
|
+
|
|
6
|
+
const ModuleSystem = {
|
|
7
|
+
name: "module-system",
|
|
8
|
+
type: "preprocessor",
|
|
9
|
+
author: "Adam-Elmi",
|
|
10
|
+
description: "Provides file inclusion capabilities and global variable substitution (e.g., [[import = ...]]).",
|
|
11
|
+
scope: "top-level",
|
|
12
|
+
async beforeLex(src) {
|
|
13
|
+
if (!src) return src;
|
|
14
|
+
|
|
15
|
+
const options = this.options || {};
|
|
16
|
+
const supportedExtensions = options.supportedExtensions || ["smark", "css", "js"];
|
|
17
|
+
const importRegex = /\[\[\s*import\s*=\s*\$([a-zA-Z0-9_-]+)\s*:\s*"([^"]+)"\s*\]\]/g;
|
|
18
|
+
const usageRegex = /\$\[\[([a-zA-Z0-9_-]+)\]\]/g;
|
|
19
|
+
|
|
20
|
+
const imports = [];
|
|
21
|
+
let srcLines = src.split('\n');
|
|
22
|
+
let processedSrc = src;
|
|
23
|
+
|
|
24
|
+
// ========================================================================== //
|
|
25
|
+
// 0. Module Position Validator & 1. Format Validator //
|
|
26
|
+
// ========================================================================== //
|
|
27
|
+
const potentialImportRegex = /\[\[\s*import\s*=/;
|
|
28
|
+
const commentRegex = /^\s*#/;
|
|
29
|
+
let contentStarted = false;
|
|
30
|
+
|
|
31
|
+
srcLines.forEach((line, index) => {
|
|
32
|
+
const isImportCandidate = potentialImportRegex.test(line);
|
|
33
|
+
const isComment = commentRegex.test(line);
|
|
34
|
+
const isEmpty = line.trim() === "";
|
|
35
|
+
|
|
36
|
+
if (isImportCandidate) {
|
|
37
|
+
if (contentStarted) {
|
|
38
|
+
runtimeError([
|
|
39
|
+
`<$red:Module Position Error:$> {N}`,
|
|
40
|
+
`Import found at line <$yellow:${index + 1}$> after content has already started. {N}`,
|
|
41
|
+
`All imports must be at the very top of the file.`
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const match = [...line.matchAll(importRegex)][0];
|
|
46
|
+
if (!match) {
|
|
47
|
+
runtimeError([
|
|
48
|
+
`<$red:Module Format Error:$> {N}`,
|
|
49
|
+
`Invalid import syntax at line <$yellow:${index + 1}$>: <$magenta:${line.trim()}$> {N}`,
|
|
50
|
+
`Expected format: <$green:[[import = $key: "path"]]$>`
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const [fullMatch, key, filePath] = match;
|
|
55
|
+
imports.push({
|
|
56
|
+
fullMatch,
|
|
57
|
+
key,
|
|
58
|
+
path: filePath,
|
|
59
|
+
line: index + 1
|
|
60
|
+
});
|
|
61
|
+
} else if (!isEmpty && !isComment) {
|
|
62
|
+
// This is actual content (blocks, text, ...)
|
|
63
|
+
contentStarted = true;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (imports.length === 0) return src;
|
|
68
|
+
|
|
69
|
+
// ========================================================================== //
|
|
70
|
+
// 2. Module Path Validator //
|
|
71
|
+
// ========================================================================== //
|
|
72
|
+
imports.forEach(imp => {
|
|
73
|
+
// Check existence
|
|
74
|
+
if (!fs.existsSync(imp.path)) {
|
|
75
|
+
runtimeError([
|
|
76
|
+
`<$red:Module Path Error:$> {N}`,
|
|
77
|
+
`File not found: <$magenta:${imp.path}$> {N}`,
|
|
78
|
+
`Imported at line <$yellow:${imp.line}$> with key <$green:${imp.key}$>`
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check extension
|
|
83
|
+
const ext = path.extname(imp.path).slice(1);
|
|
84
|
+
if (!supportedExtensions.includes(ext)) {
|
|
85
|
+
runtimeError([
|
|
86
|
+
`<$red:Module Extension Error:$> {N}`,
|
|
87
|
+
`Unsupported extension <$magenta:.${ext}$> for file <$green:${imp.path}$> {N}`,
|
|
88
|
+
`Supported extensions: <$cyan:${supportedExtensions.join(", ")}$>`
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ========================================================================== //
|
|
94
|
+
// 3. Module Usage Validator //
|
|
95
|
+
// ========================================================================== //
|
|
96
|
+
const usageMatches = [...src.matchAll(usageRegex)];
|
|
97
|
+
const usedKeys = new Set(usageMatches.map(m => m[1]));
|
|
98
|
+
|
|
99
|
+
imports.forEach(imp => {
|
|
100
|
+
if (!usedKeys.has(imp.key)) {
|
|
101
|
+
runtimeError([
|
|
102
|
+
`<$red:Module Usage Error:$> {N}`,
|
|
103
|
+
`Module <$green:$${imp.key}$> is imported but never used. {N}`,
|
|
104
|
+
`Imported at line <$yellow:${imp.line}$>: <$magenta:${imp.fullMatch}$>`
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ========================================================================== //
|
|
110
|
+
// 4. Substitution //
|
|
111
|
+
// ========================================================================== //
|
|
112
|
+
// Replace $[[key]] with content
|
|
113
|
+
for (const match of usageMatches) {
|
|
114
|
+
const key = match[1];
|
|
115
|
+
const imp = imports.find(i => i.key === key);
|
|
116
|
+
if (imp) {
|
|
117
|
+
try {
|
|
118
|
+
const content = await fsPromises.readFile(imp.path, "utf-8");
|
|
119
|
+
processedSrc = processedSrc.split(`$[[${key}]]`).join(content);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
runtimeError([`<$red:Module Read Error:$> Failed to read <$magenta:${imp.path}$>`]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ========================================================================== //
|
|
127
|
+
// 5. Remove the [[import ...]] lines //
|
|
128
|
+
// ========================================================================== //
|
|
129
|
+
imports.forEach(imp => {
|
|
130
|
+
processedSrc = processedSrc.replace(imp.fullMatch, "");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return processedSrc;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export default ModuleSystem;
|