sommark 3.3.4 → 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 +7 -17
  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 +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 +7 -12
  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 +29 -74
  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 -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/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 -120
  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
@@ -1,11 +1,44 @@
1
- // Token Types in SomMark
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
 
@@ -1,206 +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, jsonFormat } from "./formats.js";
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 = `__SOMMARK_BODY_PLACEHOLDER_${Math.random().toString(36).slice(2)}__`;
12
+ const BODY_PLACEHOLDER = `SOMMARKBODYPLACEHOLDER${Math.random().toString(36).slice(2)}SOMMARK`;
11
13
 
12
- // ========================================================================== //
13
- // Helpers //
14
- // ========================================================================== //
15
-
16
- // Finds the matching output definition for a tag identifier
17
- function matchedValue(outputs, targetId) {
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.body) return "";
21
+ if (!node?.body && !node?.content) return "";
22
+ if (node.type === ATBLOCK) return node.content || "";
30
23
  let text = "";
31
- for (const child of node.body) {
32
- if (child.type === TEXT) text += child.text;
33
- else if (child.type === INLINE) text += child.value;
34
- else if (child.type === ATBLOCK) text += child.content;
35
- else if (child.type === BLOCK) text += getNodeText(child);
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
- // Processes an individual node and its body to produce formatted output
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
- result += target.render.call(mapper_file, { args: node.args, content: placeholder, textContent, ast: node });
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
- // Process body nodes recursively //
68
- // ========================================================================== //
69
- for (let j = 0; j < node.body.length; j++) {
70
- const body_node = node.body[j];
71
- switch (body_node.type) {
72
- case TEXT:
73
- const shouldEscapeText = target.options?.escape !== false;
74
- context += (format === htmlFormat) && shouldEscapeText ? escapeHTML(body_node.text) : body_node.text;
75
- break;
76
-
77
- case INLINE:
78
- let inlineTarget = matchedValue(mapper_file.outputs, body_node.id);
79
- if (!inlineTarget) {
80
- inlineTarget = mapper_file.getUnknownTag(body_node);
81
- }
82
-
83
- if (inlineTarget) {
84
- const shouldEscapeInline = inlineTarget.options?.escape !== false;
85
- context +=
86
- inlineTarget.render.call(mapper_file, {
87
- args: body_node.args.length > 0 ? body_node.args : [],
88
- content: (format === htmlFormat) && shouldEscapeInline ? escapeHTML(body_node.value) : body_node.value,
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
- }) + (format === mdxFormat ? "\n" : "");
91
- }
92
- break;
93
-
94
- case ATBLOCK:
95
- let atTarget = matchedValue(mapper_file.outputs, body_node.id);
96
- if (!atTarget) {
97
- atTarget = mapper_file.getUnknownTag(body_node);
98
- }
99
- if (atTarget) {
100
- const shouldEscapeAt = atTarget.options?.escape !== false;
101
- let content = body_node.content;
102
- if (shouldEscapeAt && (format === htmlFormat)) {
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
- const rendered = atTarget.render.call(mapper_file, { args: body_node.args, content, ast: body_node });
106
- const finalAtRendered = (format === mdxFormat) ? rendered : rendered.trimEnd() + "\n";
107
- context = context.trim() ? context.trimEnd() + "\n" + finalAtRendered : context + finalAtRendered;
108
- }
109
- break;
110
-
111
- case COMMENT:
112
- context += " ".repeat(body_node.depth) + `\n${mapper_file.comment(body_node.text)}\n`;
113
- break;
114
-
115
- case BLOCK:
116
- const blockOutput = await generateOutput(body_node, i, format, mapper_file);
117
- const blockIsParent = format === mdxFormat && body_node.body.length > 1;
118
- if (format === mdxFormat && !blockIsParent) {
119
- context += blockOutput;
120
- } else {
121
- context = context.trim() ? context.trimEnd() + "\n" + blockOutput : context + blockOutput;
122
- }
123
- break;
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
+ }
124
195
  }
125
196
  }
126
197
 
127
- result = result.replaceAll(BODY_PLACEHOLDER, context);
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
+ }
128
206
  }
129
207
  else if (format === textFormat) {
130
- for (const body_node of node.body) {
131
- switch (body_node.type) {
132
- case TEXT: context += body_node.text; break;
133
- case INLINE: context += body_node.value + " "; break;
134
- case ATBLOCK: context += body_node.content.trimEnd() + "\n"; break;
135
- case BLOCK:
136
- const textBlockOutput = await generateOutput(body_node, i, format, mapper_file);
137
- context = context.trim() ? context.trimEnd() + "\n" + textBlockOutput : context + textBlockOutput;
138
- 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
+ }
139
223
  }
224
+ result += context;
140
225
  }
141
- result += context;
142
226
  } else {
143
227
  transpilerError([
144
228
  "{line}<$red:Invalid Identifier:$> ",
145
229
  `<$yellow:Identifier$> <$blue:'${node.id}'$> <$yellow: is not found in mapping outputs$>{line}`
146
230
  ]);
147
231
  }
148
- const newline = (format === mdxFormat && node.body.length <= 1) ? "" : "\n";
149
- const finalResult = (format === mdxFormat) ? result : result.trimEnd();
150
- return finalResult + newline;
232
+
233
+ return result;
151
234
  }
152
235
 
153
- // ========================================================================== //
154
- // Main Transpiler Entry Point //
155
- // ========================================================================== //
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 "";
156
264
 
157
- async function transpiler({ ast, format, mapperFile, includeDocument = true }) {
158
265
  let output = "";
159
- for (let i = 0; i < ast.length; i++) {
160
- const node = ast[i];
161
- switch (node.type) {
162
- case BLOCK:
163
- output += await generateOutput(ast, i, format, mapperFile);
164
- break;
165
- case COMMENT:
166
- output += mapperFile.comment(node.text);
167
- break;
168
- case TEXT:
169
- const shouldEscapeText = (format === htmlFormat);
170
- output += shouldEscapeText ? escapeHTML(node.text) : node.text;
171
- break;
172
- case INLINE:
173
- let inlineTarget = matchedValue(mapperFile.outputs, node.id);
174
- if (!inlineTarget) inlineTarget = mapperFile.getUnknownTag(node);
175
- if (inlineTarget) {
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;
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
+ }
198
283
  }
199
284
  }
200
- // ========================================================================== //
201
- // Final Post-Processing (Dynamic Formats) //
202
- // ========================================================================== //
203
- return mapperFile.formatOutput(output, includeDocument);
285
+
286
+ return output.trim();
204
287
  }
205
288
 
206
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
+ }