sommark 3.3.4 → 4.0.1

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 (62) hide show
  1. package/README.md +98 -82
  2. package/assets/logo.json +28 -0
  3. package/assets/smark.logo.png +0 -0
  4. package/assets/smark.logo.svg +21 -0
  5. package/cli/cli.mjs +7 -17
  6. package/cli/commands/build.js +26 -6
  7. package/cli/commands/color.js +22 -26
  8. package/cli/commands/help.js +10 -10
  9. package/cli/commands/init.js +20 -31
  10. package/cli/commands/print.js +18 -16
  11. package/cli/commands/show.js +4 -0
  12. package/cli/commands/version.js +6 -0
  13. package/cli/constants.js +9 -5
  14. package/cli/helpers/config.js +11 -0
  15. package/cli/helpers/file.js +17 -6
  16. package/cli/helpers/transpile.js +15 -17
  17. package/core/errors.js +49 -25
  18. package/core/formats.js +7 -3
  19. package/core/formatter.js +215 -0
  20. package/core/helpers/config-loader.js +40 -75
  21. package/core/labels.js +21 -9
  22. package/core/lexer.js +491 -212
  23. package/core/modules.js +164 -0
  24. package/core/parser.js +516 -389
  25. package/core/tokenTypes.js +36 -1
  26. package/core/transpiler.js +238 -154
  27. package/core/validator.js +79 -0
  28. package/formatter/mark.js +203 -43
  29. package/formatter/tag.js +202 -32
  30. package/grammar.ebnf +57 -50
  31. package/helpers/colorize.js +26 -13
  32. package/helpers/dedent.js +19 -0
  33. package/helpers/escapeHTML.js +13 -6
  34. package/helpers/kebabize.js +6 -0
  35. package/helpers/peek.js +9 -0
  36. package/helpers/removeChar.js +26 -13
  37. package/helpers/safeDataParser.js +114 -0
  38. package/helpers/utils.js +140 -158
  39. package/index.js +186 -188
  40. package/mappers/languages/html.js +105 -213
  41. package/mappers/languages/json.js +122 -171
  42. package/mappers/languages/markdown.js +355 -108
  43. package/mappers/languages/mdx.js +76 -120
  44. package/mappers/languages/xml.js +114 -0
  45. package/mappers/mapper.js +152 -123
  46. package/mappers/shared/index.js +22 -0
  47. package/package.json +26 -6
  48. package/SOMMARK-SPEC.md +0 -481
  49. package/cli/commands/list.js +0 -124
  50. package/constants/html_tags.js +0 -146
  51. package/core/pluginManager.js +0 -149
  52. package/core/plugins/comment-remover.js +0 -47
  53. package/core/plugins/module-system.js +0 -176
  54. package/core/plugins/raw-content-plugin.js +0 -78
  55. package/core/plugins/rules-validation-plugin.js +0 -231
  56. package/core/plugins/sommark-format.js +0 -244
  57. package/coverage_test.js +0 -21
  58. package/debug.js +0 -15
  59. package/helpers/camelize.js +0 -2
  60. package/helpers/defaultTheme.js +0 -3
  61. package/test_format_fix.js +0 -42
  62. package/v3-todo.smark +0 -73
