sommark 3.3.3 → 4.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 (61) 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 +8 -16
  6. package/cli/commands/build.js +24 -4
  7. package/cli/commands/color.js +22 -26
  8. package/cli/commands/help.js +10 -10
  9. package/cli/commands/init.js +19 -42
  10. package/cli/commands/print.js +20 -12
  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 +7 -8
  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 +37 -56
  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 +237 -151
  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/escapeHTML.js +13 -6
  33. package/helpers/kebabize.js +6 -0
  34. package/helpers/peek.js +9 -0
  35. package/helpers/removeChar.js +26 -13
  36. package/helpers/safeDataParser.js +114 -0
  37. package/helpers/utils.js +140 -158
  38. package/index.js +198 -188
  39. package/mappers/languages/html.js +105 -213
  40. package/mappers/languages/json.js +122 -171
  41. package/mappers/languages/markdown.js +355 -108
  42. package/mappers/languages/mdx.js +76 -114
  43. package/mappers/languages/xml.js +114 -0
  44. package/mappers/mapper.js +152 -123
  45. package/mappers/shared/index.js +22 -0
  46. package/package.json +26 -6
  47. package/SOMMARK-SPEC.md +0 -481
  48. package/cli/commands/list.js +0 -124
  49. package/constants/html_tags.js +0 -146
  50. package/core/pluginManager.js +0 -149
  51. package/core/plugins/comment-remover.js +0 -47
  52. package/core/plugins/module-system.js +0 -176
  53. package/core/plugins/raw-content-plugin.js +0 -78
  54. package/core/plugins/rules-validation-plugin.js +0 -231
  55. package/core/plugins/sommark-format.js +0 -244
  56. package/coverage_test.js +0 -21
  57. package/debug.js +0 -15
  58. package/helpers/camelize.js +0 -2
  59. package/helpers/defaultTheme.js +0 -3
  60. package/test_format_fix.js +0 -42
  61. 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
+ }