sommark 3.3.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +24 -4
- 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 +7 -12
- package/core/errors.js +49 -25
- package/core/formats.js +7 -3
- package/core/formatter.js +215 -0
- package/core/helpers/config-loader.js +29 -74
- 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 +237 -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/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 +198 -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/mappers/languages/mdx.js
CHANGED
|
@@ -1,139 +1,95 @@
|
|
|
1
1
|
import Mapper from "../mapper.js";
|
|
2
|
-
import MARKDOWN from "./markdown.js";
|
|
3
|
-
import { HTML_TAGS } from "../../constants/html_tags.js";
|
|
2
|
+
import MARKDOWN, { renderHeading } from "./markdown.js";
|
|
4
3
|
import { VOID_ELEMENTS } from "../../constants/void_elements.js";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The MDX Mapper used for generating Markdown with JSX.
|
|
7
|
+
*/
|
|
8
|
+
const MDX = Mapper.define({
|
|
9
|
+
/**
|
|
10
|
+
* Renders a JSX-style comment in MDX output.
|
|
11
|
+
* @param {string} text - The raw comment text.
|
|
12
|
+
* @returns {string} - Formatted JSX comment string.
|
|
13
|
+
*/
|
|
10
14
|
comment(text) {
|
|
11
|
-
return `{
|
|
12
|
-
}
|
|
15
|
+
return `{/* ${text} */}`;
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Provides high-fidelity fallback for unknown tags by rendering them as JSX components.
|
|
20
|
+
* @param {Object} node - The unknown AST node.
|
|
21
|
+
* @returns {Object} - A virtual tag registration for JSX rendering.
|
|
22
|
+
*/
|
|
13
23
|
getUnknownTag(node) {
|
|
14
24
|
const tagName = node.id;
|
|
25
|
+
const lowerId = tagName.toLowerCase();
|
|
26
|
+
const isVoid = VOID_ELEMENTS.has(lowerId);
|
|
27
|
+
const isCodeStyleOrScript = ["code", "style", "script"].includes(lowerId);
|
|
28
|
+
|
|
15
29
|
return {
|
|
16
|
-
render: (
|
|
17
|
-
const
|
|
18
|
-
element.
|
|
19
|
-
return element.body(content);
|
|
30
|
+
render: (ctx) => {
|
|
31
|
+
const { args, content } = ctx;
|
|
32
|
+
const element = this.tag(tagName).jsxProps(args);
|
|
33
|
+
return isVoid ? element.selfClose() : element.body(content);
|
|
34
|
+
},
|
|
35
|
+
options: {
|
|
36
|
+
type: isVoid ? "Block" : (isCodeStyleOrScript ? ["Block", "AtBlock"] : ["Block", "Inline", "AtBlock"]),
|
|
37
|
+
escape: !isCodeStyleOrScript,
|
|
38
|
+
rules: { is_self_closing: isVoid }
|
|
20
39
|
}
|
|
21
40
|
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
styleObj[camelProp] = value;
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
} else if (typeof val === "object") {
|
|
53
|
-
Object.assign(styleObj, val);
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
// Detection for expressions
|
|
57
|
-
const isBoolean = val === "true" || val === "false" || typeof val === "boolean";
|
|
58
|
-
const isNumeric = val !== "" && !isNaN(val) && typeof val !== "boolean";
|
|
59
|
-
const looksLikeExpression = typeof val === "string" &&
|
|
60
|
-
(/[0-9]/.test(val) && /[+\-*/%()]/.test(val)); // Math expression detection
|
|
61
|
-
|
|
62
|
-
const shouldBeJSXExpression = isEvent || isBoolean || isNumeric || looksLikeExpression;
|
|
63
|
-
|
|
64
|
-
let finalVal = val;
|
|
65
|
-
if (val === "true") finalVal = true;
|
|
66
|
-
if (val === "false") finalVal = false;
|
|
67
|
-
if (isNumeric && typeof val === "string") finalVal = Number(val);
|
|
68
|
-
|
|
69
|
-
jsxProps.push({
|
|
70
|
-
__type__: shouldBeJSXExpression ? "other" : "string",
|
|
71
|
-
[k]: finalVal
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (Object.keys(styleObj).length > 0) {
|
|
77
|
-
const styleStr = JSON.stringify(styleObj).replace(/"/g, "'").replace(/'([^']+)':/g, '$1:');
|
|
78
|
-
jsxProps.push({ __type__: "other", style: styleStr });
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
options: {
|
|
44
|
+
trimAndWrapBlocks: true
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Formats a plain text node with Markdown escaping.
|
|
49
|
+
*/
|
|
50
|
+
text(text, options) {
|
|
51
|
+
return MARKDOWN.text.call(this, text, options);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Formats inline content before rendering, respecting explicit escape flags.
|
|
56
|
+
*/
|
|
57
|
+
inlineText(text, options) {
|
|
58
|
+
return MARKDOWN.inlineText.call(this, text, options);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Formats the literal content inside AtBlocks.
|
|
63
|
+
*/
|
|
64
|
+
atBlockBody(text, options) {
|
|
65
|
+
let out = text;
|
|
66
|
+
if (options?.escape !== false) {
|
|
67
|
+
out = this.escapeHTML(out);
|
|
79
68
|
}
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
if (out.includes('\n')) {
|
|
70
|
+
out = '\n' + out + '\n';
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
82
73
|
}
|
|
83
|
-
}
|
|
74
|
+
});
|
|
84
75
|
|
|
85
|
-
const MDX = new MdxMapper();
|
|
86
76
|
const { tag } = MDX;
|
|
87
77
|
|
|
88
78
|
MDX.inherit(MARKDOWN);
|
|
79
|
+
MDX.md = MARKDOWN.md; // Provide the Markdown escaping tool
|
|
89
80
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (clean.startsWith(";")) clean = clean.substring(1).trim();
|
|
96
|
-
// Add spacing around ESM blocks
|
|
97
|
-
return "\n" + clean + "\n\n";
|
|
98
|
-
}, { escape: false, type: ["AtBlock", "Block"] });
|
|
99
|
-
|
|
100
|
-
// Re-register HTML tags to use jsxProps
|
|
101
|
-
HTML_TAGS.forEach(tagName => {
|
|
102
|
-
const capitalized = tagName.charAt(0).toUpperCase() + tagName.slice(1);
|
|
103
|
-
|
|
104
|
-
// Register even if it exists in MARKDOWN to override it with JSX version
|
|
105
|
-
const idsToRegister = [tagName, capitalized];
|
|
106
|
-
|
|
107
|
-
MDX.register(
|
|
108
|
-
idsToRegister,
|
|
109
|
-
({ args, content }) => {
|
|
110
|
-
const element = tag(tagName);
|
|
111
|
-
|
|
112
|
-
// Auto-ID for Headings
|
|
113
|
-
if (/^h[1-6]$/i.test(tagName) && !args.id && content && /^[A-Za-z0-9]/.test(content)) {
|
|
114
|
-
const id = content
|
|
115
|
-
.toString()
|
|
116
|
-
.toLowerCase()
|
|
117
|
-
.replace(/[^\w\s-]/g, "")
|
|
118
|
-
.replace(/\s+/g, "-");
|
|
119
|
-
args.id = id;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
element.props(MDX.jsxProps(args, tagName));
|
|
123
|
-
|
|
124
|
-
if (VOID_ELEMENTS.has(tagName)) {
|
|
125
|
-
return element.selfClose();
|
|
126
|
-
}
|
|
127
|
-
return element.body(content);
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
escape: false,
|
|
131
|
-
type: VOID_ELEMENTS.has(tagName) ? "Block" : ["Block", "Inline"],
|
|
132
|
-
rules: {
|
|
133
|
-
is_self_closing: VOID_ELEMENTS.has(tagName)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
);
|
|
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" });
|
|
137
86
|
});
|
|
138
87
|
|
|
88
|
+
/**
|
|
89
|
+
* mdx AtBlock - Renders raw MDX content (ESM imports, exports, or complex JSX).
|
|
90
|
+
*/
|
|
91
|
+
MDX.register("mdx", ({ content }) => {
|
|
92
|
+
return content;
|
|
93
|
+
}, { escape: false, type: "AtBlock" });
|
|
94
|
+
|
|
139
95
|
export default MDX;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import Mapper from "../mapper.js";
|
|
2
|
+
import { registerSharedOutputs } from "../shared/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Renders a standard XML tag based on the provided identifier and arguments.
|
|
6
|
+
* Ensures strict attribute quoting and handles self-closing tags for empty bodies.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} id - The XML tag identifier (case-sensitive).
|
|
9
|
+
* @param {Object} args - Key-value pairs to be rendered as XML attributes.
|
|
10
|
+
* @param {string} content - The rendered inner content of the tag.
|
|
11
|
+
* @returns {string} The fully rendered XML tag string.
|
|
12
|
+
*/
|
|
13
|
+
const renderXmlTag = function (id, args, content) {
|
|
14
|
+
// XML is case-sensitive, so we use the exact id provided
|
|
15
|
+
const element = this.tag(id);
|
|
16
|
+
|
|
17
|
+
// Filter out positional indices (numeric keys) for XML attributes
|
|
18
|
+
const namedArgs = {};
|
|
19
|
+
Object.keys(args).forEach(key => {
|
|
20
|
+
if (isNaN(parseInt(key))) {
|
|
21
|
+
namedArgs[key] = args[key];
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// In XML, attributes must always have values (strict = true)
|
|
26
|
+
element.attributes(namedArgs, true);
|
|
27
|
+
|
|
28
|
+
const hasBody = typeof content === "string" && content.trim().length > 0;
|
|
29
|
+
|
|
30
|
+
if (!hasBody) {
|
|
31
|
+
return element.selfClose();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return element.body(content);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The XML Mapper used for creating XML pages.
|
|
39
|
+
*/
|
|
40
|
+
const XML = Mapper.define({
|
|
41
|
+
/**
|
|
42
|
+
* Renders a comment in XML format.
|
|
43
|
+
* @param {string} text - The comment content.
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
comment(text) {
|
|
47
|
+
return `<!-- ${text} -->`;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves unknown tags by preserving their original case and applying XML rules.
|
|
52
|
+
* @param {Object} node - The AST node representing the unknown tag.
|
|
53
|
+
* @returns {Object} Renderer definition for the tag.
|
|
54
|
+
*/
|
|
55
|
+
getUnknownTag(node) {
|
|
56
|
+
const id = node.id;
|
|
57
|
+
return {
|
|
58
|
+
render: ({ args, content }) => renderXmlTag.call(this, id, args, content),
|
|
59
|
+
options: {
|
|
60
|
+
type: "any"
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Registers the XML declaration as a self-closing block.
|
|
68
|
+
* Usage: [xml = version: "1.0", encoding: "UTF-8"]
|
|
69
|
+
*/
|
|
70
|
+
XML.register("xml", ({ args }) => {
|
|
71
|
+
const version = args.version || "1.0";
|
|
72
|
+
const encoding = args.encoding || "UTF-8";
|
|
73
|
+
return `<?xml version="${version}" encoding="${encoding}"?>`;
|
|
74
|
+
}, { type: "Block", rules: { is_self_closing: true } });
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Registers the DOCTYPE declaration.
|
|
78
|
+
* Usage: [doctype = root: "note", system: "note.dtd"]
|
|
79
|
+
*/
|
|
80
|
+
XML.register("doctype", ({ args }) => {
|
|
81
|
+
const root = args.root || "root";
|
|
82
|
+
const system = args.system;
|
|
83
|
+
const pub = args.public || args.fpi;
|
|
84
|
+
|
|
85
|
+
if (pub && system) {
|
|
86
|
+
return `<!DOCTYPE ${root} PUBLIC "${pub}" "${system}">`;
|
|
87
|
+
} else if (system) {
|
|
88
|
+
return `<!DOCTYPE ${root} SYSTEM "${system}">`;
|
|
89
|
+
}
|
|
90
|
+
return `<!DOCTYPE ${root}>`;
|
|
91
|
+
}, { type: "Block", rules: { is_self_closing: true } });
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Registers the XML stylesheet processing instruction.
|
|
95
|
+
* Usage: [xml-stylesheet = href: "style.xsl"]
|
|
96
|
+
*/
|
|
97
|
+
XML.register("xml-stylesheet", ({ args }) => {
|
|
98
|
+
const type = args.type || "text/xsl";
|
|
99
|
+
const href = args.href;
|
|
100
|
+
if (!href) return "";
|
|
101
|
+
return `<?xml-stylesheet type="${type}" href="${href}"?>`;
|
|
102
|
+
}, { type: "Block", rules: { is_self_closing: true } });
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Registers CDATA sections.
|
|
106
|
+
* Usage: @_cdata_@: ; raw content @_end_@
|
|
107
|
+
*/
|
|
108
|
+
XML.register("cdata", ({ content }) => {
|
|
109
|
+
return `<![CDATA[${content}]]>`;
|
|
110
|
+
}, { type: "AtBlock" });
|
|
111
|
+
|
|
112
|
+
registerSharedOutputs(XML);
|
|
113
|
+
|
|
114
|
+
export default XML;
|
package/mappers/mapper.js
CHANGED
|
@@ -2,92 +2,40 @@ import TagBuilder from "../formatter/tag.js";
|
|
|
2
2
|
import MarkdownBuilder from "../formatter/mark.js";
|
|
3
3
|
import escapeHTML from "../helpers/escapeHTML.js";
|
|
4
4
|
import { sommarkError } from "../core/errors.js";
|
|
5
|
-
import { matchedValue, safeArg
|
|
5
|
+
import { matchedValue, safeArg } from "../helpers/utils.js";
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The base class for all mappers. It manages how tags and blocks are turned into final text.
|
|
10
|
+
* This is used to build HTML, MDX, and other output formats.
|
|
11
|
+
*/
|
|
11
12
|
class Mapper {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// ========================================================================== //
|
|
13
|
+
/**
|
|
14
|
+
* Sets up a new mapper with empty lists for rules and tags.
|
|
15
|
+
*/
|
|
16
16
|
constructor() {
|
|
17
|
+
/** @type {Array<Object>} List of rules for formatting tags. */
|
|
17
18
|
this.outputs = [];
|
|
19
|
+
/** @type {MarkdownBuilder} Specialized builder for Markdown-related formatting. */
|
|
18
20
|
this.md = new MarkdownBuilder();
|
|
19
|
-
|
|
20
|
-
this.
|
|
21
|
-
|
|
22
|
-
this.pageProps = {
|
|
23
|
-
pageTitle: "SomMark Page",
|
|
24
|
-
tabIcon: {
|
|
25
|
-
type: "image/x-icon",
|
|
26
|
-
src: ""
|
|
27
|
-
},
|
|
28
|
-
charset: "UTF-8",
|
|
29
|
-
viewport: "width=device-width, initial-scale=1.0",
|
|
30
|
-
httpEquiv: { "X-UA-Compatible": "IE=edge" }
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
this.#customHeaderContent = "";
|
|
34
|
-
|
|
21
|
+
/** @type {Set<string>} A list of extra property names this mapper understands. */
|
|
22
|
+
this.customProps = new Set();
|
|
23
|
+
/** @type {Function} Helper that makes text safe for HTML. */
|
|
35
24
|
this.escapeHTML = escapeHTML;
|
|
36
|
-
this.
|
|
37
|
-
this.
|
|
38
|
-
this.todo = todo;
|
|
39
|
-
this.styles = [];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ========================================================================== //
|
|
43
|
-
// Style Management //
|
|
44
|
-
// ========================================================================== //
|
|
45
|
-
|
|
46
|
-
addStyle(css) {
|
|
47
|
-
if (typeof css === "object" && css !== null) {
|
|
48
|
-
let styleString = "";
|
|
49
|
-
for (const [selector, rules] of Object.entries(css)) {
|
|
50
|
-
let rulesString = "";
|
|
51
|
-
for (const [prop, value] of Object.entries(rules)) {
|
|
52
|
-
rulesString += `${prop}:${value};`;
|
|
53
|
-
}
|
|
54
|
-
styleString += `${selector}{${rulesString}}`;
|
|
55
|
-
}
|
|
56
|
-
css = styleString;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (typeof css === "string" && css.trim() && !this.styles.includes(css.trim())) {
|
|
60
|
-
this.styles.push(css.trim());
|
|
61
|
-
}
|
|
25
|
+
/** @type {Object} Settings that change how this mapper works. */
|
|
26
|
+
this.options = {};
|
|
62
27
|
}
|
|
63
28
|
|
|
29
|
+
// -- Tag Registration ---------------------------------------------------- //
|
|
64
30
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
metas += `${this.tag("meta").attributes({ name: "viewport", content: viewport }).selfClose()}\n`;
|
|
74
|
-
|
|
75
|
-
for (const [key, value] of Object.entries(httpEquiv)) {
|
|
76
|
-
metas += `${this.tag("meta").attributes({ "http-equiv": key, content: value }).selfClose()}\n`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
metas += `${this.tag("title").body(pageTitle)}\n`;
|
|
80
|
-
|
|
81
|
-
if (tabIcon && tabIcon.src) {
|
|
82
|
-
metas += `${this.tag("link").attributes({ rel: "icon", type: tabIcon.type, href: tabIcon.src }).selfClose()}\n`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return metas + this.#customHeaderContent;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ========================================================================== //
|
|
89
|
-
// Mappings //
|
|
90
|
-
// ========================================================================== //
|
|
31
|
+
/**
|
|
32
|
+
* Registers a new tag rule. It needs a name and a function that says how to format it.
|
|
33
|
+
*
|
|
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.
|
|
36
|
+
* @param {Object} [options={ escape: true }] - Settings for this tag.
|
|
37
|
+
* @param {boolean} [options.escape=true] - If true, the content will be made safe for HTML automatically.
|
|
38
|
+
*/
|
|
91
39
|
register(id, renderOutput, options = { escape: true }) {
|
|
92
40
|
if (!id || !renderOutput) {
|
|
93
41
|
throw new Error("Expected arguments are not defined");
|
|
@@ -100,17 +48,8 @@ class Mapper {
|
|
|
100
48
|
if (typeof renderOutput !== "function") {
|
|
101
49
|
throw new TypeError("argument 'renderOutput' expected to be a function");
|
|
102
50
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
typeof data !== "object" ||
|
|
106
|
-
data === null ||
|
|
107
|
-
!Object.prototype.hasOwnProperty.call(data, "args") ||
|
|
108
|
-
!Object.prototype.hasOwnProperty.call(data, "content")
|
|
109
|
-
) {
|
|
110
|
-
throw new TypeError("render expects an object with properties { args, content }");
|
|
111
|
-
}
|
|
112
|
-
return renderOutput.call(this, data);
|
|
113
|
-
};
|
|
51
|
+
|
|
52
|
+
const render = renderOutput;
|
|
114
53
|
|
|
115
54
|
// Prevent duplicate IDs by removing any existing overlap before registering
|
|
116
55
|
const ids = Array.isArray(id) ? id : [id];
|
|
@@ -121,6 +60,12 @@ class Mapper {
|
|
|
121
60
|
this.outputs.push({ id, render, options });
|
|
122
61
|
}
|
|
123
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Inherits all registered outputs from one or more other mappers.
|
|
65
|
+
* Last-match-wins logic: If an output exists in multiple mappers, the one from the last mapper in the list is used.
|
|
66
|
+
*
|
|
67
|
+
* @param {...Mapper} mappers - The mapper instances to inherit from.
|
|
68
|
+
*/
|
|
124
69
|
inherit(...mappers) {
|
|
125
70
|
for (const mapper of mappers) {
|
|
126
71
|
if (mapper && Array.isArray(mapper.outputs)) {
|
|
@@ -135,46 +80,100 @@ class Mapper {
|
|
|
135
80
|
}
|
|
136
81
|
}
|
|
137
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Removes a specific output registration by its ID.
|
|
85
|
+
* @param {string} id - The output identifier to remove.
|
|
86
|
+
*/
|
|
138
87
|
removeOutput(id) {
|
|
139
|
-
this.outputs = this.outputs
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
88
|
+
this.outputs = this.outputs
|
|
89
|
+
.map(output => {
|
|
90
|
+
if (Array.isArray(output.id)) {
|
|
91
|
+
// Only remove the specific ID from the array
|
|
92
|
+
const newIds = output.id.filter(singleId => singleId !== id);
|
|
93
|
+
if (newIds.length === 0) return null; // Remove entire entry if no IDs left
|
|
94
|
+
if (newIds.length === output.id.length) return output; // No change
|
|
95
|
+
return { ...output, id: newIds }; // Return updated entry
|
|
96
|
+
} else {
|
|
97
|
+
return output.id === id ? null : output;
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.filter(Boolean); // Clean up nulls
|
|
146
101
|
}
|
|
147
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Retrieves a registered output entry (render function and options) by ID.
|
|
105
|
+
* @param {string} id - The output identifier.
|
|
106
|
+
* @returns {Object|null} - The output entry or null if not found.
|
|
107
|
+
*/
|
|
148
108
|
get(id) {
|
|
149
109
|
return matchedValue(this.outputs, id) || null;
|
|
150
110
|
}
|
|
151
111
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
112
|
+
// -- Utility Helpers ------------------------------------------------------ //
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Placeholder for comment rendering. Should be overridden by specific mappers.
|
|
116
|
+
* @param {string} text - The raw comment text.
|
|
117
|
+
* @returns {string} - The formatted comment string.
|
|
118
|
+
*/
|
|
155
119
|
comment(text) {
|
|
156
120
|
return "";
|
|
157
121
|
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Formats a plain text node.
|
|
125
|
+
* @param {string} text - The raw text content.
|
|
126
|
+
* @param {Object} [options] - Target options like { escape: false }.
|
|
127
|
+
* @returns {string} - The formatted text string.
|
|
128
|
+
*/
|
|
129
|
+
text(text, options) {
|
|
130
|
+
return text;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Formats the content of an inline statement.
|
|
135
|
+
* @param {string} text - The raw inline content.
|
|
136
|
+
* @param {Object} options - The target output options.
|
|
137
|
+
* @returns {string} - The formatted inline string.
|
|
138
|
+
*/
|
|
139
|
+
inlineText(text, options) {
|
|
140
|
+
return text;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Formats the raw body of an At-Block.
|
|
145
|
+
* @param {string} text - The raw atblock body.
|
|
146
|
+
* @param {Object} options - The target output options.
|
|
147
|
+
* @returns {string} - The formatted atblock string.
|
|
148
|
+
*/
|
|
149
|
+
atBlockBody(text, options) {
|
|
150
|
+
return text;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Handles unknown tags. Should be overridden by specific mappers to provide fallback behavior.
|
|
156
|
+
* @param {Object} node - The AST node for the unknown id.
|
|
157
|
+
* @returns {Object|null} - A tag-like entry for fallback rendering.
|
|
158
|
+
*/
|
|
158
159
|
getUnknownTag(node) {
|
|
159
160
|
return null;
|
|
160
161
|
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Creates a new TagBuilder instance for programmatic HTML-like tag creation.
|
|
165
|
+
* @param {string} tagName - The name of the tag (e.g., 'div', 'p').
|
|
166
|
+
* @returns {TagBuilder}
|
|
167
|
+
*/
|
|
161
168
|
tag(tagName) {
|
|
162
169
|
return new TagBuilder(tagName);
|
|
163
170
|
}
|
|
164
171
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (Array.isArray(rawData)) {
|
|
171
|
-
for (const data of rawData) {
|
|
172
|
-
if (typeof data === "string") {
|
|
173
|
-
this.#customHeaderContent += data + "\n";
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
172
|
+
/**
|
|
173
|
+
* Checks if this mapper has any of the specified IDs registered.
|
|
174
|
+
* @param {Array<string>} ids - List of IDs to check for.
|
|
175
|
+
* @returns {boolean} - True if at least one ID exists.
|
|
176
|
+
*/
|
|
178
177
|
includesId(ids) {
|
|
179
178
|
try {
|
|
180
179
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
@@ -209,35 +208,65 @@ class Mapper {
|
|
|
209
208
|
}
|
|
210
209
|
}
|
|
211
210
|
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Safely retrieves an argument value, handling both named and positional access.
|
|
213
|
+
*
|
|
214
|
+
* @param {Object} options - Resolution options.
|
|
215
|
+
* @returns {any} - The resolved argument value.
|
|
216
|
+
*/
|
|
217
|
+
safeArg(options) {
|
|
218
|
+
return safeArg(options);
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Creates a deep clone of the mapper instance, isolating output registrations.
|
|
223
|
+
* Inherits all properties and binds methods to the new instance.
|
|
224
|
+
*
|
|
225
|
+
* @returns {Mapper} - The cloned mapper instance.
|
|
226
|
+
*/
|
|
216
227
|
clone() {
|
|
217
|
-
const newMapper = new
|
|
228
|
+
const newMapper = new Mapper();
|
|
229
|
+
for (const [key, val] of Object.entries(this)) {
|
|
230
|
+
if (key === "outputs" || key === "customProps" || key === "md") continue;
|
|
218
231
|
|
|
219
|
-
|
|
232
|
+
if (typeof val === "object" && val !== null && !Array.isArray(val)) {
|
|
233
|
+
newMapper[key] = { ...val };
|
|
234
|
+
} else {
|
|
235
|
+
newMapper[key] = val;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
newMapper.options = { ...this.options };
|
|
240
|
+
|
|
241
|
+
// Deep-clone specific structural properties
|
|
220
242
|
newMapper.outputs = this.outputs.map(out => ({
|
|
221
243
|
...out,
|
|
222
244
|
options: out.options ? { ...out.options } : { escape: true }
|
|
223
245
|
}));
|
|
224
246
|
|
|
225
|
-
newMapper.
|
|
226
|
-
|
|
227
|
-
// deep clone pageProps
|
|
228
|
-
newMapper.pageProps = JSON.parse(JSON.stringify(this.pageProps));
|
|
229
|
-
|
|
230
|
-
newMapper.extraProps = new Set(this.extraProps);
|
|
231
|
-
newMapper.setHeader([this.getCustomHeaderContent()]);
|
|
247
|
+
newMapper.customProps = new Set(this.customProps);
|
|
248
|
+
|
|
232
249
|
return newMapper;
|
|
233
250
|
}
|
|
234
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Clears all registered outputs from the mapper.
|
|
254
|
+
*/
|
|
235
255
|
clear() {
|
|
236
256
|
this.outputs = [];
|
|
237
257
|
}
|
|
238
258
|
|
|
239
|
-
|
|
240
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Static factory method to create a new Mapper instance with pre-defined properties.
|
|
261
|
+
* @param {Object} [options={}] - Properties and methods to add to the mapper.
|
|
262
|
+
* @returns {Mapper}
|
|
263
|
+
*/
|
|
264
|
+
static define(options = {}) {
|
|
265
|
+
const mapper = new Mapper();
|
|
266
|
+
for (const [key, val] of Object.entries(options)) {
|
|
267
|
+
mapper[key] = val;
|
|
268
|
+
}
|
|
269
|
+
return mapper;
|
|
241
270
|
}
|
|
242
271
|
}
|
|
243
272
|
export default Mapper;
|