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.
Files changed (45) hide show
  1. package/README.md +47 -42
  2. package/SOMMARK-SPEC.md +483 -0
  3. package/cli/cli.mjs +42 -2
  4. package/cli/commands/color.js +36 -0
  5. package/cli/commands/help.js +7 -0
  6. package/cli/commands/init.js +2 -0
  7. package/cli/commands/list.js +119 -0
  8. package/cli/commands/print.js +61 -11
  9. package/cli/commands/show.js +24 -27
  10. package/cli/constants.js +1 -1
  11. package/cli/helpers/config.js +14 -4
  12. package/cli/helpers/transpile.js +27 -32
  13. package/constants/html_props.js +100 -0
  14. package/constants/html_tags.js +146 -0
  15. package/constants/void_elements.js +26 -0
  16. package/core/lexer.js +70 -39
  17. package/core/parser.js +100 -84
  18. package/core/pluginManager.js +139 -0
  19. package/core/plugins/comment-remover.js +47 -0
  20. package/core/plugins/module-system.js +137 -0
  21. package/core/plugins/quote-escaper.js +37 -0
  22. package/core/plugins/raw-content-plugin.js +72 -0
  23. package/core/plugins/rules-validation-plugin.js +197 -0
  24. package/core/plugins/sommark-format.js +211 -0
  25. package/core/transpiler.js +65 -198
  26. package/debug.js +9 -4
  27. package/format.js +23 -0
  28. package/formatter/mark.js +3 -3
  29. package/formatter/tag.js +6 -2
  30. package/grammar.ebnf +5 -5
  31. package/helpers/camelize.js +2 -0
  32. package/helpers/colorize.js +20 -14
  33. package/helpers/kebabize.js +2 -0
  34. package/helpers/utils.js +161 -0
  35. package/index.js +243 -44
  36. package/mappers/languages/html.js +200 -105
  37. package/mappers/languages/json.js +23 -4
  38. package/mappers/languages/markdown.js +88 -67
  39. package/mappers/languages/mdx.js +130 -2
  40. package/mappers/mapper.js +77 -246
  41. package/package.json +7 -5
  42. package/unformatted.smark +90 -0
  43. package/v3-todo.smark +75 -0
  44. package/CHANGELOG.md +0 -119
  45. 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 and numbers"
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}${contextText}${current.value}{N}${pointerPadding}<$yellow:^$>{N}{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 value = current_token(tokens, i).value.trim();
137
- // ========================================================================== //
138
- // consume Value //
139
- // ========================================================================== //
157
+ let val = current_token(tokens, i).value;
158
+ // consume Value
140
159
  i++;
141
- return [value, i];
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
- !current_token(tokens, i) ||
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
- if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.VALUE) {
417
- inlineNode.value += current_token(tokens, i).value;
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
- inlineNode.args.push(v);
482
- if (!Number.isInteger(Number(v))) {
483
- inlineNode.args[v] = v;
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
- if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.TEXT) {
568
- textNode.text += current_token(tokens, i).value;
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 (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.ESCAPE) {
575
- textNode.text += current_token(tokens, i).value.slice(1);
576
- // ========================================================================== //
577
- // consume Text Node //
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).value === "[" && peek(tokens, i, 1) && peek(tokens, i, 1).type !== TOKEN_TYPES.END_KEYWORD) {
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).value === "@_") {
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 (current_token(tokens, i).type !== TOKEN_TYPES.COMMENT && current_token(tokens, i).depth === 0) {
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;