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/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,203 +1,289 @@
|
|
|
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
5
|
|
|
6
6
|
/**
|
|
7
7
|
* SomMark Transpiler
|
|
8
|
+
* This engine converts the AST into its final text format (like HTML or Markdown)
|
|
9
|
+
* using rules provided by a mapper.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
|
-
const BODY_PLACEHOLDER = `
|
|
12
|
+
const BODY_PLACEHOLDER = `SOMMARKBODYPLACEHOLDER${Math.random().toString(36).slice(2)}SOMMARK`;
|
|
11
13
|
|
|
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
|
|
14
|
+
/**
|
|
15
|
+
* Extracts all plain text from a node and its children.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} node - The node to read.
|
|
18
|
+
* @returns {string} - The extracted text.
|
|
19
|
+
*/
|
|
28
20
|
function getNodeText(node) {
|
|
29
|
-
if (!node
|
|
21
|
+
if (!node?.body && !node?.content) return "";
|
|
22
|
+
if (node.type === ATBLOCK) return node.content || "";
|
|
30
23
|
let text = "";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
if (node.body) {
|
|
25
|
+
for (const child of node.body) {
|
|
26
|
+
if (child.type === TEXT) text += child.text || "";
|
|
27
|
+
else if (child.type === INLINE) text += child.value || "";
|
|
28
|
+
else if (child.type === ATBLOCK) text += child.content || "";
|
|
29
|
+
else if (child.type === BLOCK) text += getNodeText(child);
|
|
30
|
+
}
|
|
36
31
|
}
|
|
37
32
|
return text;
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
// ========================================================================== //
|
|
41
|
-
// Core Rendering Logic //
|
|
42
|
-
// ========================================================================== //
|
|
43
35
|
|
|
44
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Converts a code node into its final format (like HTML).
|
|
38
|
+
*
|
|
39
|
+
* @param {Object|Object[]} ast - The node or list of nodes to convert.
|
|
40
|
+
* @param {number} i - The current position in the list.
|
|
41
|
+
* @param {string} format - The target format (e.g., 'html').
|
|
42
|
+
* @param {Object} mapper_file - The rules for how to convert each node.
|
|
43
|
+
* @returns {Promise<string>} - The final text for this node.
|
|
44
|
+
*/
|
|
45
45
|
async function generateOutput(ast, i, format, mapper_file) {
|
|
46
46
|
const node = Array.isArray(ast) ? ast[i] : ast;
|
|
47
|
+
if (!node) return "";
|
|
48
|
+
|
|
47
49
|
let result = "";
|
|
48
50
|
let context = "";
|
|
51
|
+
let isParentBlock = false;
|
|
52
|
+
|
|
53
|
+
if (node.id === mapper_file?.options?.moduleIdentityToken) {
|
|
54
|
+
const oldFilename = mapper_file.options.filename;
|
|
55
|
+
mapper_file.options.filename = node.args?.filename || oldFilename;
|
|
56
|
+
let bodyOutput = "";
|
|
57
|
+
if (node.body) {
|
|
58
|
+
for (let j = 0; j < node.body.length; j++) {
|
|
59
|
+
bodyOutput += await generateOutput(node.body, j, format, mapper_file);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
mapper_file.options.filename = oldFilename;
|
|
63
|
+
return bodyOutput;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (node.type === TEXT) {
|
|
67
|
+
const text = String(node.text || "");
|
|
68
|
+
return mapper_file ? mapper_file.text(text) : text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (node.type === COMMENT) {
|
|
72
|
+
if (mapper_file?.options?.removeComments) return "";
|
|
73
|
+
const cleanComment = String(node.text || "").replace(/^#/, "").trim();
|
|
74
|
+
return " ".repeat(node.depth) + `${mapper_file?.comment(cleanComment) || ""}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
let target = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
|
|
50
|
-
|
|
51
|
-
if (!target) {
|
|
78
|
+
if (!target && mapper_file) {
|
|
52
79
|
target = mapper_file.getUnknownTag(node);
|
|
53
80
|
}
|
|
54
81
|
|
|
55
82
|
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;
|
|
83
|
+
const shouldResolveImmediate = target.options?.resolve === true;
|
|
61
84
|
const textContent = getNodeText(node);
|
|
62
85
|
|
|
63
|
-
|
|
86
|
+
let content = (node.body?.length === 0) ? "" :
|
|
87
|
+
(node.type === ATBLOCK ? (node.content || "") :
|
|
88
|
+
(node.type === INLINE ? (node.value || "") : BODY_PLACEHOLDER));
|
|
89
|
+
|
|
90
|
+
// Apply pipelines to format literal values
|
|
91
|
+
if (node.type === INLINE) {
|
|
92
|
+
content = String(content || "");
|
|
93
|
+
content = mapper_file ? mapper_file.inlineText(content, target.options) : content;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 1. Determine if this is a parent block that needs newline wrapping (Trim-and-Wrap)
|
|
97
|
+
// Priority: Target options > Mapper global options
|
|
98
|
+
const effectiveTrimAndWrap = (target.options?.trimAndWrapBlocks !== undefined)
|
|
99
|
+
? target.options.trimAndWrapBlocks
|
|
100
|
+
: mapper_file?.options?.trimAndWrapBlocks;
|
|
101
|
+
|
|
102
|
+
isParentBlock = !shouldResolveImmediate && effectiveTrimAndWrap &&
|
|
103
|
+
(node.body?.length > 1 || (node.body?.length === 1 && textContent.trim().includes('\n')));
|
|
104
|
+
|
|
105
|
+
if (isParentBlock) {
|
|
106
|
+
content = `\n${BODY_PLACEHOLDER}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (shouldResolveImmediate && node.body) {
|
|
110
|
+
let resolvedBody = "";
|
|
111
|
+
for (let j = 0; j < node.body.length; j++) {
|
|
112
|
+
resolvedBody += await generateOutput(node.body, j, format, mapper_file);
|
|
113
|
+
}
|
|
114
|
+
content = resolvedBody;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
result += await target.render.call(mapper_file, { nodeType: node.type, args: node.args, content, textContent, ast: node });
|
|
64
118
|
if (isParentBlock) result = "\n" + result + "\n";
|
|
65
119
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
if (shouldResolveImmediate) {
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const isManualMode = target.options?.handleAst === true;
|
|
125
|
+
|
|
126
|
+
if (!isManualMode && node.body) {
|
|
127
|
+
let prev_body_node = null;
|
|
128
|
+
for (let j = 0; j < node.body.length; j++) {
|
|
129
|
+
const body_node = node.body[j];
|
|
130
|
+
let bodyOutput = "";
|
|
131
|
+
|
|
132
|
+
switch (body_node.type) {
|
|
133
|
+
case TEXT:
|
|
134
|
+
const text = String(body_node.text || "");
|
|
135
|
+
bodyOutput = mapper_file ? mapper_file.text(text, target?.options) : text;
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case INLINE:
|
|
139
|
+
let inlineTarget = matchedValue(mapper_file.outputs, body_node.id);
|
|
140
|
+
if (!inlineTarget) {
|
|
141
|
+
inlineTarget = mapper_file.getUnknownTag(body_node);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (inlineTarget) {
|
|
145
|
+
let inlineValue = String(body_node.value || "").trim();
|
|
146
|
+
if (mapper_file) inlineValue = mapper_file.inlineText(inlineValue, inlineTarget.options);
|
|
147
|
+
|
|
148
|
+
const hasArgs = body_node.args && typeof body_node.args === "object" && Object.keys(body_node.args).length > 0;
|
|
149
|
+
bodyOutput = await inlineTarget.render.call(mapper_file, {
|
|
150
|
+
nodeType: body_node.type,
|
|
151
|
+
args: hasArgs ? body_node.args : {},
|
|
152
|
+
content: inlineValue,
|
|
89
153
|
ast: body_node
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
content = escapeHTML(content);
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
let fallback = body_node.value || "";
|
|
157
|
+
if (mapper_file) fallback = mapper_file.inlineText(fallback, {});
|
|
158
|
+
bodyOutput = fallback;
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case ATBLOCK:
|
|
163
|
+
console.log(`[TRANSPILER] Processing ATBLOCK: ${body_node.id}`);
|
|
164
|
+
let atTarget = matchedValue(mapper_file.outputs, body_node.id);
|
|
165
|
+
if (!atTarget) {
|
|
166
|
+
atTarget = mapper_file.getUnknownTag(body_node);
|
|
104
167
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
168
|
+
|
|
169
|
+
let atContent = String(body_node.content || "").trim();
|
|
170
|
+
if (mapper_file) {
|
|
171
|
+
console.log(`[TRANSPILER] Calling atBlockBody for ${body_node.id}`);
|
|
172
|
+
atContent = mapper_file.atBlockBody(atContent, atTarget?.options || {});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Removed multiline injection since atBlockBody handles formatting
|
|
176
|
+
bodyOutput = atTarget
|
|
177
|
+
? await atTarget.render.call(mapper_file, { nodeType: body_node.type, args: body_node.args, content: atContent, ast: body_node })
|
|
178
|
+
: atContent;
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case COMMENT:
|
|
182
|
+
if (mapper_file?.options?.removeComments) break;
|
|
183
|
+
const cleanComment = String(body_node.text || "").replace(/^#/, "").trim();
|
|
184
|
+
bodyOutput = " ".repeat(body_node.depth) + `${mapper_file.comment(cleanComment)}`;
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case BLOCK:
|
|
188
|
+
bodyOutput = await generateOutput(body_node, 0, format, mapper_file);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (bodyOutput) {
|
|
193
|
+
context += bodyOutput;
|
|
194
|
+
}
|
|
123
195
|
}
|
|
124
196
|
}
|
|
125
197
|
|
|
126
|
-
|
|
198
|
+
// Trim only leading/trailing newlines and their surrounding spaces to preserve indentation
|
|
199
|
+
const finalContext = effectiveTrimAndWrap ? context.replace(/^\s*[\r\n]+|[\r\n]+\s*$/g, "") : context;
|
|
200
|
+
|
|
201
|
+
if (result.includes(BODY_PLACEHOLDER)) {
|
|
202
|
+
result = result.replaceAll(BODY_PLACEHOLDER, finalContext);
|
|
203
|
+
} else if (finalContext.trim()) {
|
|
204
|
+
result += finalContext;
|
|
205
|
+
}
|
|
127
206
|
}
|
|
128
207
|
else if (format === textFormat) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
break;
|
|
208
|
+
if (node.type === ATBLOCK) {
|
|
209
|
+
result = node.content || "";
|
|
210
|
+
} else if (node.type === INLINE) {
|
|
211
|
+
result = node.value || "";
|
|
212
|
+
} else if (node.body) {
|
|
213
|
+
for (const body_node of node.body) {
|
|
214
|
+
switch (body_node.type) {
|
|
215
|
+
case TEXT: context += body_node.text || ""; break;
|
|
216
|
+
case INLINE: context += (body_node.value || "") + " "; break;
|
|
217
|
+
case ATBLOCK: context += (body_node.content || "").trimEnd() + "\n"; break;
|
|
218
|
+
case BLOCK:
|
|
219
|
+
const textBlockOutput = await generateOutput(body_node, 0, format, mapper_file);
|
|
220
|
+
context = context.trim() ? context.trimEnd() + "\n" + textBlockOutput : context + textBlockOutput;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
138
223
|
}
|
|
224
|
+
result += context;
|
|
139
225
|
}
|
|
140
|
-
result += context;
|
|
141
226
|
} else {
|
|
142
227
|
transpilerError([
|
|
143
228
|
"{line}<$red:Invalid Identifier:$> ",
|
|
144
229
|
`<$yellow:Identifier$> <$blue:'${node.id}'$> <$yellow: is not found in mapping outputs$>{line}`
|
|
145
230
|
]);
|
|
146
231
|
}
|
|
147
|
-
|
|
148
|
-
return result
|
|
232
|
+
|
|
233
|
+
return result;
|
|
149
234
|
}
|
|
150
235
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
236
|
+
/**
|
|
237
|
+
* The main entry point for the SomMark Transpiler.
|
|
238
|
+
* It takes an AST and turns it into the final formatted output.
|
|
239
|
+
*
|
|
240
|
+
* @param {Object|Object[]} optionsOrAst - Either the full options object or just the AST.
|
|
241
|
+
* @param {string} [format] - The target format.
|
|
242
|
+
* @param {Object} [mapperFile] - The mapper rules to use.
|
|
243
|
+
* @returns {Promise<string>} - The final formatted document.
|
|
244
|
+
*/
|
|
245
|
+
export async function transpiler(optionsOrAst, format, mapperFile) {
|
|
246
|
+
let body = null;
|
|
247
|
+
let targetFormat = format;
|
|
248
|
+
let targetMapper = mapperFile;
|
|
249
|
+
|
|
250
|
+
if (typeof optionsOrAst === "object" && !Array.isArray(optionsOrAst) && (optionsOrAst.ast || Array.isArray(optionsOrAst))) {
|
|
251
|
+
if (optionsOrAst.ast) {
|
|
252
|
+
const root = optionsOrAst.ast;
|
|
253
|
+
body = Array.isArray(root) ? root : (root.body || [root]);
|
|
254
|
+
targetFormat = optionsOrAst.format;
|
|
255
|
+
targetMapper = optionsOrAst.mapperFile;
|
|
256
|
+
} else if (Array.isArray(optionsOrAst)) {
|
|
257
|
+
body = optionsOrAst;
|
|
258
|
+
}
|
|
259
|
+
} else if (Array.isArray(optionsOrAst)) {
|
|
260
|
+
body = optionsOrAst;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!body || !Array.isArray(body)) return "";
|
|
154
264
|
|
|
155
|
-
async function transpiler({ ast, format, mapperFile, includeDocument = true }) {
|
|
156
265
|
let output = "";
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const shouldEscapeInline = inlineTarget.options?.escape !== false;
|
|
175
|
-
output += inlineTarget.render.call(mapperFile, {
|
|
176
|
-
args: node.args.length > 0 ? node.args : [],
|
|
177
|
-
content: (format === htmlFormat || format === mdxFormat) && shouldEscapeInline ? escapeHTML(node.value) : node.value,
|
|
178
|
-
ast: node
|
|
179
|
-
}) + (format === mdxFormat ? "\n" : "");
|
|
180
|
-
}
|
|
181
|
-
break;
|
|
182
|
-
case ATBLOCK:
|
|
183
|
-
let atTarget = matchedValue(mapperFile.outputs, node.id);
|
|
184
|
-
if (!atTarget) atTarget = mapperFile.getUnknownTag(node);
|
|
185
|
-
if (atTarget) {
|
|
186
|
-
const shouldEscapeAt = atTarget.options?.escape !== false;
|
|
187
|
-
let content = node.content;
|
|
188
|
-
if (shouldEscapeAt && (format === htmlFormat || format === mdxFormat)) {
|
|
189
|
-
content = escapeHTML(content);
|
|
190
|
-
}
|
|
191
|
-
const rendered = atTarget.render.call(mapperFile, { args: node.args, content, ast: node }).trimEnd() + "\n";
|
|
192
|
-
output = output.trim() ? output.trimEnd() + "\n" + rendered : output + rendered;
|
|
193
|
-
}
|
|
194
|
-
break;
|
|
266
|
+
let prev_body_node = null;
|
|
267
|
+
for (let i = 0; i < body.length; i++) {
|
|
268
|
+
const node = body[i];
|
|
269
|
+
const blockOutput = await generateOutput(body, i, targetFormat, targetMapper);
|
|
270
|
+
|
|
271
|
+
if (blockOutput) {
|
|
272
|
+
output += blockOutput;
|
|
273
|
+
if (node.type !== TEXT || node.text.trim().length > 0) {
|
|
274
|
+
prev_body_node = node;
|
|
275
|
+
}
|
|
276
|
+
} else if (node.type === COMMENT && targetMapper?.options?.removeComments) {
|
|
277
|
+
// If a comment is removed, check the next node.
|
|
278
|
+
// If it's just a blank line, skip it so we don't have extra gaps in the output.
|
|
279
|
+
const nextNode = body[i + 1];
|
|
280
|
+
if (nextNode && nextNode.type === TEXT && (nextNode.text === "\n" || nextNode.text === "\r\n")) {
|
|
281
|
+
i++; // Skip the next newline node
|
|
282
|
+
}
|
|
195
283
|
}
|
|
196
284
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// ========================================================================== //
|
|
200
|
-
return mapperFile.formatOutput(output, includeDocument);
|
|
285
|
+
|
|
286
|
+
return output.trim();
|
|
201
287
|
}
|
|
202
288
|
|
|
203
289
|
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
|
+
}
|