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.
Files changed (62) 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 +26 -6
  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 +15 -17
  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 +40 -75
  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 +238 -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/dedent.js +19 -0
  33. package/helpers/escapeHTML.js +13 -6
  34. package/helpers/kebabize.js +6 -0
  35. package/helpers/peek.js +9 -0
  36. package/helpers/removeChar.js +26 -13
  37. package/helpers/safeDataParser.js +114 -0
  38. package/helpers/utils.js +140 -158
  39. package/index.js +186 -188
  40. package/mappers/languages/html.js +105 -213
  41. package/mappers/languages/json.js +122 -171
  42. package/mappers/languages/markdown.js +355 -108
  43. package/mappers/languages/mdx.js +76 -120
  44. package/mappers/languages/xml.js +114 -0
  45. package/mappers/mapper.js +152 -123
  46. package/mappers/shared/index.js +22 -0
  47. package/package.json +26 -6
  48. package/SOMMARK-SPEC.md +0 -481
  49. package/cli/commands/list.js +0 -124
  50. package/constants/html_tags.js +0 -146
  51. package/core/pluginManager.js +0 -149
  52. package/core/plugins/comment-remover.js +0 -47
  53. package/core/plugins/module-system.js +0 -176
  54. package/core/plugins/raw-content-plugin.js +0 -78
  55. package/core/plugins/rules-validation-plugin.js +0 -231
  56. package/core/plugins/sommark-format.js +0 -244
  57. package/coverage_test.js +0 -21
  58. package/debug.js +0 -15
  59. package/helpers/camelize.js +0 -2
  60. package/helpers/defaultTheme.js +0 -3
  61. package/test_format_fix.js +0 -42
  62. package/v3-todo.smark +0 -73
