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.
- package/README.md +98 -82
- package/assets/logo.json +28 -0
- package/assets/smark.logo.png +0 -0
- package/assets/smark.logo.svg +21 -0
- package/cli/cli.mjs +8 -16
- package/cli/commands/build.js +24 -4
- package/cli/commands/color.js +22 -26
- package/cli/commands/help.js +10 -10
- package/cli/commands/init.js +19 -42
- package/cli/commands/print.js +20 -12
- package/cli/commands/show.js +4 -0
- package/cli/commands/version.js +6 -0
- package/cli/constants.js +9 -5
- package/cli/helpers/config.js +11 -0
- package/cli/helpers/file.js +17 -6
- package/cli/helpers/transpile.js +7 -8
- package/core/errors.js +49 -25
- package/core/formats.js +7 -3
- package/core/formatter.js +215 -0
- package/core/helpers/config-loader.js +37 -56
- package/core/labels.js +21 -9
- package/core/lexer.js +491 -212
- package/core/modules.js +164 -0
- package/core/parser.js +516 -389
- package/core/tokenTypes.js +36 -1
- package/core/transpiler.js +237 -151
- package/core/validator.js +79 -0
- package/formatter/mark.js +203 -43
- package/formatter/tag.js +202 -32
- package/grammar.ebnf +57 -50
- package/helpers/colorize.js +26 -13
- package/helpers/escapeHTML.js +13 -6
- package/helpers/kebabize.js +6 -0
- package/helpers/peek.js +9 -0
- package/helpers/removeChar.js +26 -13
- package/helpers/safeDataParser.js +114 -0
- package/helpers/utils.js +140 -158
- package/index.js +198 -188
- package/mappers/languages/html.js +105 -213
- package/mappers/languages/json.js +122 -171
- package/mappers/languages/markdown.js +355 -108
- package/mappers/languages/mdx.js +76 -114
- package/mappers/languages/xml.js +114 -0
- package/mappers/mapper.js +152 -123
- package/mappers/shared/index.js +22 -0
- package/package.json +26 -6
- package/SOMMARK-SPEC.md +0 -481
- package/cli/commands/list.js +0 -124
- package/constants/html_tags.js +0 -146
- package/core/pluginManager.js +0 -149
- package/core/plugins/comment-remover.js +0 -47
- package/core/plugins/module-system.js +0 -176
- package/core/plugins/raw-content-plugin.js +0 -78
- package/core/plugins/rules-validation-plugin.js +0 -231
- package/core/plugins/sommark-format.js +0 -244
- package/coverage_test.js +0 -21
- package/debug.js +0 -15
- package/helpers/camelize.js +0 -2
- package/helpers/defaultTheme.js +0 -3
- package/test_format_fix.js +0 -42
- package/v3-todo.smark +0 -73
package/core/modules.js
ADDED
|
@@ -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
|
+
}
|