sommark 4.0.3 → 4.2.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 (43) hide show
  1. package/README.md +304 -73
  2. package/cli/cli.mjs +1 -1
  3. package/cli/commands/build.js +3 -1
  4. package/cli/commands/help.js +2 -0
  5. package/cli/commands/init.js +25 -6
  6. package/cli/constants.js +2 -1
  7. package/cli/helpers/transpile.js +5 -2
  8. package/constants/html_props.js +1 -0
  9. package/core/evaluator.js +1061 -0
  10. package/core/formats.js +15 -7
  11. package/core/helpers/config-loader.js +16 -8
  12. package/core/helpers/lib.js +72 -0
  13. package/core/helpers/preprocessor.js +202 -0
  14. package/core/helpers/runtimeOutput.js +28 -0
  15. package/core/helpers/url.js +12 -0
  16. package/core/labels.js +9 -2
  17. package/core/lexer.js +228 -61
  18. package/core/modules.js +338 -60
  19. package/core/parser.js +275 -55
  20. package/core/tokenTypes.js +11 -0
  21. package/core/transpiler.js +352 -66
  22. package/core/validator.js +70 -7
  23. package/formatter/tag.js +31 -7
  24. package/grammar.ebnf +21 -10
  25. package/helpers/fetch-fs.js +37 -0
  26. package/helpers/safeDataParser.js +3 -3
  27. package/helpers/spinner.js +97 -0
  28. package/helpers/utils.js +46 -0
  29. package/helpers/virtual-fs.js +29 -0
  30. package/index.browser.js +87 -0
  31. package/index.js +23 -332
  32. package/index.shared.js +443 -0
  33. package/mappers/languages/html.js +50 -9
  34. package/mappers/languages/json.js +81 -38
  35. package/mappers/languages/jsonc.js +82 -0
  36. package/mappers/languages/markdown.js +88 -48
  37. package/mappers/languages/mdx.js +50 -15
  38. package/mappers/languages/text.js +67 -0
  39. package/mappers/languages/xml.js +6 -6
  40. package/mappers/mapper.js +36 -4
  41. package/mappers/shared/index.js +12 -13
  42. package/package.json +11 -2
  43. package/core/formatter.js +0 -215
@@ -9,7 +9,7 @@ import { getPositionalArgs, matchedValue, safeArg } from "../../helpers/utils.js
9
9
  /**
10
10
  * Returns a string representing the specified indentation level.
11
11
  */