package/formatter/mark.js CHANGED
@@ -1,8 +1,16 @@
1
+ /**
2
+ * MarkdownBuilder - A utility class for generating Markdown strings from structured data.
3
+ * Used primarily by the Markdown mapper to produce formatted text.
4
+ */
1
5
  class MarkdownBuilder {
2
6
  constructor() { }
3
- // ========================================================================== //
4
- // Headings //
5
- // ========================================================================== //
7
+
8
+ /**
9
+ * Formats text as a Markdown heading.
10
+ * @param {string} text - The heading content.
11
+ * @param {number|string} [level=1] - The heading level (1-6).
12
+ * @returns {string} - Formatted heading string.
13
+ */
6
14
  heading(text, level) {
7
15
  if (!text && !level) {
8
16
  return "";
@@ -18,22 +26,32 @@ class MarkdownBuilder {
18
26
  } else if (level < min) {
19
27
  level = min;
20
28
  }
21
- return `\n${"#".repeat(level)} ${text}\n`;
29
+ return `${"#".repeat(level)} ${text}`;
22
30
  }
23
31
  return text;
24
32
  }
25
- // ========================================================================== //
26
- // Url //
27
- // ========================================================================== //
33
+
34
+ /**
35
+ * Formats a Markdown link or image.
36
+ * @param {string} [type=""] - The link type ("image" for '!', otherwise empty).
37
+ * @param {string} text - The display text or alt text.
38
+ * @param {string} [url=""] - The target URL.
39
+ * @param {string} [title=""] - Optional hover title.
40
+ * @returns {string} - Formatted Markdown link/image string.
41
+ */
28
42
  url(type = "", text, url = "", title = "") {
29
43
  if (!text && !url) {
30
44
  return "";
31
45
  }
32
- return ` ${type === "image" ? "!" : ""}[${text}](${url + (title ? " " : "")}${title ? JSON.stringify(title) : ""}) `;
46
+ return `${type === "image" ? "!" : ""}[${text}](${url + (title ? " " : "")}${title ? JSON.stringify(title) : ""})`;
33
47
  }
34
- // ========================================================================== //
35
- // Bold //
36
- // ========================================================================== //
48
+
49
+ /**
50
+ * Formats text as bold.
51
+ * @param {string} text - The content to bold.
52
+ * @param {boolean} [is_underscore=false] - Use underscores ('__') instead of asterisks ('**').
53
+ * @returns {string} - Formatted bold string.
54
+ */
37
55
  bold(text, is_underscore = false) {
38
56
  if (!text) {
39
57
  return "";
@@ -41,9 +59,13 @@ class MarkdownBuilder {
41
59
  const format = is_underscore ? "__" : "**";
42
60
  return `${format}${text}${format}`;
43
61
  }
44
- // ========================================================================== //
45
- // Italic //
46
- // ========================================================================== //
62
+
63
+ /**
64
+ * Formats text as italic (emphasis).
65
+ * @param {string} text - The content to italicize.
66
+ * @param {boolean} [is_underscore=false] - Use underscores ('_') instead of asterisks ('*').
67
+ * @returns {string} - Formatted italic string.
68
+ */
47
69
  italic(text, is_underscore = false) {
48
70
  if (!text) {
49
71
  return "";
@@ -51,9 +73,13 @@ class MarkdownBuilder {
51
73
  const format = is_underscore ? "_" : "*";
52
74
  return `${format}${text}${format}`;
53
75
  }
54
- // ========================================================================== //
55
- // Emphasis //
56
- // ========================================================================== //
76
+
77
+ /**
78
+ * Formats text as bold-italic (strong emphasis).
79
+ * @param {string} text - The content to emphasize.
80
+ * @param {boolean} [is_underscore=false] - Use underscores ('___') instead of asterisks ('***').
81
+ * @returns {string} - Formatted emphasized string.
82
+ */
57
83
  emphasis(text, is_underscore = false) {
58
84
  if (!text) {
59
85
  return "";
@@ -61,9 +87,25 @@ class MarkdownBuilder {
61
87
  const format = is_underscore ? "___" : "***";
62
88
  return `${format}${text}${format}`;
63
89
  }
64
- // ========================================================================== //
65
- // Code Block //
66
- // ========================================================================== //
90
+
91
+ /**
92
+ * Formats text with strikethrough.
93
+ * @param {string} text - The content to strike.
94
+ * @returns {string} - Formatted strikethrough string.
95
+ */
96
+ strike(text) {
97
+ if (!text) {
98
+ return "";
99
+ }
100
+ return `~~${text}~~`;
101
+ }
102
+
103
+ /**
104
+ * Formats source code as a Markdown fenced code block.
105
+ * @param {string|Array} code - The code content.
106
+ * @param {string} [language=""] - The language identifier for syntax highlighting.
107
+ * @returns {string} - Formatted code block string.
108
+ */
67
109
  codeBlock(code, language = "") {
68
110
  if (!code) return "";
69
111
 
@@ -78,31 +120,85 @@ class MarkdownBuilder {
78
120
  if (!content) return "";
79
121
 
80
122
  const lang = language ? language : "";
81
- return `\n\`\`\`${lang}\n${content}\`\`\`\n`;
123
+ return `\n\`\`\`${lang}\n${content.trim()}\n\`\`\``;
82
124
  }
83
125
 
84
- // ========================================================================== //
85
- // Horizontal rule //
86
- // ========================================================================== //
126
+ /**
127
+ * Formats a Markdown horizontal rule.
128
+ * @param {string} [format="*"] - The character to use for the rule.
129
+ * @returns {string} - Formatted horizontal rule string.
130
+ */
87
131
  horizontal(format = "*") {
88
- return `\n${format.repeat(3)}\n`;
132
+ return `\n${format.repeat(3)}`;
89
133
  }
90
- // ========================================================================== //
91
- // Escape //
92
- // ========================================================================== //
134
+
135
+ /**
136
+ * Escapes special Markdown characters to prevent unintended formatting.
137
+ * @param {string} text - The text to escape.
138
+ * @returns {string} - Escaped text string.
139
+ */
93
140
  escape(text) {
94
141
  if (!text) return "";
95
142
 
96
- const special = /[\\*_{}\[\]()#+\-.!>|]/g;
143
+ const special = /[\\*_{}\[\]()#+\-.!>|~`]/g;
97
144
 
98
145
  return text.replace(special, "\\$&");
99
146
  }
100
147
 
101
- // ========================================================================== //
102
- // Table //
103
- // ========================================================================== //
148
+ /**
149
+ * Smartly escapes text to prevent unintended Markdown/HTML formatting
150
+ * while keeping "innocent" symbols clean (e.g., math, solo symbols).
151
+ *
152
+ * @param {string} text - The raw text to escape.
153
+ * @returns {string} - The safely escaped text.
154
+ */
155
+ smartEscaper(text) {
156
+ if (!text) return "";
157
+
158
+ // 1. Literal backslashes must stay literal
159
+ let result = text.replace(/\\/g, "\\\\");
160
+
161
+ // 2. HTML Tags: Detect <tag ... > or </tag>
162
+ // We do this BEFORE escaping & to prevent &amp;lt;
163
+ result = result.replace(/<([a-zA-Z\/][^>]*?)>/g, "&lt;$1&gt;");
164
+
165
+ // 3. Basics: Escape ampersands and quotes
166
+ // We use a lookahead to avoid double-escaping the '&' in '&lt;' and '&gt;' we just created
167
+ result = result.replace(/&(?!lt;|gt;)/g, "&amp;")
168
+ .replace(/"/g, "&quot;")
169
+ .replace(/'/g, "&#39;");
170
+
171
+ // 4. Markdown Heading Triggers: # at the start of a line
172
+ result = result.replace(/^#{1,6}\s+/gm, "\\$&");
173
+
174
+ // 5. Markdown List Triggers: -, *, +, or 1. at the start of a line
175
+ result = result.replace(/^([-*+]\s+)/gm, "\\$1");
176
+ result = result.replace(/^(\d+\.\s+)/gm, "\\$1");
177
+
178
+ // 6. Emphasis Triggers: *text*, **text**, _text_, ~~text~~
179
+ // We look for balanced wrappers around non-whitespace content.
180
+ result = result.replace(/(\*+|_+|~~)(\S[\s\S]*?\S)\1/g, (match, prefix, content) => {
181
+ const escapedPrefix = prefix.split("").map(c => "\\" + c).join("");
182
+ return escapedPrefix + content + escapedPrefix;
183
+ });
184
+
185
+ // 7. Horizontal Rule Triggers: ---, ***, ___ on their own line
186
+ result = result.replace(/^([*_-]{3,})\s*$/gm, "\\$1");
187
+
188
+ return result;
189
+ }
190
+
191
+
192
+
193
+
194
+ /**
195
+ * Formats data as a Markdown table.
196
+ * @param {Array<string>} headers - The table column headers.
197
+ * @param {Array<string|Array>} rows - The table row data.
198
+ * @returns {string} - Formatted Markdown table string.
199
+ */
104
200
  table(headers, rows) {
105
- let result = "\n\n";
201
+ let result = "";
106
202
  const isNotEmptyArray = arr => Array.isArray(arr) && arr.length > 0;
107
203
  if (isNotEmptyArray(headers) && isNotEmptyArray(rows)) {
108
204
  for (let i = 0; i < headers.length; i++) {
@@ -113,8 +209,8 @@ class MarkdownBuilder {
113
209
  const header = headers[i];
114
210
  result +=
115
211
  i === 0
116
- ? `|${"-".repeat(header.length + 2)}|`
117
- : `${"-".repeat(header.length + 2)}|${i === headers.length - 1 ? "\n" : ""}`;
212
+ ? `| --- |`
213
+ : ` --- |${i === headers.length - 1 ? "\n" : ""}`;
118
214
  }
119
215
  rows = rows.map(row => {
120
216
  let columns;
@@ -132,18 +228,82 @@ class MarkdownBuilder {
132
228
 
133
229
  return `| ${columns.join(" | ")}`;
134
230
  });
135
- for (const row of rows) {
136
- result += `${row} |\n`;
231
+ for (let i = 0; i < rows.length; i++) {
232
+ const row = rows[i];
233
+ result += `${row} |${i === rows.length - 1 ? "" : "\n"}`;
137
234
  }
138
235
  }
139
- return result + "\n";
236
+ return result;
140
237
  }
141
- // ========================================================================== //
142
- // Todo //
143
- // ========================================================================== //
144
- todo(checked = false, text) {
238
+
239
+ /**
240
+ * Formats a task list item.
241
+ * @param {boolean|string} [status=false] - The task status (true, "x", or "done" for checked).
242
+ * @param {string} text - The task description.
243
+ * @returns {string} - Formatted task list item string.
244
+ */
245
+ todo(status = false, text) {
145
246
  if (!text) return "";
146
- return checked ? `- [x] ${text}\n` : `- [ ] ${text}\n`;
247
+ let checked = status;
248
+ if (typeof status === "string") {
249
+ const s = status.trim().toLowerCase();
250
+ checked = s === "x" || s === "done";
251
+ }
252
+ return checked ? `- [x] ${text}` : `- [ ] ${text}`;
253
+ }
254
+
255
+ /**
256
+ * Formats an unordered Markdown list.
257
+ * @param {Array<string>} items - The list items.
258
+ * @param {number} [depth=0] - The nesting depth for indentation.
259
+ * @param {string} [marker="-"] - The bullet point character.
260
+ * @returns {string} - Formatted unordered list string.
261
+ */
262
+ unorderedList(items, depth = 0, marker = "-") {
263
+ if (!Array.isArray(items)) return "";
264
+ const indent = " ".repeat(depth);
265
+ return items.map(item => `${indent}${marker} ${item.replace(/\n/g, "\n" + indent + " ")}`).join("\n");
266
+ }
267
+
268
+ /**
269
+ * Formats an ordered Markdown list.
270
+ * @param {Array<string>} items - The list items.
271
+ * @param {number} [depth=0] - The nesting depth for indentation.
272
+ * @returns {string} - Formatted ordered list string.
273
+ */
274
+ orderedList(items, depth = 0) {
275
+ if (!Array.isArray(items)) return "";
276
+ const indent = " ".repeat(depth);
277
+ return items.map((item, i) => `${indent}${i + 1}. ${item.replace(/\n/g, "\n" + indent + " ")}`).join("\n");
278
+ }
279
+
280
+ /**
281
+ * Formats text as a Markdown blockquote or GFM alert (admonition).
282
+ * @param {string} content - The content to quote.
283
+ * @param {string} [type=""] - The alert type ("note", "tip", "important", "caution", "warning").
284
+ * @returns {string} - Formatted blockquote string.
285
+ */
286
+ /**
287
+ * Formats text as a Markdown blockquote or GFM alert (admonition).
288
+ * @param {string} content - The content to quote.
289
+ * @param {string} [type=""] - The alert type ("note", "tip", "important", "caution", "warning").
290
+ * @returns {string} - Formatted blockquote string.
291
+ */
292
+ quote(content, type = "") {
293
+ if (!content) return "";
294
+
295
+ const alertTypes = ["note", "tip", "important", "caution", "warning"];
296
+ const alertType = type ? type.toLowerCase().trim() : "";
297
+ const isAlert = alertTypes.includes(alertType);
298
+
299
+ const cleanContent = content.trim();
300
+
301
+ const prefix = isAlert ? `[!${alertType.toUpperCase()}]\n` : "";
302
+
303
+ const fullText = prefix + cleanContent;
304
+ const lines = fullText.split(/\r?\n/);
305
+
306
+ return lines.map(line => `> ${line}`).join("\n");
147
307
  }
148
308
  }
149
309
  export default MarkdownBuilder;
package/formatter/tag.js CHANGED
@@ -1,28 +1,45 @@
1
1
  import escapeHTML from "../helpers/escapeHTML.js";
2
+ import kebabize from "../helpers/kebabize.js";
3
+ import { HTML_PROPS } from "../constants/html_props.js";
4
+ import { VOID_ELEMENTS } from "../constants/void_elements.js";
5
+
6
+ /**
7
+ * TagBuilder - A builder pattern utility for programmatic HTML/XML tag generation.
8
+ * Handles attributes, body content, and self-closing tags with high-fidelity escaping.
9
+ */
2
10
  class TagBuilder {
3
11
  #children;
4
12
  #attr;
5
13
  #is_self_close;
6
- // ========================================================================== //
7
- // Constructor //
8
- // ========================================================================== //
14
+
15
+ /**
16
+ * Creates a new TagBuilder instance.
17
+ * @param {string} tagName - The name of the tag (e.g., 'div', 'span').
18
+ */
9
19
  constructor(tagName) {
10
20
  this.tagName = tagName;
11
21
  this.#children = "";
12
22
  this.#attr = [];
13
23
  this.#is_self_close = false;
14
24
  }
15
- // ========================================================================== //
16
- // Attributes //
17
- // ========================================================================== //
18
- attributes(obj, ...arr) {
25
+
26
+ /**
27
+ * Adds attributes to the tag.
28
+ * @param {Object} obj - Key-value pair of attributes.
29
+ * @param {boolean} [strict=false] - If true, boolean true values render as key="true".
30
+ * @param {...string} arr - Optional list of boolean attributes (e.g., 'disabled', 'required').
31
+ * @returns {TagBuilder} - Returns this instance for chaining.
32
+ */
33
+ attributes(obj, strict = false, ...arr) {
19
34
  if (obj && obj instanceof Object) {
20
35
  Object.entries(obj).forEach(([key, value]) => {
36
+ if (!isNaN(parseInt(key))) return; // Skip numeric positional arguments
21
37
  if (value === true) {
22
- this.#attr.push(`${key}`);
38
+ this.#attr.push(strict ? `${key}="true"` : `${key}`);
23
39
  } else if (value !== false) {
24
40
  let val = value ?? "";
25
41
  if (key === "style" && typeof val === "string") {
42
+ // V4 DYNAMIC CSS: Automatically wrap CSS variables in var()
26
43
  val = val.replace(/(^|[^\w\-_$])(--[\w\-_$]+)(?![\w\-_$]|:)/g, "$1var($2)");
27
44
  }
28
45
  this.#attr.push(`${key}="${escapeHTML(val)}"`);
@@ -36,30 +53,174 @@ class TagBuilder {
36
53
  }
37
54
  return this;
38
55
  }
39
- // ========================================================================== //
40
- // Props //
41
- // ========================================================================== //
56
+
57
+ /**
58
+ * Adds attributes with project-certified smart handling (kebabization, styling fallback).
59
+ * Implements the V4 "Smart Styling Fallback" strategy.
60
+ *
61
+ * @param {Object} args - Key-value pair of Smark arguments/attributes.
62
+ * @param {Set<string>} [customProps=new Set()] - Set of project-certified custom properties.
63
+ * @param {Object} [options={}] - Configuration flags (e.g., skipSmartHandling).
64
+ * @returns {TagBuilder} - Returns this instance for chaining.
65
+ */
66
+ smartAttributes(args, customProps = new Set(), options = {}) {
67
+ if (!args || typeof args !== "object") return this;
68
+
69
+ const id = this.tagName.toLowerCase();
70
+ const isCodeStyleOrScript = ["style", "script"].includes(id);
71
+ let inline_style = "";
72
+
73
+ // 1. Initial CSS Variable/Style processing
74
+ if (!isCodeStyleOrScript && args.style) {
75
+ if (typeof args.style === "object") {
76
+ inline_style = Object.entries(args.style)
77
+ .map(([k, v]) => `${kebabize(k)}:${v}`)
78
+ .join(";") + (Object.keys(args.style).length > 0 ? ";" : "");
79
+ } else if (typeof args.style === "string") {
80
+ inline_style = args.style.endsWith(";") ? args.style : args.style + ";";
81
+ } else {
82
+ inline_style = String(args.style) + ";";
83
+ }
84
+ }
85
+
86
+ // 2. Attribute Dispatching
87
+ const keys = Object.keys(args).filter(arg => isNaN(parseInt(arg)));
88
+ keys.forEach(key => {
89
+ if (!isNaN(parseInt(key))) return; // Skip numeric positional arguments
90
+ if (key === "style") return;
91
+ if (isCodeStyleOrScript && key === "scoped") return;
92
+
93
+ const isDimensionAttributeSupported = ["img", "video", "svg", "canvas", "iframe", "object", "embed"].includes(id);
94
+ const isWidthOrHeight = key === "width" || key === "height";
95
+ const isEvent = key.toLowerCase().startsWith("on");
96
+ const isNative = HTML_PROPS.has(key);
97
+ const isCustom = customProps.has(key) || customProps.has(kebabize(key));
98
+ const isDataOrAria = kebabize(key).startsWith("data-") || kebabize(key).startsWith("aria-");
99
+
100
+ const k = isEvent ? key.toLowerCase() : (isNative || isCustom) ? key : kebabize(key);
101
+
102
+ if (isCodeStyleOrScript) {
103
+ // Specialized tags: only render standard attributes, no styling fallback
104
+ this.#attr.push(`${k}="${escapeHTML(String(args[key]))}"`);
105
+ } else {
106
+ // Standard elements: process smart styling fallbacks for non-native props
107
+ if (isEvent || ((isNative || isCustom) && (!isWidthOrHeight || isDimensionAttributeSupported)) || isDataOrAria) {
108
+ const val = typeof args[key] === "object" ? JSON.stringify(args[key]) : args[key];
109
+ this.#attr.push(`${k}="${escapeHTML(String(val))}"`);
110
+ } else {
111
+ const val = typeof args[key] === "object" ? JSON.stringify(args[key]) : args[key];
112
+ inline_style += `${k}:${val};`;
113
+ }
114
+ }
115
+ });
116
+
117
+ if (inline_style) {
118
+ // V4 DYNAMIC CSS: Automatically wrap CSS variables in var()
119
+ const processedStyle = inline_style.replace(/(^|[^\w\-_$])(--[\w\-_$]+)(?![\w\-_$]|:)/g, "$1var($2)");
120
+ this.#attr.push(`style="${escapeHTML(processedStyle)}"`);
121
+ }
122
+
123
+ return this;
124
+ }
125
+
126
+ /**
127
+ * Converts SomMark arguments into JSX props and applies them to the tag.
128
+ * Implements smart handling for className, style objects, and automated JSX expression wrapping.
129
+ *
130
+ * @param {Object} args - The list of arguments.
131
+ * @returns {TagBuilder} - Returns this instance for chaining.
132
+ */
133
+ jsxProps(args) {
134
+ if (!args || typeof args !== "object") return this;
135
+
136
+ const jsxProps = [];
137
+ const styleObj = {};
138
+
139
+ const keys = Object.keys(args).filter(arg => isNaN(parseInt(arg)));
140
+ keys.forEach(key => {
141
+ let val = args[key];
142
+
143
+ let k = key;
144
+ if (k === "class") k = "className";
145
+
146
+ // Strip quotes from string literals if they were passed raw
147
+ if (typeof val === "string" && ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")))) {
148
+ val = val.slice(1, -1);
149
+ }
150
+
151
+ if (k === "style") {
152
+ // Convert CSS strings to React-style objects
153
+ if (typeof val === "string") {
154
+ const pairs = val.includes(";") ? val.split(";") : val.split(",");
155
+ pairs.forEach(pair => {
156
+ let [prop, value] = pair.split(":").map(s => s.trim());
157
+ if (prop && value) {
158
+ const camelProp = prop.replace(/-([a-z])/g, g => g[1].toUpperCase());
159
+ styleObj[camelProp] = value;
160
+ }
161
+ });
162
+ } else if (typeof val === "object") {
163
+ Object.assign(styleObj, val);
164
+ }
165
+ } else {
166
+ // Detect if it should be wrapped in {} (JSX Expression)
167
+ const isObject = typeof val === "object" && val !== null;
168
+ const isBoolean = typeof val === "boolean" || val === "true" || val === "false";
169
+ const isNumeric = typeof val === "number" || (typeof val === "string" && val !== "" && !isNaN(val));
170
+ const isWrappedInBraces = typeof val === "string" && val.startsWith("{") && val.endsWith("}");
171
+
172
+ const shouldBeJSXExpression = isObject || isBoolean || isNumeric || isWrappedInBraces;
173
+
174
+ let finalVal = val;
175
+ if (val === "true") finalVal = true;
176
+ if (val === "false") finalVal = false;
177
+ if (typeof val === "string" && isNumeric) finalVal = Number(val);
178
+
179
+ if (isWrappedInBraces) {
180
+ // Strip outer braces: {theme} -> theme
181
+ finalVal = val.slice(1, -1);
182
+ } else if (isObject) {
183
+ // Clean JS string for JSX: { a: 1 } instead of {"a":1}
184
+ finalVal = JSON.stringify(val).replace(/"([^"]+)":/g, "$1:");
185
+ }
186
+
187
+ jsxProps.push({
188
+ __type__: shouldBeJSXExpression ? "other" : "string",
189
+ [k]: finalVal
190
+ });
191
+ }
192
+ });
193
+
194
+ if (Object.keys(styleObj).length > 0) {
195
+ const styleStr = JSON.stringify(styleObj).replace(/"([^"]+)":/g, "$1:");
196
+ jsxProps.push({ __type__: "other", style: styleStr });
197
+ }
198
+
199
+ // Use the legacy props helper to apply the generated list
200
+ this.props(jsxProps);
201
+ return this;
202
+ }
203
+
204
+ /**
205
+ * Internal helper to apply MDX-style property entries.
206
+ * Note: This method no longer returns 'this' and is removed from the chain.
207
+ * Use .jsxProps(args) instead.
208
+ *
209
+ * @param {Object|Array} propsList - The property entries to add.
210
+ */
42
211
  props(propsList) {
43
212
  const list = Array.isArray(propsList) ? propsList : [propsList];
44
213
  if (list.length > 0) {
45
214
  for (const propEntry of list) {
46
- if (typeof propEntry !== "object" || propEntry === null) {
47
- throw new TypeError("prop expects an object with property { __type__ }");
48
- }
49
-
50
- if (!Object.prototype.hasOwnProperty.call(propEntry, "__type__")) {
51
- throw new TypeError("prop expects an object with property { __type__ }");
215
+ if (typeof propEntry !== "object" || propEntry === null || !Object.prototype.hasOwnProperty.call(propEntry, "__type__")) {
216
+ continue;
52
217
  }
53
218
 
54
219
  const { __type__, ...rest } = propEntry;
55
220
  const entries = Object.entries(rest);
56
-
57
- if (entries.length === 0) {
58
- continue;
59
- }
221
+ if (entries.length === 0) continue;
60
222
 
61
223
  const [key, value] = entries[0];
62
-
63
224
  switch (__type__) {
64
225
  case "string":
65
226
  this.#attr.push(`${key}="${escapeHTML(String(value))}"`);
@@ -70,11 +231,14 @@ class TagBuilder {
70
231
  }
71
232
  }
72
233
  }
73
- return this;
74
234
  }
75
- // ========================================================================== //
76
- // Body //
77
- // ========================================================================== //
235
+
236
+ /**
237
+ * Sets the body content of the tag.
238
+ * Note: Calling this method finalizes the builder state by returning the generated string.
239
+ * @param {string|Array} nodes - The inner content of the tag.
240
+ * @returns {string} - The generated HTML string.
241
+ */
78
242
  body(nodes) {
79
243
  if (nodes) {
80
244
  let space = this.#children ? " " : "";
@@ -82,16 +246,22 @@ class TagBuilder {
82
246
  }
83
247
  return this.builder();
84
248
  }
85
- // ========================================================================== //
86
- // Self Close //
87
- // ========================================================================== //
249
+
250
+ /**
251
+ * Marks the tag as self-closing (e.g., <img />).
252
+ * Note: Calling this method finalizes the builder state by returning the generated string.
253
+ * @returns {string} - The generated HTML string.
254
+ */
88
255
  selfClose() {
89
256
  this.#is_self_close = true;
90
257
  return this.builder();
91
258
  }
92
- // ========================================================================== //
93
- // Builder //
94
- // ========================================================================== //
259
+
260
+ /**
261
+ * Internal method to construct the final tag string.
262
+ * @private
263
+ * @returns {string} - The generated HTML string.
264
+ */
95
265
  builder() {
96
266
  const props = this.#attr.length > 0 ? " " + this.#attr.join(" ") : "";
97
267
  if (this.#is_self_close) {