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.
- 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 +7 -17
- package/cli/commands/build.js +26 -6
- package/cli/commands/color.js +22 -26
- package/cli/commands/help.js +10 -10
- package/cli/commands/init.js +20 -31
- package/cli/commands/print.js +18 -16
- 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 +15 -17
- package/core/errors.js +49 -25
- package/core/formats.js +7 -3
- package/core/formatter.js +215 -0
- package/core/helpers/config-loader.js +40 -75
- 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 +238 -154
- 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/dedent.js +19 -0
- 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 +186 -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 -120
- 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/tokenTypes.js
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Token Types in SomMark.
|
|
3
|
+
* These represent the basic lexical atoms identified by the lexer.
|
|
4
|
+
*
|
|
5
|
+
* @constant {Object}
|
|
6
|
+
* @property {string} OPEN_BRACKET - '[' char.
|
|
7
|
+
* @property {string} CLOSE_BRACKET - ']' char.
|
|
8
|
+
* @property {string} END_KEYWORD - 'end' value.
|
|
9
|
+
* @property {string} IDENTIFIER - Block or inline name (e.g. 'Person', 'import', '$use-module').
|
|
10
|
+
* @property {string} EQUAL - '=' char.
|
|
11
|
+
* @property {string} VALUE - Data values. Encapsulates Quoted Strings ("...") and Prefix Layers (js{}, p{}).
|
|
12
|
+
* @property {string} TEXT - Plain unformatted text content.
|
|
13
|
+
* @property {string} THIN_ARROW - '->' sequence.
|
|
14
|
+
* @property {string} OPEN_PAREN - '(' char.
|
|
15
|
+
* @property {string} CLOSE_PAREN - ')' char.
|
|
16
|
+
* @property {string} OPEN_AT - '@_' sequence (At-Block start).
|
|
17
|
+
* @property {string} CLOSE_AT - '_@' sequence (At-Header end).
|
|
18
|
+
* @property {string} COLON - ':' char.
|
|
19
|
+
* @property {string} COMMA - ',' char.
|
|
20
|
+
* @property {string} SEMICOLON - ';' char (At-Block separator).
|
|
21
|
+
* @property {string} COMMENT - '#' comments.
|
|
22
|
+
* @property {string} ESCAPE - '\' char. Used for literalizing structural chars like '\"' or '\['.
|
|
23
|
+
* @property {string} QUOTE - '"' delimiter.
|
|
24
|
+
* @property {string} IMPORT - 'import' keyword.
|
|
25
|
+
* @property {string} USE_MODULE - '$use-module' keyword.
|
|
26
|
+
* @property {string} PREFIX_JS - 'js{}' prefix layer.
|
|
27
|
+
* @property {string} PREFIX_P - 'p{}' placeholder layer.
|
|
28
|
+
* @property {string} EOF - End of File indicator.
|
|
29
|
+
*/
|
|
2
30
|
const TOKEN_TYPES = {
|
|
3
31
|
OPEN_BRACKET: "OPEN_BRACKET",
|
|
4
32
|
CLOSE_BRACKET: "CLOSE_BRACKET",
|
|
5
33
|
END_KEYWORD: "END_KEYWORD",
|
|
34
|
+
IMPORT: "IMPORT",
|
|
35
|
+
USE_MODULE: "USE_MODULE",
|
|
6
36
|
IDENTIFIER: "IDENTIFIER",
|
|
7
37
|
EQUAL: "EQUAL",
|
|
8
38
|
VALUE: "VALUE",
|
|
39
|
+
QUOTE: "QUOTE",
|
|
40
|
+
PREFIX_JS: "PREFIX_JS",
|
|
41
|
+
PREFIX_P: "PREFIX_P",
|
|
9
42
|
TEXT: "TEXT",
|
|
10
43
|
THIN_ARROW: "THIN_ARROW",
|
|
11
44
|
OPEN_PAREN: "OPEN_PAREN",
|
|
@@ -17,6 +50,8 @@ const TOKEN_TYPES = {
|
|
|
17
50
|
SEMICOLON: "SEMICOLON",
|
|
18
51
|
COMMENT: "COMMENT",
|
|
19
52
|
ESCAPE: "ESCAPE",
|
|
53
|
+
KEY: "KEY",
|
|
54
|
+
WHITESPACE: "WHITESPACE",
|
|
20
55
|
EOF: "EOF"
|
|
21
56
|
};
|
|
22
57
|
|
package/core/transpiler.js
CHANGED
|
@@ -1,206 +1,290 @@
|
|
|
1
1
|
import { BLOCK, TEXT, INLINE, ATBLOCK, COMMENT } from "./labels.js";
|
|
2
|
-
import escapeHTML from "../helpers/escapeHTML.js";
|
|
3
2
|
import { transpilerError } from "./errors.js";
|
|
4
|
-
import { textFormat, htmlFormat, markdownFormat, mdxFormat,
|
|
3
|
+
import { textFormat, htmlFormat, markdownFormat, mdxFormat, xmlFormat } from "./formats.js";
|
|
4
|
+
import { matchedValue } from "../helpers/utils.js";
|
|
5
|
+
import { dedentBy } from "../helpers/dedent.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* SomMark Transpiler
|
|
9
|
+
* This engine converts the AST into its final text format (like HTML or Markdown)
|
|
10
|
+
* using rules provided by a mapper.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
const BODY_PLACEHOLDER = `
|
|
13
|
+
const BODY_PLACEHOLDER = `SOMMARKBODYPLACEHOLDER${Math.random().toString(36).slice(2)}SOMMARK`;
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!outputs || !targetId) return undefined;
|
|
19
|
-
return outputs.find(output => {
|
|
20
|
-
if (Array.isArray(output.id)) {
|
|
21
|
-
return output.id.some(id => id === targetId);
|
|
22
|
-
}
|
|
23
|
-
return typeof output.id === "string" && output.id === targetId;
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Recursively collects all plain text from a node and its children
|
|
15
|
+
/**
|
|
16
|
+
* Extracts all plain text from a node and its children.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} node - The node to read.
|
|
19
|
+
* @returns {string} - The extracted text.
|
|
20
|
+
*/
|
|
28
21
|
function getNodeText(node) {
|
|
29
|
-
if (!node
|
|
22
|
+
if (!node?.body && !node?.content) return "";
|
|
23
|
+
if (node.type === ATBLOCK) return node.content || "";
|
|
30
24
|
let text = "";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
if (node.body) {
|
|
26
|
+
for (const child of node.body) {
|
|
27
|
+
if (child.type === TEXT) text += child.text || "";
|
|
28
|
+
else if (child.type === INLINE) text += child.value || "";
|
|
29
|
+
else if (child.type === ATBLOCK) text += child.content || "";
|
|
30
|
+
else if (child.type === BLOCK) text += getNodeText(child);
|
|
31
|
+
}
|
|
36
32
|
}
|
|
37
33
|
return text;
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
// ========================================================================== //
|
|
41
|
-
// Core Rendering Logic //
|
|
42
|
-
// ========================================================================== //
|
|
43
36
|
|
|
44
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Converts a code node into its final format (like HTML).
|
|
39
|
+
*
|
|
40
|
+
* @param {Object|Object[]} ast - The node or list of nodes to convert.
|
|
41
|
+
* @param {number} i - The current position in the list.
|
|
42
|
+
* @param {string} format - The target format (e.g., 'html').
|
|
43
|
+
* @param {Object} mapper_file - The rules for how to convert each node.
|
|
44
|
+
* @returns {Promise<string>} - The final text for this node.
|
|
45
|
+
*/
|
|
45
46
|
async function generateOutput(ast, i, format, mapper_file) {
|
|
46
47
|
const node = Array.isArray(ast) ? ast[i] : ast;
|
|
48
|
+
if (!node) return "";
|
|
49
|
+
|
|
47
50
|
let result = "";
|
|
48
51
|
let context = "";
|
|
52
|
+
let isParentBlock = false;
|
|
53
|
+
|
|
54
|
+
if (node.id === mapper_file?.options?.moduleIdentityToken) {
|
|
55
|
+
const oldFilename = mapper_file.options.filename;
|
|
56
|
+
mapper_file.options.filename = node.args?.filename || oldFilename;
|
|
57
|
+
let bodyOutput = "";
|
|
58
|
+
if (node.body) {
|
|
59
|
+
for (let j = 0; j < node.body.length; j++) {
|
|
60
|
+
bodyOutput += await generateOutput(node.body, j, format, mapper_file);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
mapper_file.options.filename = oldFilename;
|
|
64
|
+
return bodyOutput;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (node.type === TEXT) {
|
|
68
|
+
const text = String(node.text || "");
|
|
69
|
+
return mapper_file ? mapper_file.text(text) : text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (node.type === COMMENT) {
|
|
73
|
+
if (mapper_file?.options?.removeComments) return "";
|
|
74
|
+
const cleanComment = String(node.text || "").replace(/^#/, "").trim();
|
|
75
|
+
return " ".repeat(node.depth) + `${mapper_file?.comment(cleanComment) || ""}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
let target = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
|
|
50
|
-
|
|
51
|
-
if (!target) {
|
|
79
|
+
if (!target && mapper_file) {
|
|
52
80
|
target = mapper_file.getUnknownTag(node);
|
|
53
81
|
}
|
|
54
82
|
|
|
55
83
|
if (target) {
|
|
56
|
-
|
|
57
|
-
// Always use placeholders for blocks to support wrapping //
|
|
58
|
-
// ========================================================================== //
|
|
59
|
-
const isParentBlock = format === mdxFormat && node.body.length > 1;
|
|
60
|
-
const placeholder = isParentBlock ? `\n${BODY_PLACEHOLDER}\n` : BODY_PLACEHOLDER;
|
|
84
|
+
const shouldResolveImmediate = target.options?.resolve === true;
|
|
61
85
|
const textContent = getNodeText(node);
|
|
62
86
|
|
|
63
|
-
|
|
87
|
+
let content = (node.body?.length === 0) ? "" :
|
|
88
|
+
(node.type === ATBLOCK ? dedentBy(node.content || "", node.range?.start?.character || 0).trim() :
|
|
89
|
+
(node.type === INLINE ? (node.value || "") : BODY_PLACEHOLDER));
|
|
90
|
+
|
|
91
|
+
// Apply pipelines to format literal values
|
|
92
|
+
if (node.type === INLINE) {
|
|
93
|
+
content = String(content || "");
|
|
94
|
+
content = mapper_file ? mapper_file.inlineText(content, target.options) : content;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 1. Determine if this is a parent block that needs newline wrapping (Trim-and-Wrap)
|
|
98
|
+
// Priority: Target options > Mapper global options
|
|
99
|
+
const effectiveTrimAndWrap = (target.options?.trimAndWrapBlocks !== undefined)
|
|
100
|
+
? target.options.trimAndWrapBlocks
|
|
101
|
+
: mapper_file?.options?.trimAndWrapBlocks;
|
|
102
|
+
|
|
103
|
+
isParentBlock = !shouldResolveImmediate && effectiveTrimAndWrap &&
|
|
104
|
+
(node.body?.length > 1 || (node.body?.length === 1 && textContent.trim().includes('\n')));
|
|
105
|
+
|
|
106
|
+
if (isParentBlock) {
|
|
107
|
+
content = `\n${BODY_PLACEHOLDER}\n`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (shouldResolveImmediate && node.body) {
|
|
111
|
+
let resolvedBody = "";
|
|
112
|
+
for (let j = 0; j < node.body.length; j++) {
|
|
113
|
+
resolvedBody += await generateOutput(node.body, j, format, mapper_file);
|
|
114
|
+
}
|
|
115
|
+
content = resolvedBody;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
result += await target.render.call(mapper_file, { nodeType: node.type, args: node.args, content, textContent, ast: node });
|
|
64
119
|
if (isParentBlock) result = "\n" + result + "\n";
|
|
65
120
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
121
|
+
if (shouldResolveImmediate) {
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isManualMode = target.options?.handleAst === true;
|
|
126
|
+
|
|
127
|
+
if (!isManualMode && node.body) {
|
|
128
|
+
let prev_body_node = null;
|
|
129
|
+
for (let j = 0; j < node.body.length; j++) {
|
|
130
|
+
const body_node = node.body[j];
|
|
131
|
+
let bodyOutput = "";
|
|
132
|
+
|
|
133
|
+
switch (body_node.type) {
|
|
134
|
+
case TEXT:
|
|
135
|
+
const text = String(body_node.text || "");
|
|
136
|
+
// Dedent text relative to the parent block's indentation
|
|
137
|
+
const localDedentedText = dedentBy(text, node.range?.start?.character || 0);
|
|
138
|
+
bodyOutput = mapper_file ? mapper_file.text(localDedentedText, target?.options) : localDedentedText;
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case INLINE:
|
|
142
|
+
let inlineTarget = matchedValue(mapper_file.outputs, body_node.id);
|
|
143
|
+
if (!inlineTarget) {
|
|
144
|
+
inlineTarget = mapper_file.getUnknownTag(body_node);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (inlineTarget) {
|
|
148
|
+
let inlineValue = String(body_node.value || "").trim();
|
|
149
|
+
if (mapper_file) inlineValue = mapper_file.inlineText(inlineValue, inlineTarget.options);
|
|
150
|
+
|
|
151
|
+
const hasArgs = body_node.args && typeof body_node.args === "object" && Object.keys(body_node.args).length > 0;
|
|
152
|
+
bodyOutput = await inlineTarget.render.call(mapper_file, {
|
|
153
|
+
nodeType: body_node.type,
|
|
154
|
+
args: hasArgs ? body_node.args : {},
|
|
155
|
+
content: inlineValue,
|
|
89
156
|
ast: body_node
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (shouldEscapeAt && (format === htmlFormat)) {
|
|
103
|
-
content = escapeHTML(content);
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
let fallback = body_node.value || "";
|
|
160
|
+
if (mapper_file) fallback = mapper_file.inlineText(fallback, {});
|
|
161
|
+
bodyOutput = fallback;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case ATBLOCK:
|
|
166
|
+
let atTarget = matchedValue(mapper_file.outputs, body_node.id);
|
|
167
|
+
if (!atTarget) {
|
|
168
|
+
atTarget = mapper_file.getUnknownTag(body_node);
|
|
104
169
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
170
|
+
|
|
171
|
+
// AtBlocks handle their own absolute dedenting
|
|
172
|
+
let atContent = dedentBy(body_node.content || "", body_node.range?.start?.character || 0).trim();
|
|
173
|
+
if (mapper_file) {
|
|
174
|
+
atContent = mapper_file.atBlockBody(atContent, atTarget?.options || {});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Removed multiline injection since atBlockBody handles formatting
|
|
178
|
+
bodyOutput = atTarget
|
|
179
|
+
? await atTarget.render.call(mapper_file, { nodeType: body_node.type, args: body_node.args, content: atContent, ast: body_node })
|
|
180
|
+
: atContent;
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case COMMENT:
|
|
184
|
+
if (mapper_file?.options?.removeComments) break;
|
|
185
|
+
const cleanComment = String(body_node.text || "").replace(/^#/, "").trim();
|
|
186
|
+
bodyOutput = " ".repeat(body_node.depth) + `${mapper_file.comment(cleanComment)}`;
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case BLOCK:
|
|
190
|
+
bodyOutput = await generateOutput(body_node, 0, format, mapper_file);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (bodyOutput) {
|
|
195
|
+
context += bodyOutput;
|
|
196
|
+
}
|
|
124
197
|
}
|
|
125
198
|
}
|
|
126
199
|
|
|
127
|
-
|
|
200
|
+
const finalContext = effectiveTrimAndWrap ? context.replace(/^\s*[\r\n]+|[\r\n]+\s*$/g, "") : context;
|
|
201
|
+
|
|
202
|
+
if (result.includes(BODY_PLACEHOLDER)) {
|
|
203
|
+
result = result.replaceAll(BODY_PLACEHOLDER, finalContext);
|
|
204
|
+
} else if (finalContext.trim()) {
|
|
205
|
+
result += finalContext;
|
|
206
|
+
}
|
|
128
207
|
}
|
|
129
208
|
else if (format === textFormat) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
break;
|
|
209
|
+
if (node.type === ATBLOCK) {
|
|
210
|
+
result = node.content || "";
|
|
211
|
+
} else if (node.type === INLINE) {
|
|
212
|
+
result = node.value || "";
|
|
213
|
+
} else if (node.body) {
|
|
214
|
+
for (const body_node of node.body) {
|
|
215
|
+
switch (body_node.type) {
|
|
216
|
+
case TEXT: context += body_node.text || ""; break;
|
|
217
|
+
case INLINE: context += (body_node.value || "") + " "; break;
|
|
218
|
+
case ATBLOCK: context += (body_node.content || "").trimEnd() + "\n"; break;
|
|
219
|
+
case BLOCK:
|
|
220
|
+
const textBlockOutput = await generateOutput(body_node, 0, format, mapper_file);
|
|
221
|
+
context = context.trim() ? context.trimEnd() + "\n" + textBlockOutput : context + textBlockOutput;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
139
224
|
}
|
|
225
|
+
result += context;
|
|
140
226
|
}
|
|
141
|
-
result += context;
|
|
142
227
|
} else {
|
|
143
228
|
transpilerError([
|
|
144
229
|
"{line}<$red:Invalid Identifier:$> ",
|
|
145
230
|
`<$yellow:Identifier$> <$blue:'${node.id}'$> <$yellow: is not found in mapping outputs$>{line}`
|
|
146
231
|
]);
|
|
147
232
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return finalResult + newline;
|
|
233
|
+
|
|
234
|
+
return result;
|
|
151
235
|
}
|
|
152
236
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
237
|
+
/**
|
|
238
|
+
* The main entry point for the SomMark Transpiler.
|
|
239
|
+
* It takes an AST and turns it into the final formatted output.
|
|
240
|
+
*
|
|
241
|
+
* @param {Object|Object[]} optionsOrAst - Either the full options object or just the AST.
|
|
242
|
+
* @param {string} [format] - The target format.
|
|
243
|
+
* @param {Object} [mapperFile] - The mapper rules to use.
|
|
244
|
+
* @returns {Promise<string>} - The final formatted document.
|
|
245
|
+
*/
|
|
246
|
+
export async function transpiler(optionsOrAst, format, mapperFile) {
|
|
247
|
+
let body = null;
|
|
248
|
+
let targetFormat = format;
|
|
249
|
+
let targetMapper = mapperFile;
|
|
250
|
+
|
|
251
|
+
if (typeof optionsOrAst === "object" && !Array.isArray(optionsOrAst) && (optionsOrAst.ast || Array.isArray(optionsOrAst))) {
|
|
252
|
+
if (optionsOrAst.ast) {
|
|
253
|
+
const root = optionsOrAst.ast;
|
|
254
|
+
body = Array.isArray(root) ? root : (root.body || [root]);
|
|
255
|
+
targetFormat = optionsOrAst.format;
|
|
256
|
+
targetMapper = optionsOrAst.mapperFile;
|
|
257
|
+
} else if (Array.isArray(optionsOrAst)) {
|
|
258
|
+
body = optionsOrAst;
|
|
259
|
+
}
|
|
260
|
+
} else if (Array.isArray(optionsOrAst)) {
|
|
261
|
+
body = optionsOrAst;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!body || !Array.isArray(body)) return "";
|
|
156
265
|
|
|
157
|
-
async function transpiler({ ast, format, mapperFile, includeDocument = true }) {
|
|
158
266
|
let output = "";
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const shouldEscapeInline = inlineTarget.options?.escape !== false;
|
|
177
|
-
output += inlineTarget.render.call(mapperFile, {
|
|
178
|
-
args: node.args.length > 0 ? node.args : [],
|
|
179
|
-
content: (format === htmlFormat) && shouldEscapeInline ? escapeHTML(node.value) : node.value,
|
|
180
|
-
ast: node
|
|
181
|
-
}) + (format === mdxFormat ? "\n" : "");
|
|
182
|
-
}
|
|
183
|
-
break;
|
|
184
|
-
case ATBLOCK:
|
|
185
|
-
let atTarget = matchedValue(mapperFile.outputs, node.id);
|
|
186
|
-
if (!atTarget) atTarget = mapperFile.getUnknownTag(node);
|
|
187
|
-
if (atTarget) {
|
|
188
|
-
const shouldEscapeAt = atTarget.options?.escape !== false;
|
|
189
|
-
let content = node.content;
|
|
190
|
-
if (shouldEscapeAt && (format === htmlFormat)) {
|
|
191
|
-
content = escapeHTML(content);
|
|
192
|
-
}
|
|
193
|
-
const rendered = atTarget.render.call(mapperFile, { args: node.args, content, ast: node });
|
|
194
|
-
const finalAtRendered = (format === mdxFormat) ? rendered : rendered.trimEnd() + "\n";
|
|
195
|
-
output = output.trim() ? output.trimEnd() + "\n" + finalAtRendered : output + finalAtRendered;
|
|
196
|
-
}
|
|
197
|
-
break;
|
|
267
|
+
let prev_body_node = null;
|
|
268
|
+
for (let i = 0; i < body.length; i++) {
|
|
269
|
+
const node = body[i];
|
|
270
|
+
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper);
|
|
271
|
+
|
|
272
|
+
if (blockOutput) {
|
|
273
|
+
output += blockOutput;
|
|
274
|
+
if (node.type !== TEXT || node.text.trim().length > 0) {
|
|
275
|
+
prev_body_node = node;
|
|
276
|
+
}
|
|
277
|
+
} else if (node.type === COMMENT && targetMapper?.options?.removeComments) {
|
|
278
|
+
// If a comment is removed, check the next node.
|
|
279
|
+
// If it's just a blank line, skip it so we don't have extra gaps in the output.
|
|
280
|
+
const nextNode = body[i + 1];
|
|
281
|
+
if (nextNode && nextNode.type === TEXT && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
|
|
282
|
+
i++; // Skip the next newline node
|
|
283
|
+
}
|
|
198
284
|
}
|
|
199
285
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// ========================================================================== //
|
|
203
|
-
return mapperFile.formatOutput(output, includeDocument);
|
|
286
|
+
|
|
287
|
+
return output.trim();
|
|
204
288
|
}
|
|
205
289
|
|
|
206
290
|
export default transpiler;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { transpilerError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SomMark Rules Validator
|
|
5
|
+
*
|
|
6
|
+
* This module ensures the AST matches the expected structure.
|
|
7
|
+
* It focuses on structural integrity (like self-closing tags)
|
|
8
|
+
* while leaving specific patterns to the mapper's discretion.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Runs all validation rules against a single code node.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} node - The code node to check.
|
|
15
|
+
* @param {Object} target - The rule definition for this node.
|
|
16
|
+
* @param {Object} instance - The current SomMark context.
|
|
17
|
+
*/
|
|
18
|
+
const runValidations = (node, target, instance) => {
|
|
19
|
+
if (!target || !target.options) return;
|
|
20
|
+
const rules = target.options.rules || {};
|
|
21
|
+
const id = (target.id) ? (Array.isArray(target.id) ? target.id.join(" | ") : target.id) : "Unknown";
|
|
22
|
+
const context = instance ? { src: instance.src, range: node.range, filename: instance.filename } : null;
|
|
23
|
+
|
|
24
|
+
// -- Structural Integrity (Self-Closing) ------------------------------ //
|
|
25
|
+
if (rules.is_self_closing && (node.type === "Block" && (node.body && node.body.length > 0))) {
|
|
26
|
+
transpilerError(
|
|
27
|
+
[
|
|
28
|
+
"<$red:Validation Error:$> ",
|
|
29
|
+
`<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is self-closing and cannot have children (body).$>`
|
|
30
|
+
],
|
|
31
|
+
context
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Checks every node in the entire document for structural issues.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object|Array} ast - The Abstract Syntax Tree to validate.
|
|
40
|
+
* @param {Object} mapperFile - The mapper instance containing tag registrations.
|
|
41
|
+
* @param {Object} instance - The current SomMark context.
|
|
42
|
+
* @returns {Object|Array} - The validated AST.
|
|
43
|
+
*/
|
|
44
|
+
export function validateAST(ast, mapperFile, instance) {
|
|
45
|
+
if (!mapperFile) return ast;
|
|
46
|
+
|
|
47
|
+
const validateNode = (node) => {
|
|
48
|
+
if (!node) return;
|
|
49
|
+
|
|
50
|
+
// Handle filename context updates for module identity tokens
|
|
51
|
+
if (instance?.moduleIdentityToken && node.id === instance.moduleIdentityToken) {
|
|
52
|
+
const oldFilename = instance.filename;
|
|
53
|
+
instance.filename = node.args?.filename || oldFilename;
|
|
54
|
+
if (node.body) {
|
|
55
|
+
node.body.forEach(child => validateNode(child));
|
|
56
|
+
}
|
|
57
|
+
instance.filename = oldFilename;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 1. Identify Target
|
|
62
|
+
if (node.id) {
|
|
63
|
+
const target = mapperFile.get(node.id) || (mapperFile.getUnknownTag ? mapperFile.getUnknownTag(node) : null);
|
|
64
|
+
if (target) {
|
|
65
|
+
runValidations(node, target, instance);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. Recursive Traversal
|
|
70
|
+
if (node.body && Array.isArray(node.body)) {
|
|
71
|
+
node.body.forEach(child => validateNode(child));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const rootNodes = Array.isArray(ast) ? ast : [ast];
|
|
76
|
+
rootNodes.forEach(node => validateNode(node));
|
|
77
|
+
|
|
78
|
+
return ast;
|
|
79
|
+
}
|