sommark 3.3.4 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -82
- package/assets/logo.json +28 -0
- package/assets/smark.logo.png +0 -0
- package/assets/smark.logo.svg +21 -0
- package/cli/cli.mjs +7 -17
- package/cli/commands/build.js +26 -6
- package/cli/commands/color.js +22 -26
- package/cli/commands/help.js +10 -10
- package/cli/commands/init.js +20 -31
- package/cli/commands/print.js +18 -16
- package/cli/commands/show.js +4 -0
- package/cli/commands/version.js +6 -0
- package/cli/constants.js +9 -5
- package/cli/helpers/config.js +11 -0
- package/cli/helpers/file.js +17 -6
- package/cli/helpers/transpile.js +15 -17
- package/core/errors.js +49 -25
- package/core/formats.js +7 -3
- package/core/formatter.js +215 -0
- package/core/helpers/config-loader.js +40 -75
- package/core/labels.js +21 -9
- package/core/lexer.js +491 -212
- package/core/modules.js +164 -0
- package/core/parser.js +516 -389
- package/core/tokenTypes.js +36 -1
- package/core/transpiler.js +238 -154
- package/core/validator.js +79 -0
- package/formatter/mark.js +203 -43
- package/formatter/tag.js +202 -32
- package/grammar.ebnf +57 -50
- package/helpers/colorize.js +26 -13
- package/helpers/dedent.js +19 -0
- package/helpers/escapeHTML.js +13 -6
- package/helpers/kebabize.js +6 -0
- package/helpers/peek.js +9 -0
- package/helpers/removeChar.js +26 -13
- package/helpers/safeDataParser.js +114 -0
- package/helpers/utils.js +140 -158
- package/index.js +186 -188
- package/mappers/languages/html.js +105 -213
- package/mappers/languages/json.js +122 -171
- package/mappers/languages/markdown.js +355 -108
- package/mappers/languages/mdx.js +76 -120
- package/mappers/languages/xml.js +114 -0
- package/mappers/mapper.js +152 -123
- package/mappers/shared/index.js +22 -0
- package/package.json +26 -6
- package/SOMMARK-SPEC.md +0 -481
- package/cli/commands/list.js +0 -124
- package/constants/html_tags.js +0 -146
- package/core/pluginManager.js +0 -149
- package/core/plugins/comment-remover.js +0 -47
- package/core/plugins/module-system.js +0 -176
- package/core/plugins/raw-content-plugin.js +0 -78
- package/core/plugins/rules-validation-plugin.js +0 -231
- package/core/plugins/sommark-format.js +0 -244
- package/coverage_test.js +0 -21
- package/debug.js +0 -15
- package/helpers/camelize.js +0 -2
- package/helpers/defaultTheme.js +0 -3
- package/test_format_fix.js +0 -42
- package/v3-todo.smark +0 -73
package/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
|
-
|
|
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
|
|
29
|
+
return `${"#".repeat(level)} ${text}`;
|
|
22
30
|
}
|
|
23
31
|
return text;
|
|
24
32
|
}
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
46
|
+
return `${type === "image" ? "!" : ""}[${text}](${url + (title ? " " : "")}${title ? JSON.stringify(title) : ""})`;
|
|
33
47
|
}
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
123
|
+
return `\n\`\`\`${lang}\n${content.trim()}\n\`\`\``;
|
|
82
124
|
}
|
|
83
125
|
|
|
84
|
-
|
|
85
|
-
|
|
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)}
|
|
132
|
+
return `\n${format.repeat(3)}`;
|
|
89
133
|
}
|
|
90
|
-
|
|
91
|
-
|
|
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 = /[\\*_{}\[\]()
|
|
143
|
+
const special = /[\\*_{}\[\]()#+\-.!>|~`]/g;
|
|
97
144
|
|
|
98
145
|
return text.replace(special, "\\$&");
|
|
99
146
|
}
|
|
100
147
|
|
|
101
|
-
|
|
102
|
-
|
|
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 &lt;
|
|
163
|
+
result = result.replace(/<([a-zA-Z\/][^>]*?)>/g, "<$1>");
|
|
164
|
+
|
|
165
|
+
// 3. Basics: Escape ampersands and quotes
|
|
166
|
+
// We use a lookahead to avoid double-escaping the '&' in '<' and '>' we just created
|
|
167
|
+
result = result.replace(/&(?!lt;|gt;)/g, "&")
|
|
168
|
+
.replace(/"/g, """)
|
|
169
|
+
.replace(/'/g, "'");
|
|
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 = "
|
|
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
|
-
?
|
|
117
|
-
:
|
|
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 (
|
|
136
|
-
|
|
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
|
|
236
|
+
return result;
|
|
140
237
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|