sommark 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +274 -73
- package/cli/cli.mjs +2 -2
- package/cli/commands/build.js +3 -1
- package/cli/commands/help.js +4 -1
- package/cli/commands/init.js +25 -6
- package/cli/commands/show.js +20 -10
- package/cli/constants.js +2 -1
- package/cli/helpers/transpile.js +5 -2
- package/constants/html_props.js +1 -0
- package/core/evaluator.js +785 -0
- package/core/formats.js +15 -7
- package/core/helpers/config-loader.js +28 -15
- package/core/helpers/lib.js +75 -0
- package/core/helpers/preprocessor.js +185 -0
- package/core/helpers/runtimeOutput.js +28 -0
- package/core/labels.js +9 -2
- package/core/lexer.js +228 -61
- package/core/modules.js +331 -55
- package/core/parser.js +275 -55
- package/core/tokenTypes.js +11 -0
- package/core/transpiler.js +341 -59
- package/core/validator.js +85 -13
- package/formatter/tag.js +31 -7
- package/grammar.ebnf +21 -10
- package/helpers/safeDataParser.js +3 -3
- package/helpers/spinner.js +91 -0
- package/helpers/utils.js +46 -0
- package/index.js +125 -38
- package/mappers/languages/html.js +50 -9
- package/mappers/languages/json.js +81 -38
- package/mappers/languages/jsonc.js +82 -0
- package/mappers/languages/markdown.js +88 -48
- package/mappers/languages/mdx.js +50 -15
- package/mappers/languages/text.js +67 -0
- package/mappers/languages/xml.js +6 -6
- package/mappers/mapper.js +36 -4
- package/mappers/shared/index.js +12 -13
- package/package.json +6 -1
- package/core/formatter.js +0 -215
|
@@ -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
|
|
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 (
|
|
156
|
-
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
429
|
+
return await renderNodeAst(bodyAst, this);
|
|
390
430
|
}, { type: "Block", handleAst: true, trimAndWrapBlocks: false });
|
|
391
431
|
|
|
392
432
|
/**
|
package/mappers/languages/mdx.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Mapper from "../mapper.js";
|
|
2
|
-
import MARKDOWN
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
84
|
+
MDX.md = MARKDOWN.md;
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Mapper from "../mapper.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Text Mapper used for plain-text extraction.
|
|
5
|
+
*/
|
|
6
|
+
const TEXT = Mapper.define({
|
|
7
|
+
options: {
|
|
8
|
+
trimAndWrapBlocks: false
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Comments are discarded in plain-text output.
|
|
13
|
+
*/
|
|
14
|
+
comment() {
|
|
15
|
+
return "";
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Comment blocks are discarded in plain-text output.
|
|
20
|
+
*/
|
|
21
|
+
commentBlock() {
|
|
22
|
+
return "";
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Runtime logic is discarded in plain-text output.
|
|
27
|
+
*/
|
|
28
|
+
runtimeLogic() {
|
|
29
|
+
return "";
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns plain text literally.
|
|
34
|
+
*/
|
|
35
|
+
text(text) {
|
|
36
|
+
return text;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns inline text literally.
|
|
41
|
+
*/
|
|
42
|
+
inlineText(text) {
|
|
43
|
+
return text;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns at-block body text literally.
|
|
48
|
+
*/
|
|
49
|
+
atBlockBody(text) {
|
|
50
|
+
return text;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fallback for all tags - extracts inner content.
|
|
55
|
+
*/
|
|
56
|
+
getUnknownTag(node) {
|
|
57
|
+
const isBlock = node.type === "Block" || node.type === "ForEach";
|
|
58
|
+
return {
|
|
59
|
+
render: ({ content }) => content,
|
|
60
|
+
options: {
|
|
61
|
+
type: isBlock ? "Block" : (node.type === "AtBlock" ? "AtBlock" : "Inline")
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export default TEXT;
|
package/mappers/languages/xml.js
CHANGED
|
@@ -10,7 +10,7 @@ import { registerSharedOutputs } from "../shared/index.js";
|
|
|
10
10
|
* @param {string} content - The rendered inner content of the tag.
|
|
11
11
|
* @returns {string} The fully rendered XML tag string.
|
|
12
12
|
*/
|
|
13
|
-
const renderXmlTag = function (id, args, content) {
|
|
13
|
+
const renderXmlTag = function (id, args, content, isSelfClosing) {
|
|
14
14
|
// XML is case-sensitive, so we use the exact id provided
|
|
15
15
|
const element = this.tag(id);
|
|
16
16
|
|
|
@@ -27,7 +27,7 @@ const renderXmlTag = function (id, args, content) {
|
|
|
27
27
|
|
|
28
28
|
const hasBody = typeof content === "string" && content.trim().length > 0;
|
|
29
29
|
|
|
30
|
-
if (!hasBody) {
|
|
30
|
+
if (isSelfClosing || !hasBody) {
|
|
31
31
|
return element.selfClose();
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -55,7 +55,7 @@ const XML = Mapper.define({
|
|
|
55
55
|
getUnknownTag(node) {
|
|
56
56
|
const id = node.id;
|
|
57
57
|
return {
|
|
58
|
-
render: ({ args, content }) => renderXmlTag.call(this, id, args, content),
|
|
58
|
+
render: ({ args, content, isSelfClosing }) => renderXmlTag.call(this, id, args, content, isSelfClosing),
|
|
59
59
|
options: {
|
|
60
60
|
type: "any"
|
|
61
61
|
}
|
|
@@ -71,13 +71,13 @@ XML.register("xml", ({ args }) => {
|
|
|
71
71
|
const version = args.version || "1.0";
|
|
72
72
|
const encoding = args.encoding || "UTF-8";
|
|
73
73
|
return `<?xml version="${version}" encoding="${encoding}"?>`;
|
|
74
|
-
}, { type: "Block", rules: {
|
|
74
|
+
}, { type: "Block", rules: { is_empty_body: true } });
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* Registers the DOCTYPE declaration.
|
|
78
78
|
* Usage: [doctype = root: "note", system: "note.dtd"]
|
|
79
79
|
*/
|
|
80
|
-
XML.register("doctype", ({ args }) => {
|
|
80
|
+
XML.register(["DOCTYPE", "doctype"], ({ args }) => {
|
|
81
81
|
const root = args.root || "root";
|
|
82
82
|
const system = args.system;
|
|
83
83
|
const pub = args.public || args.fpi;
|
|
@@ -88,7 +88,7 @@ XML.register("doctype", ({ args }) => {
|
|
|
88
88
|
return `<!DOCTYPE ${root} SYSTEM "${system}">`;
|
|
89
89
|
}
|
|
90
90
|
return `<!DOCTYPE ${root}>`;
|
|
91
|
-
}, { type: "Block", rules: {
|
|
91
|
+
}, { type: "Block", rules: { is_empty_body: true } });
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* Registers the XML stylesheet processing instruction.
|
package/mappers/mapper.js
CHANGED
|
@@ -32,7 +32,13 @@ class Mapper {
|
|
|
32
32
|
* Registers a new tag rule. It needs a name and a function that says how to format it.
|
|
33
33
|
*
|
|
34
34
|
* @param {string|Array<string>} id - The name of the tag (like 'Person' or ['p', 'para']).
|
|
35
|
-
* @param {Function} renderOutput - The function that formats this tag.
|
|
35
|
+
* @param {Function} renderOutput - The function that formats this tag. It receives:
|
|
36
|
+
* - `args`: Tag attributes.
|
|
37
|
+
* - `content`: Formatted inner content.
|
|
38
|
+
* - `textContent`: Raw inner text.
|
|
39
|
+
* - `nodeType`: Type of node (Block, Inline, etc.).
|
|
40
|
+
* - `isSelfClosing`: (Blocks only) True if marked with !.
|
|
41
|
+
* - `ast`: The full AST node.
|
|
36
42
|
* @param {Object} [options={ escape: true }] - Settings for this tag.
|
|
37
43
|
* @param {boolean} [options.escape=true] - If true, the content will be made safe for HTML automatically.
|
|
38
44
|
*/
|
|
@@ -48,12 +54,18 @@ class Mapper {
|
|
|
48
54
|
if (typeof renderOutput !== "function") {
|
|
49
55
|
throw new TypeError("argument 'renderOutput' expected to be a function");
|
|
50
56
|
}
|
|
51
|
-
|
|
57
|
+
|
|
52
58
|
const render = renderOutput;
|
|
53
59
|
|
|
54
|
-
//
|
|
60
|
+
// -- RESERVED KEYWORD PROTECTION --
|
|
61
|
+
// We protect core engine keywords that would cause syntax errors if used as tag names.
|
|
62
|
+
const RESERVED = new Set(["end", "import", "slot", "$use-module", "for-each"]);
|
|
55
63
|
const ids = Array.isArray(id) ? id : [id];
|
|
64
|
+
|
|
56
65
|
for (const singleId of ids) {
|
|
66
|
+
if (RESERVED.has(singleId.toLowerCase())) {
|
|
67
|
+
sommarkError(`<$red:Reserved Keyword Error:$> Cannot register mapper for <$yellow:${singleId}$>. This is a protected SomMark engine keyword.`);
|
|
68
|
+
}
|
|
57
69
|
this.removeOutput(singleId);
|
|
58
70
|
}
|
|
59
71
|
|
|
@@ -85,6 +97,10 @@ class Mapper {
|
|
|
85
97
|
* @param {string} id - The output identifier to remove.
|
|
86
98
|
*/
|
|
87
99
|
removeOutput(id) {
|
|
100
|
+
if (typeof id !== "string") {
|
|
101
|
+
throw new TypeError("argument 'id' expected to be a string");
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
this.outputs = this.outputs
|
|
89
105
|
.map(output => {
|
|
90
106
|
if (Array.isArray(output.id)) {
|
|
@@ -120,6 +136,15 @@ class Mapper {
|
|
|
120
136
|
return "";
|
|
121
137
|
}
|
|
122
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Placeholder for block comment rendering. Should be overridden by specific mappers.
|
|
141
|
+
* @param {string} text - The raw comment text.
|
|
142
|
+
* @returns {string} - The formatted block comment string.
|
|
143
|
+
*/
|
|
144
|
+
commentBlock(text, indent = "") {
|
|
145
|
+
return this.comment(text);
|
|
146
|
+
}
|
|
147
|
+
|
|
123
148
|
/**
|
|
124
149
|
* Formats a plain text node.
|
|
125
150
|
* @param {string} text - The raw text content.
|
|
@@ -150,6 +175,13 @@ class Mapper {
|
|
|
150
175
|
return text;
|
|
151
176
|
}
|
|
152
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Formats runtime logic blocks natively.
|
|
180
|
+
* By default, this returns an empty string to discard logic for formats like JSON/MDX.
|
|
181
|
+
*/
|
|
182
|
+
runtimeLogic(code, isGlobal) {
|
|
183
|
+
return "";
|
|
184
|
+
}
|
|
153
185
|
|
|
154
186
|
/**
|
|
155
187
|
* Handles unknown tags. Should be overridden by specific mappers to provide fallback behavior.
|
|
@@ -245,7 +277,7 @@ class Mapper {
|
|
|
245
277
|
}));
|
|
246
278
|
|
|
247
279
|
newMapper.customProps = new Set(this.customProps);
|
|
248
|
-
|
|
280
|
+
|
|
249
281
|
return newMapper;
|
|
250
282
|
}
|
|
251
283
|
|
package/mappers/shared/index.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
|
+
import { runtimeError } from "../../core/errors.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Registers
|
|
4
|
+
* Registers universal utility tags shared across all SomMark mappers.
|
|
5
|
+
* These tags are considered "Format Agnostic."
|
|
6
|
+
*
|
|
3
7
|
* @param {Mapper} mapper - The mapper instance to register tags on.
|
|
4
8
|
*/
|
|
5
9
|
export function registerSharedOutputs(mapper) {
|
|
6
10
|
// 1. 'raw' - AtBlock that return the raw, unparsed content.
|
|
7
|
-
mapper.register("raw", ({ content }) =>
|
|
11
|
+
mapper.register("raw", ({ content, nodeType }) => {
|
|
12
|
+
if (nodeType === "Block") {
|
|
13
|
+
return String(content);
|
|
14
|
+
}
|
|
15
|
+
return content;
|
|
16
|
+
}, {
|
|
8
17
|
type: "AtBlock",
|
|
9
|
-
escape: false
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
// 2. 'css' - An Inline tag that applies inline styles to its content.
|
|
13
|
-
// Usage: (text)->(css: "color: red")
|
|
14
|
-
mapper.register("css", ({ args, content }) => {
|
|
15
|
-
let style = mapper.safeArg({ args, index: 0, key: "style", fallBack: "" });
|
|
16
|
-
style = style.split(";").map(s => s.trim().split(":").map(s => s.trim()).join(":")).join(";");
|
|
17
|
-
return mapper.tag("span").attributes({ style }).body(content);
|
|
18
|
-
}, {
|
|
19
|
-
type: "Inline"
|
|
18
|
+
escape: false
|
|
20
19
|
});
|
|
21
20
|
|
|
22
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sommark",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "SomMark is a declarative, extensible markup language for structured content that can be converted to HTML, Markdown, MDX, JSON, XML, and more.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -61,10 +61,15 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@mdx-js/mdx": "^3.1.1",
|
|
63
63
|
"fast-xml-parser": "^5.5.11",
|
|
64
|
+
"isomorphic-dompurify": "^3.14.0",
|
|
64
65
|
"jsdom": "^29.0.2",
|
|
65
66
|
"remark": "^15.0.1",
|
|
66
67
|
"remark-gfm": "^4.0.1",
|
|
67
68
|
"remark-parse": "^11.0.0",
|
|
68
69
|
"vitest": "^4.0.16"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@sebastianwessel/quickjs": "^1.1.1",
|
|
73
|
+
"acorn": "^8.15.0"
|
|
69
74
|
}
|
|
70
75
|
}
|