@@ -0,0 +1,164 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { runtimeError } from "./errors.js";
5
+ import { IMPORT, USE_MODULE, TEXT, COMMENT } from "./labels.js";
6
+
7
+ /**
8
+ * Changes a filename or file URL into a full, absolute file path.
9
+ *
10
+ * @param {string} filename - The name of the file or its URL.
11
+ * @returns {string} - The corrected absolute path.
12
+ */
13
+ const normalizePath = (filename) => {
14
+ if (!filename || filename === "anonymous") return process.cwd();
15
+ if (filename.startsWith("file://")) {
16
+ try {
17
+ return fileURLToPath(filename);
18
+ } catch (e) {
19
+ return filename;
20
+ }
21
+ }
22
+ return path.resolve(process.cwd(), filename);
23
+ };
24
+
25
+ /**
26
+ * Handles all [import] and [$use-module] blocks in your code.
27
+ * It loads the requested files, checks for errors, and puts the content into the main document.
28
+ *
29
+ * @param {Object[]} ast - The list of code parts to check.
30
+ * @param {Object} context - Settings like the filename and current format.
31
+ * @returns {Promise<Object[]>} - The final list of code parts with modules loaded.
32
+ */
33
+ export async function resolveModules(ast, context) {
34
+ const modules = new Map();
35
+ const filename = context.filename || "anonymous";
36
+ const absFilename = normalizePath(filename);
37
+ const baseDir = filename === "anonymous" ? absFilename : path.dirname(absFilename);
38
+
39
+ let hasContentStarted = false;
40
+
41
+ const processNodes = async (nodes, currentBaseDir, isTopLevel = false) => {
42
+ for (let i = 0; i < nodes.length; i++) {
43
+ const node = nodes[i];
44
+
45
+ // 1. Handle Import Node: [import = alias: "path"]
46
+ if (node.type === IMPORT) {
47
+ if (hasContentStarted || !isTopLevel) {
48
+ runtimeError([`<$red:Module Placement Error:$> Imports must be declared at the top level before any content at line <$yellow:${node.range.start.line + 1}$>`]);
49
+ }
50
+
51
+ const alias = Object.keys(node.args).find(k => isNaN(k));
52
+ let filePath = alias ? node.args[alias] : node.args[0];
53
+
54
+ if (typeof filePath === "string") {
55
+ filePath = filePath.trim().replace(/^["']|["']$/g, "");
56
+ }
57
+
58
+ if (!filePath) {
59
+ runtimeError([`<$red:Module Path Error:$> Missing file path for alias <$magenta:${alias}$> at line <$yellow:${node.range.start.line + 1}$>`]);
60
+ }
61
+
62
+ const absolutePath = path.resolve(currentBaseDir, filePath);
63
+
64
+ if (!fs.existsSync(absolutePath)) {
65
+ runtimeError([`<$red:Module Path Error:$> File not found: <$magenta:${filePath}$> at line <$yellow:${node.range.start.line + 1}$>`]);
66
+ }
67
+
68
+ const ext = path.extname(absolutePath).slice(1);
69
+ if (ext !== "smark") {
70
+ runtimeError([`<$red:Module Extension Error:$> Unsupported extension .${ext} for module <$magenta:${alias}$>. Only .smark files are supported.`]);
71
+ }
72
+
73
+ modules.set(alias, { path: absolutePath, type: ext, used: false, range: node.range });
74
+
75
+ // Remove import node from AST
76
+ nodes.splice(i, 1);
77
+ i--;
78
+ }
79
+ // 2. Handle Usage Node: [$use-module = alias]
80
+ else if (node.type === USE_MODULE) {
81
+ hasContentStarted = true;
82
+ const alias = node.args[0] || Object.values(node.args)[0];
83
+ if (!alias || !modules.has(alias)) {
84
+ runtimeError([`<$red:Module Usage Error:$> Undefined module alias <$magenta:${alias}$> at line <$yellow:${node.range.start.line + 1}$>`]);
85
+ }
86
+
87
+ const mod = modules.get(alias);
88
+ mod.used = true;
89
+
90
+ if (mod.type === "smark") {
91
+ const stack = context.importStack || [];
92
+ if (stack.includes(mod.path)) {
93
+ const chain = [...stack, mod.path].map(p => path.basename(p)).join(" -> ");
94
+ runtimeError([
95
+ `{line}<$red:Circular Dependency Detected$>:`,
96
+ `<$yellow:The following import chain was found:$>`,
97
+ `<$magenta:${chain}$>{line}`
98
+ ]);
99
+ }
100
+
101
+ // Recursive Parse for Smark files
102
+ const content = fs.readFileSync(mod.path, "utf-8");
103
+ const SomMark = context.instance.constructor;
104
+
105
+ const subSmark = new SomMark({
106
+ src: content,
107
+ format: context.format,
108
+ filename: mod.path,
109
+ mapperFile: context.instance.mapperFile,
110
+ removeComments: context.instance.removeComments,
111
+ placeholders: context.instance.placeholders,
112
+ importStack: [...stack, absFilename]
113
+ });
114
+
115
+ const subAst = await subSmark.parse();
116
+
117
+ // Wrap the imported code in a virtual block to keep its identity.
118
+ const wrapperNode = {
119
+ type: "Block",
120
+ id: context.instance.moduleIdentityToken,
121
+ args: { filename: mod.path },
122
+ body: subAst,
123
+ depth: node.depth,
124
+ range: node.range
125
+ };
126
+
127
+ // Splice the wrapper into the current body
128
+ nodes.splice(i, 1, wrapperNode);
129
+ i += 0; // The wrapper is a single node now
130
+ } else {
131
+ runtimeError([`<$red:Module Extension Error:$> Unsupported extension .${mod.type} for module <$magenta:${alias}$>. Only .smark files are supported.`]);
132
+ }
133
+ }
134
+ // 3. Recurse into children
135
+ else {
136
+ if (node.type === TEXT && node.text.trim() === "") {
137
+ // Ignore structural whitespace between imports
138
+ } else if (node.type !== COMMENT) {
139
+ // Any meaningful node that isn't an IMPORT or COMMENT is considered "Content"
140
+ hasContentStarted = true;
141
+ }
142
+
143
+ if (node.body && Array.isArray(node.body)) {
144
+ await processNodes(node.body, currentBaseDir, false);
145
+ }
146
+ }
147
+ }
148
+ };
149
+
150
+ await processNodes(ast, baseDir, true);
151
+
152
+ // 4. Report Unused Imports
153
+ for (const [alias, mod] of modules) {
154
+ if (!mod.used) {
155
+ context.instance.warnings.push({
156
+ type: "UnusedModule",
157
+ message: `Module alias <$magenta:${alias}$> is imported but never used.`,
158
+ range: mod.range
159
+ });
160
+ }
161
+ }
162
+
163
+ return ast;
164
+ }