12
- function getIndent(depth) {
12
+ export function getIndent(depth) {
13
13
  return " ".repeat(depth);
14
14
  }
15
15
 
@@ -18,21 +18,35 @@ function getIndent(depth) {
18
18
  * @param {string} str - The string to escape.
19
19
  * @param {boolean} [trim=false] - Whether to trim the string.
20
20
  */
21
- function escapeString(str, trim = false) {
21
+ export function escapeString(str, trim = false) {
22
22
  let out = String(str);
23
23
  if (trim) out = out.trim();
24
24
  return JSON.stringify(out);
25
25
  }
26
26
 
27
+ import evaluator from "../../core/evaluator.js";
28
+
27
29
  /**
28
30
  * Recursively extracts text content from a node, ignoring structural metadata.
31
+ * Evaluates StaticLogic nodes using the Evaluator to inject build-time values.
29
32
  */
30
- function getNodeText(node) {
33
+ async function getNodeText(node) {
31
34
  if (!node.body) return "";
32
35
  let text = "";
33
36
  for (const child of node.body) {
34
- if (child.type === "Text") text += child.text;
35
- else if (child.type === "Block") text += getNodeText(child);
37
+ if (child.type === "Text") text += child.text || "";
38
+ else if (child.type === "StaticLogic") {
39
+ try {
40
+ const result = await evaluator.execute(child.code);
41
+ if (result !== undefined && typeof result !== "object") {
42
+ text += String(result);
43
+ }
44
+ } catch (err) {
45
+ console.error(`\x1b[31mLogic Error in JSON mapper:\x1b[0m ${err.message}`);
46
+ console.error(`\x1b[33mCode:\x1b[0m \x1b[34m${child.code}\x1b[0m`);
47
+ }
48
+ }
49
+ else if (child.type === "Block") text += await getNodeText(child);
36
50
  }
37
51
  return text;
38
52
  }
@@ -40,7 +54,9 @@ function getNodeText(node) {
40
54
  /**
41
55
  * Resolves the key-value pairing for a JSON member.
42
56
  */
43
- function renderMember(args, value) {
57
+ export function renderMember(args, value, inArray = false) {
58
+ if (inArray) return value;
59
+
44
60
  const posArgs = getPositionalArgs(args);
45
61
  const key = args.key || posArgs[0]; // The 'key' rule determines the member name
46
62
 
@@ -53,37 +69,63 @@ function renderMember(args, value) {
53
69
  /**
54
70
  * Formats a given node and tracks its indentation.
55
71
  */
56
- async function renderNode(node, mapper, depth = 0) {
72
+ export async function renderNode(node, mapper, depth = 0, inArray = false) {
57
73
  const target = matchedValue(mapper.outputs, node.id) || mapper.getUnknownTag(node);
58
74
  if (!target) return "";
59
75
 
60
- const textContent = getNodeText(node);
61
- return await target.render.call(mapper, {
76
+ evaluator.pushScope();
77
+ const textContent = await getNodeText(node);
78
+ const output = await target.render.call(mapper, {
62
79
  nodeType: node.type,
63
80
  args: node.args,
64
81
  content: "",
65
82
  textContent,
66
83
  ast: node,
67
- depth
84
+ depth,
85
+ inArray
68
86
  });
87
+ await evaluator.popScope();
88
+ return output;
69
89
  }
70
90
 
71
91
  /**
72
92
  * Formats the children of a node into a neat list.
73
93
  */
74
- async function renderChildren(node, mapper, depth = 0) {
94
+ export async function renderChildren(node, mapper, depth = 0, inArray = false) {
75
95
  let results = [];
76
96
  const childIndent = getIndent(depth + 1);
77
97
 
78
98
  for (const child of node.body) {
79
99
  if (child.type === "Block") {
80
- const output = await renderNode(child, mapper, depth + 1);
100
+ const output = await renderNode(child, mapper, depth + 1, inArray);
81
101
  if (output) {
82
- results.push(childIndent + output);
102
+ results.push({ type: "Block", value: childIndent + output });
103
+ }
104
+ }
105
+ }
106
+
107
+ let finalOutput = "";
108
+ for (let i = 0; i < results.length; i++) {
109
+ const current = results[i];
110
+ finalOutput += current.value;
111
+
112
+ if (current.type === "Block") {
113
+ // Add comma if there is another Block later
114
+ let hasNextBlock = false;
115
+ for (let j = i + 1; j < results.length; j++) {
116
+ if (results[j].type === "Block") {
117
+ hasNextBlock = true;
118
+ break;
119
+ }
83
120
  }
121
+ if (hasNextBlock) finalOutput += ",";
122
+ }
123
+
124
+ if (i < results.length - 1) {
125
+ finalOutput += "\n";
84
126
  }
85
127
  }
86
- return results.join(",\n");
128
+ return finalOutput;
87
129
  }
88
130
 
89
131
  const Json = Mapper.define({});
@@ -91,52 +133,53 @@ const Json = Mapper.define({});
91
133
  /**
92
134
  * The JSON object node rule.
93
135
  */
94
- Json.register(["Object", "object"], async ({ args, ast, depth = 0 }) => {
95
- if (ast.body.length === 0) return renderMember(args, "{}");
96
- const content = await renderChildren(ast, Json, depth);
136
+ Json.register(["Object", "object"], async ({ args, ast, depth = 0, inArray = false }) => {
137
+ if (ast.body.length === 0) return renderMember(args, "{}", inArray);
138
+ const content = await renderChildren(ast, Json, depth, false);
97
139
  const val = `{\n${content}\n${getIndent(depth)}}`;
98
- return renderMember(args, val);
140
+ return renderMember(args, val, inArray);
99
141
  }, { type: "Block", handleAst: true });
100
142
 
101
143
  /**
102
144
  * The JSON array node rule.
103
145
  */
104
- Json.register(["Array", "array"], async ({ args, ast, depth = 0 }) => {
105
- if (ast.body.length === 0) return renderMember(args, "[]");
106
- const content = await renderChildren(ast, Json, depth);
146
+ Json.register(["Array", "array"], async ({ args, ast, depth = 0, inArray = false }) => {
147
+ if (ast.body.length === 0) return renderMember(args, "[]", inArray);
148
+ const content = await renderChildren(ast, Json, depth, true);
107
149
  const val = `[\n${content}\n${getIndent(depth)}]`;
108
- return renderMember(args, val);
150
+ return renderMember(args, val, inArray);
109
151
  }, { type: "Block", handleAst: true });
110
152
 
111
153
  /**
112
154
  * JSON Primitives
113
155
  */
114
- Json.register("string", ({ args, textContent }) => {
115
- const trim = safeArg({
116
- args,
117
- key: "trim",
118
- type: "boolean",
156
+ Json.register("string", ({ args, textContent, inArray }) => {
157
+ const trim = safeArg({
158
+ args,
159
+ key: "trim",
160
+ type: "boolean",
119
161
  setType: v => v === "true" || v === true,
120
- fallBack: false
162
+ fallBack: false
121
163
  });
122
- const val = escapeString(textContent, trim);
123
- return renderMember(args, val);
164
+ const raw = safeArg({ args, index: inArray ? 0 : undefined, key: "value", fallBack: textContent });
165
+ const val = escapeString(raw, trim);
166
+ return renderMember(args, val, inArray);
124
167
  }, { type: "Block", handleAst: true });
125
168
 
126
- Json.register("number", ({ args, textContent }) => {
127
- const raw = textContent.trim();
169
+ Json.register("number", ({ args, textContent, inArray }) => {
170
+ const raw = String(safeArg({ args, index: inArray ? 0 : undefined, key: "value", fallBack: textContent })).trim();
128
171
  const val = (isNaN(Number(raw)) || raw === "") ? "0" : raw;
129
- return renderMember(args, val);
172
+ return renderMember(args, val, inArray);
130
173
  }, { type: "Block", handleAst: true });
131
174
 
132
- Json.register("bool", ({ args, textContent }) => {
133
- const raw = textContent.trim().toLowerCase();
175
+ Json.register("bool", ({ args, textContent, inArray }) => {
176
+ const raw = String(safeArg({ args, index: inArray ? 0 : undefined, key: "value", fallBack: textContent })).trim().toLowerCase();
134
177
  const val = (raw === "true" || raw === "1") ? "true" : "false";
135
- return renderMember(args, val);
178
+ return renderMember(args, val, inArray);
136
179
  }, { type: "Block", handleAst: true });
137
180
 
138
- Json.register("null", ({ args }) => {
139
- return renderMember(args, "null");
181
+ Json.register("null", ({ args, inArray }) => {
182
+ return renderMember(args, "null", inArray);
140
183
  }, { type: "Block", handleAst: true });
141
184
 
142
185
  export default Json;
@@ -0,0 +1,82 @@
1
+ import Json, { renderNode, getIndent, renderMember } from "./json.js";
2
+
3
+ /**
4
+ * JSONC Mapper - Creates JSON output with comments.
5
+ * It inherits from the standard JSON mapper and adds comment support.
6
+ */
7
+
8
+ async function renderChildren(node, mapper, depth = 0, inArray = false) {
9
+ let results = [];
10
+ const childIndent = getIndent(depth + 1);
11
+
12
+ for (const child of node.body) {
13
+ if (child.type === "Block") {
14
+ const output = await renderNode(child, mapper, depth + 1, inArray);
15
+ if (output) {
16
+ results.push({ type: "Block", value: childIndent + output });
17
+ }
18
+ } else if (child.type === "Comment") {
19
+ if (!mapper.options?.removeComments) {
20
+ results.push({ type: "Comment", value: childIndent + mapper.comment(child.text) });
21
+ }
22
+ } else if (child.type === "CommentBlock") {
23
+ if (!mapper.options?.removeComments) {
24
+ results.push({ type: "CommentBlock", value: childIndent + mapper.commentBlock(child.text, childIndent) });
25
+ }
26
+ }
27
+ }
28
+
29
+ let finalOutput = "";
30
+ for (let i = 0; i < results.length; i++) {
31
+ const current = results[i];
32
+ finalOutput += current.value;
33
+
34
+ if (current.type === "Block") {
35
+ // Add comma if there is another Block later
36
+ let hasNextBlock = false;
37
+ for (let j = i + 1; j < results.length; j++) {
38
+ if (results[j].type === "Block") {
39
+ hasNextBlock = true;
40
+ break;
41
+ }
42
+ }
43
+ if (hasNextBlock) finalOutput += ",";
44
+ }
45
+
46
+ if (i < results.length - 1) {
47
+ finalOutput += "\n";
48
+ }
49
+ }
50
+ return finalOutput;
51
+ }
52
+
53
+ const Jsonc = Json.clone();
54
+
55
+ Jsonc.comment = function (text) {
56
+ return `// ${text}`;
57
+ };
58
+
59
+ Jsonc.commentBlock = function (text, indent = " ") {
60
+ if (text.includes("\n")) {
61
+ const lines = text.split("\n");
62
+ return `/*\n${lines.map(line => indent + line).join("\n")}\n${indent}*/`;
63
+ }
64
+ return `/* ${text} */`;
65
+ };
66
+
67
+ // Re-register Object and Array to use the new renderChildren logic
68
+ Jsonc.register(["Object", "object"], async function ({ args, ast, depth = 0, inArray = false }) {
69
+ if (ast.body.length === 0) return renderMember(args, "{}", inArray);
70
+ const content = await renderChildren(ast, this, depth, false);
71
+ const val = `{\n${content}\n${getIndent(depth)}}`;
72
+ return renderMember(args, val, inArray);
73
+ }, { type: "Block", handleAst: true });
74
+
75
+ Jsonc.register(["Array", "array"], async function ({ args, ast, depth = 0, inArray = false }) {
76
+ if (ast.body.length === 0) return renderMember(args, "[]", inArray);
77
+ const content = await renderChildren(ast, this, depth, true);
78
+ const val = `[\n${content}\n${getIndent(depth)}]`;
79
+ return renderMember(args, val, inArray);
80
+ }, { type: "Block", handleAst: true });
81
+
82
+ export default Jsonc;
@@ -1,9 +1,68 @@
1
1
  import Mapper from "../mapper.js";
2
2
  import HTML from "./html.js";
3
3
  import { registerSharedOutputs } from "../shared/index.js";
4
- import { BLOCK, TEXT} from "../../core/labels.js";
5
- import transpiler from "../../core/transpiler.js";
4
+ import { BLOCK, TEXT, INLINE, STATIC_LOGIC } from "../../core/labels.js";
6
5
  import { VOID_ELEMENTS } from "../../constants/void_elements.js";
6
+ import evaluator from "../../core/evaluator.js";
7
+ import { matchedValue } from "../../helpers/utils.js";
8
+
9
+ /**
10
+ * Helper to manually render AST children inside handleAst blocks,
11
+ * avoiding the need to call the core transpiler recursively.
12
+ */
13
+ async function renderNodeAst(astArray, mapperFile) {
14
+ if (!astArray || !Array.isArray(astArray)) return "";
15
+ let result = "";
16
+ for (const node of astArray) {
17
+ if (node.type === TEXT) {
18
+ const text = String(node.text || "");
19
+ result += mapperFile.text(text);
20
+ } else if (node.type === INLINE) {
21
+ let target = matchedValue(mapperFile.outputs, node.id) || mapperFile.getUnknownTag(node);
22
+ if (target) {
23
+ let inlineValue = String(node.value || "").trim();
24
+ inlineValue = mapperFile.inlineText(inlineValue, target.options);
25
+ result += await target.render.call(mapperFile, {
26
+ nodeType: node.type,
27
+ args: node.args || {},
28
+ content: inlineValue,
29
+ ast: node
30
+ });
31
+ } else {
32
+ result += mapperFile.inlineText(node.value || "", {});
33
+ }
34
+ } else if (node.type === STATIC_LOGIC) {
35
+ try {
36
+ const val = await evaluator.execute(node.code);
37
+ if (val !== undefined && typeof val !== "object") {
38
+ result += mapperFile.text(String(val));
39
+ }
40
+ } catch (e) {
41
+ console.error(`\x1b[31mLogic Error in Markdown mapper:\x1b[0m ${e.message}`);
42
+ }
43
+ } else if (node.type === BLOCK) {
44
+ let target = matchedValue(mapperFile.outputs, node.id) || mapperFile.getUnknownTag(node);
45
+ if (target) {
46
+ const isSelfClosing = node.isSelfClosing || false;
47
+ let content = "";
48
+ evaluator.pushScope();
49
+ if (!target.options?.handleAst && node.body) {
50
+ content = await renderNodeAst(node.body, mapperFile);
51
+ }
52
+ const output = await target.render.call(mapperFile, {
53
+ nodeType: node.type,
54
+ args: node.args || {},
55
+ content,
56
+ ast: node,
57
+ isSelfClosing
58
+ });
59
+ await evaluator.popScope();
60
+ result += output;
61
+ }
62
+ }
63
+ }
64
+ return result;
65
+ }
7
66
 
8
67
  /**
9
68
  * The Markdown Mapper used for generating Markdown text.
@@ -62,11 +121,11 @@ const MARKDOWN = Mapper.define({
62
121
 
63
122
  return {
64
123
  render: async (ctx) => {
65
- const { args, ast } = ctx;
124
+ const { args, ast, isSelfClosing } = ctx;
66
125
  const body = ast && ast.body ? ast.body : [];
67
126
  const meaningful = body.filter(c => c.type !== TEXT || c.text.trim());
68
127
  const childCount = meaningful.length;
69
- const element = this.tag(id).smartAttributes(args, this.customProps);
128
+ const element = this.tag(id).smartAttributes(args, this.customProps, this.options);
70
129
 
71
130
  // Use the transpiler to format the children if any, otherwise use direct content
72
131
  let rawContent;
@@ -77,10 +136,10 @@ const MARKDOWN = Mapper.define({
77
136
  rawContent = node.value || "";
78
137
  rawContent = this.inlineText(rawContent, ctx);
79
138
  } else {
80
- rawContent = (await transpiler({ ast: body, mapperFile: this })).trim();
139
+ rawContent = (await renderNodeAst(body, this)).trim();
81
140
  }
82
141
 
83
- if (VOID_ELEMENTS.has(id)) {
142
+ if (isSelfClosing || VOID_ELEMENTS.has(id)) {
84
143
  return element.selfClose();
85
144
  }
86
145
 
@@ -115,45 +174,20 @@ MARKDOWN.register("quote", ({ args, content }) => {
115
174
  return md.quote(content, type);
116
175
  }, { type: "Block", resolve: true });
117
176
 
118
- /**
119
- * Unified heading renderer for Markdown and MDX mappers.
120
- * @param {Object} options - Mapper context and args.
121
- * @param {string} defaultFormat - Default format ("markdown" or "html").
122
- * @returns {string} - Rendered heading.
123
- */
124
- export function renderHeading({ args, content, ast }, defaultFormat = "markdown") {
125
- const heading = ast.id;
126
- const format = safeArg({ args, index: 0, key: "format", type: "string", fallBack: defaultFormat });
127
- const lvl = heading[1] && !isNaN(Number(heading[1])) ? Number(heading[1]) : 1;
128
-
129
- // Remove formatting arguments before checking for attributes
130
- const cleanArgs = { ...args };
131
- delete cleanArgs.format;
132
- delete cleanArgs["0"]; // Clean positional 'format'
133
-
134
- const hasAttributes = Object.keys(cleanArgs).length > 0;
135
-
136
- // Hybrid Dispatch: Switch to HTML if format is requested OR if attributes are present
137
- if (format === "html" || hasAttributes) {
138
- let htmlTarget = HTML.get(heading);
139
- if (!htmlTarget) {
140
- htmlTarget = HTML.getUnknownTag(ast);
141
- }
142
-
143
- if (htmlTarget) {
144
- return htmlTarget.render.call(this, { args: cleanArgs, content, ast, nodeType: ast.type });
145
- }
146
- }
147
-
148
- return md.heading(content, lvl);
149
- }
150
-
151
177
  /**
152
178
  * Headings - Renders H1-H6 block headings.
153
179
  */
154
180
  ["h1", "h2", "h3", "h4", "h5", "h6"].forEach(heading => {
155
- MARKDOWN.register(heading, function (ctx) {
156
- return renderHeading.call(this, ctx, "markdown");
181
+ MARKDOWN.register(heading, function ({ args, content, isSelfClosing }) {
182
+ const format = safeArg({ args, key: "format", type: "string", fallBack: "" });
183
+ const lvl = heading[1] && !isNaN(Number(heading[1])) ? Number(heading[1]) : 1;
184
+ if (format.toLowerCase() === "html") {
185
+ delete args.format;
186
+ const el = this.tag(heading).smartAttributes(args);
187
+ if (isSelfClosing) return el.selfClose();
188
+ return el.body(content);
189
+ }
190
+ return this.md.heading(content, lvl);
157
191
  }, { type: "Block" });
158
192
  });
159
193
 
@@ -215,7 +249,7 @@ MARKDOWN.register(
215
249
  },
216
250
  {
217
251
  type: ["Block", "Inline"],
218
- rules: { is_self_closing: false }
252
+ rules: { is_empty_body: false }
219
253
  }
220
254
  );
221
255
 
@@ -232,7 +266,7 @@ MARKDOWN.register(
232
266
  },
233
267
  {
234
268
  type: "Block",
235
- rules: { is_self_closing: true }
269
+ rules: { is_empty_body: true }
236
270
  }
237
271
  );
238
272
 
@@ -247,7 +281,7 @@ MARKDOWN.register(
247
281
  },
248
282
  {
249
283
  type: "Block",
250
- rules: { is_self_closing: true }
284
+ rules: { is_empty_body: true }
251
285
  }
252
286
  );
253
287
 
@@ -277,7 +311,7 @@ MARKDOWN.register(
277
311
 
278
312
  for (const child of cellAst) {
279
313
  if (child.type === BLOCK && (child.id.toLowerCase() === "cell" || child.id.toLowerCase() === "th" || child.id.toLowerCase() === "td")) {
280
- const cellContent = await transpiler({ ast: child.body, mapperFile: this });
314
+ const cellContent = await renderNodeAst(child.body, this);
281
315
  cells.push(cellContent.trim());
282
316
  }
283
317
  }
@@ -294,6 +328,8 @@ MARKDOWN.register(
294
328
  if (rowNode.type === BLOCK && rowNode.id.toLowerCase() === "row") {
295
329
  const rowData = await extractCells(rowNode);
296
330
  if (rowData.length > 0) sectionRows.push(rowData);
331
+ } else if (rowNode.type === STATIC_LOGIC) {
332
+ try { await evaluator.execute(rowNode.code); } catch (e) { console.error(`Logic Error: ${e.message}`); }
297
333
  }
298
334
  }
299
335
  return sectionRows;
@@ -303,6 +339,10 @@ MARKDOWN.register(
303
339
  // Remove empty text blocks
304
340
  const tableNodes = ast.body.filter(n => n.type !== TEXT || n.text.trim());
305
341
  for (const node of tableNodes) {
342
+ if (node.type === STATIC_LOGIC) {
343
+ try { await evaluator.execute(node.code); } catch (e) { console.error(`Logic Error: ${e.message}`); }
344
+ continue;
345
+ }
306
346
  if (node.type !== BLOCK) continue;
307
347
 
308
348
  const id = node.id.toLowerCase();
@@ -361,12 +401,12 @@ MARKDOWN.register(["list", "List"], async function ({ ast, args }) {
361
401
  // Trim spaces inside the list item
362
402
  const itemBody = node.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
363
403
  .filter(n => n.type !== TEXT || n.text);
364
- const itemContent = await transpiler({ ast: itemBody, mapperFile: this });
404
+ const itemContent = await renderNodeAst(itemBody, this);
365
405
  items.push(itemContent.trim());
366
406
  } else if (node.type === BLOCK && (id === "list")) {
367
407
  // Add nested lists to the latest item
368
408
  if (items.length > 0) {
369
- const listContent = await transpiler({ ast: [node], mapperFile: this });
409
+ const listContent = await renderNodeAst([node], this);
370
410
  items[items.length - 1] += "\n" + listContent;
371
411
  }
372
412
  }
@@ -386,7 +426,7 @@ MARKDOWN.register(["item", "Item"], async function ({ ast }) {
386
426
  // Trim whitespace but keep line breaks
387
427
  const bodyAst = ast.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
388
428
  .filter(n => n.type !== TEXT || n.text);
389
- return await transpiler({ ast: bodyAst, mapperFile: this });
429
+ return await renderNodeAst(bodyAst, this);
390
430
  }, { type: "Block", handleAst: true, trimAndWrapBlocks: false });
391
431
 
392
432
  /**
@@ -1,5 +1,5 @@
1
1
  import Mapper from "../mapper.js";
2
- import MARKDOWN, { renderHeading } from "./markdown.js";
2
+ import MARKDOWN from "./markdown.js";
3
3
  import { VOID_ELEMENTS } from "../../constants/void_elements.js";
4
4
 
5
5
  /**
@@ -28,14 +28,14 @@ const MDX = Mapper.define({
28
28
 
29
29
  return {
30
30
  render: (ctx) => {
31
- const { args, content } = ctx;
31
+ const { args, content, isSelfClosing } = ctx;
32
32
  const element = this.tag(tagName).jsxProps(args);
33
- return isVoid ? element.selfClose() : element.body(content);
33
+ return (isSelfClosing || isVoid) ? element.selfClose() : element.body(content);
34
34
  },
35
35
  options: {
36
36
  type: isVoid ? "Block" : (isCodeStyleOrScript ? ["Block", "AtBlock"] : ["Block", "Inline", "AtBlock"]),
37
37
  escape: !isCodeStyleOrScript,
38
- rules: { is_self_closing: isVoid }
38
+ rules: { is_empty_body: isVoid }
39
39
  }
40
40
  };
41
41
  },
@@ -48,14 +48,22 @@ const MDX = Mapper.define({
48
48
  * Formats a plain text node with Markdown escaping.
49
49
  */
50
50
  text(text, options) {
51
- return MARKDOWN.text.call(this, text, options);
51
+ let out = text;
52
+ if (options?.escape !== false) {
53
+ out = this.escapeHTML(out);
54
+ }
55
+ return out;
52
56
  },
53
57
 
54
58
  /**
55
59
  * Formats inline content before rendering, respecting explicit escape flags.
56
60
  */
57
61
  inlineText(text, options) {
58
- return MARKDOWN.inlineText.call(this, text, options);
62
+ let out = text;
63
+ if (options?.escape !== false) {
64
+ out = this.escapeHTML(out);
65
+ }
66
+ return out;
59
67
  },
60
68
 
61
69
  /**
@@ -66,9 +74,6 @@ const MDX = Mapper.define({
66
74
  if (options?.escape !== false) {
67
75
  out = this.escapeHTML(out);
68
76
  }
69
- if (out.includes('\n')) {
70
- out = '\n' + out + '\n';
71
- }
72
77
  return out;
73
78
  }
74
79
  });
@@ -76,13 +81,17 @@ const MDX = Mapper.define({
76
81
  const { tag } = MDX;
77
82
 
78
83
  MDX.inherit(MARKDOWN);
79
- MDX.md = MARKDOWN.md; // Provide the Markdown escaping tool
84
+ MDX.md = MARKDOWN.md;
80
85
 
81
- // MDX defaults to HTML tags for headings to ensure high-fidelity JSX output
82
- ["h1", "h2", "h3", "h4", "h5", "h6"].forEach(heading => {
83
- MDX.register(heading, function (ctx) {
84
- return renderHeading.call(this, ctx, "html");
85
- }, { type: "Block" });
86
+ ["h1", "h2", "h3", "h4", "h5", "h6"].forEach(h => {
87
+ MDX.register(h, function ({ args, content }) {
88
+ const format = this.safeArg({ args, key: "format", fallBack: "" });
89
+ if (format === "md" || format === "markdown") {
90
+ return this.md.heading(content, h.slice(1) || 1);
91
+ }
92
+ delete args.format;
93
+ return tag(h).jsxProps(args).body(content);
94
+ });
86
95
  });
87
96
 
88
97
  /**
@@ -92,4 +101,30 @@ MDX.register("mdx", ({ content }) => {
92
101
  return content;
93
102
  }, { escape: false, type: "AtBlock" });
94
103
 
104
+ // Inline CSS tag (Moved from shared)
105
+ MDX.register("css", ({ args, content }) => {
106
+ // Compile style from named arguments (keys that are not numeric digits)
107
+ const namedStyle = Object.keys(args)
108
+ .filter(k => isNaN(parseInt(k)))
109
+ .map(k => `${k}:${args[k]}`)
110
+ .join(";");
111
+
112
+ // Fetch positional style string (index 0) or "style" key if present
113
+ let positionalStyle = MDX.safeArg({ args, index: 0, key: "style", fallBack: "" });
114
+
115
+ // Filter out positional styles that are just duplicates of named arguments
116
+ const hasDuplicateNamed = Object.keys(args)
117
+ .filter(k => isNaN(parseInt(k)))
118
+ .some(k => args[k] === positionalStyle);
119
+
120
+ if (hasDuplicateNamed) {
121
+ positionalStyle = "";
122
+ }
123
+
124
+ // Combine both together
125
+ let style = [positionalStyle, namedStyle].filter(s => s.trim()).join(";");
126
+
127
+ return MDX.tag("span").jsxProps({ style }).body(content);
128
+ }, { type: "Inline" });
129
+
95
130
  export default MDX;