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
|
@@ -1,92 +1,126 @@
|
|
|
1
1
|
import Mapper from "../mapper.js";
|
|
2
|
-
import { HTML_TAGS } from "../../constants/html_tags.js";
|
|
3
|
-
import { HTML_PROPS } from "../../constants/html_props.js";
|
|
4
2
|
import { VOID_ELEMENTS } from "../../constants/void_elements.js";
|
|
5
|
-
import
|
|
6
|
-
|
|
3
|
+
import { registerSharedOutputs } from "../shared/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper to format an HTML tag with attributes and content.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} id - The name of the HTML tag.
|
|
9
|
+
* @param {Object} args - The attributes for the tag.
|
|
10
|
+
* @param {string} content - The text or tags inside this tag.
|
|
11
|
+
* @returns {string} - The finished HTML string.
|
|
12
|
+
*/
|
|
13
|
+
const renderHtmlTag = function (id, args, content) {
|
|
14
|
+
const element = this.tag(id);
|
|
15
|
+
|
|
16
|
+
element.smartAttributes(args, this.customProps);
|
|
17
|
+
|
|
18
|
+
let finalContent = content;
|
|
19
|
+
if (id.toLowerCase() === "script" && args.scoped === true) {
|
|
20
|
+
finalContent = `(function(){\n${content}\n})();`;
|
|
21
|
+
}
|
|
7
22
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
super();
|
|
23
|
+
if (VOID_ELEMENTS.has(id.toLowerCase())) {
|
|
24
|
+
return element.selfClose();
|
|
11
25
|
}
|
|
26
|
+
|
|
27
|
+
return element.body(finalContent);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The HTML Mapper used for generating web pages.
|
|
32
|
+
*/
|
|
33
|
+
const HTML = Mapper.define({
|
|
34
|
+
/**
|
|
35
|
+
* Formats an HTML comment.
|
|
36
|
+
* @param {string} text - The text inside the comment.
|
|
37
|
+
* @returns {string} - The finished comment.
|
|
38
|
+
*/
|
|
12
39
|
comment(text) {
|
|
13
|
-
return `<!-- ${text
|
|
14
|
-
}
|
|
40
|
+
return `<!-- ${text} -->`;
|
|
41
|
+
},
|
|
15
42
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Formats plain text and makes sure it's safe for HTML if needed.
|
|
45
|
+
*/
|
|
46
|
+
text(text, options) {
|
|
47
|
+
if (options?.escape === false) return text;
|
|
48
|
+
return this.escapeHTML(text);
|
|
49
|
+
},
|
|
22
50
|
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Formats text inside inline tags (like bold or links).
|
|
53
|
+
*/
|
|
54
|
+
inlineText(text, options) {
|
|
55
|
+
if (options?.escape !== false) {
|
|
56
|
+
return this.escapeHTML(text);
|
|
57
|
+
}
|
|
58
|
+
return text;
|
|
59
|
+
},
|
|
25
60
|
|
|
26
|
-
|
|
27
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Formats the content inside AtBlocks.
|
|
63
|
+
*/
|
|
64
|
+
atBlockBody(text, options) {
|
|
65
|
+
let out = String(text);
|
|
66
|
+
if (options?.escape !== false) {
|
|
67
|
+
out = this.escapeHTML(out);
|
|
68
|
+
}
|
|
69
|
+
if (out.includes('\n')) {
|
|
70
|
+
out = '\n' + out + '\n';
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
},
|
|
28
74
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Provides high-fidelity fallback for unknown ids by rendering them as HTML elements.
|
|
77
|
+
* @param {Object} node - The unknown AST node.
|
|
78
|
+
* @returns {Object} - A virtual id registration for fallback rendering.
|
|
79
|
+
*/
|
|
80
|
+
getUnknownTag(node) {
|
|
81
|
+
const id = node.id.toLowerCase();
|
|
82
|
+
const isVoid = VOID_ELEMENTS.has(id);
|
|
83
|
+
const isCodeStyleOrScript = ["code", "style", "script"].includes(id);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
render: function ({ args, content }) { return renderHtmlTag.call(this, id, args, content); },
|
|
87
|
+
options: {
|
|
88
|
+
type: isCodeStyleOrScript ? ["Block", "AtBlock"] : ["Block", "Inline"],
|
|
89
|
+
escape: !isCodeStyleOrScript,
|
|
90
|
+
rules: { is_self_closing: isVoid }
|
|
32
91
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let finalHeader = this.header;
|
|
39
|
-
let styleContent = "";
|
|
40
|
-
const updateStyleTag = style => {
|
|
41
|
-
if (style) {
|
|
42
|
-
const styleTag = `<style>\n${style}\n</style>`;
|
|
43
|
-
if (!finalHeader.includes(styleTag)) {
|
|
44
|
-
finalHeader += styleTag + "\n";
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
styleContent = this.styles.join("\n");
|
|
51
|
-
updateStyleTag(styleContent);
|
|
52
|
-
|
|
53
|
-
return `<!DOCTYPE html>\n<html>\n${finalHeader}\n<body>\n${output}\n</body>\n</html>\n`;
|
|
54
|
-
}
|
|
55
|
-
return output;
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
options: {
|
|
96
|
+
// trimAndWrapBlocks: false // Default to false for high-fidelity
|
|
56
97
|
}
|
|
57
|
-
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// DOCTYPE tag
|
|
101
|
+
HTML.register(["DOCTYPE", "doctype"], () => {
|
|
102
|
+
return "<!DOCTYPE html>";
|
|
103
|
+
}, { type: "Block", rules: { is_self_closing: true } });
|
|
58
104
|
|
|
59
|
-
|
|
105
|
+
// head tag
|
|
106
|
+
HTML.register("head", function ({ content }) {
|
|
107
|
+
let varsStyle = "";
|
|
108
|
+
if (this.cssVariables) {
|
|
109
|
+
varsStyle = `<style>:root { ${this.cssVariables} }</style>\n`;
|
|
110
|
+
}
|
|
111
|
+
return this.tag("head").body(`${varsStyle}${content}`);
|
|
112
|
+
}, { type: "Block", escape: false });
|
|
60
113
|
|
|
114
|
+
// Root tag for Metadata and CSS Variables (Collector)
|
|
61
115
|
HTML.register(
|
|
62
|
-
"
|
|
116
|
+
["Root", "root"],
|
|
63
117
|
function ({ args }) {
|
|
64
|
-
this.
|
|
65
|
-
this.pageProps.charset = this.safeArg(args, undefined, "charset", null, null, this.pageProps.charset);
|
|
66
|
-
this.pageProps.tabIcon.src = this.safeArg(args, undefined, "iconSrc", null, null, this.pageProps.tabIcon.src);
|
|
67
|
-
this.pageProps.tabIcon.type = this.safeArg(args, undefined, "iconType", null, null, this.pageProps.tabIcon.type);
|
|
68
|
-
this.pageProps.httpEquiv["X-UA-Compatible"] = this.safeArg(
|
|
69
|
-
args,
|
|
70
|
-
undefined,
|
|
71
|
-
"httpEquiv",
|
|
72
|
-
null,
|
|
73
|
-
null,
|
|
74
|
-
this.pageProps.httpEquiv["X-UA-Compatible"]
|
|
75
|
-
);
|
|
76
|
-
this.pageProps.viewport = this.safeArg(args, undefined, "viewport", null, null, this.pageProps.viewport);
|
|
77
|
-
|
|
78
|
-
// Global CSS Variables
|
|
79
|
-
let cssVars = "";
|
|
118
|
+
this.cssVariables = this.cssVariables || "";
|
|
80
119
|
Object.keys(args).forEach(key => {
|
|
81
120
|
if (key.startsWith("--")) {
|
|
82
|
-
|
|
121
|
+
this.cssVariables += `${key}:${args[key]};`;
|
|
83
122
|
}
|
|
84
123
|
});
|
|
85
|
-
|
|
86
|
-
if (cssVars) {
|
|
87
|
-
this.addStyle(`:root { ${cssVars} }`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
124
|
return "";
|
|
91
125
|
},
|
|
92
126
|
{
|
|
@@ -94,148 +128,6 @@ HTML.register(
|
|
|
94
128
|
}
|
|
95
129
|
);
|
|
96
130
|
|
|
97
|
-
|
|
98
|
-
HTML.register(
|
|
99
|
-
"Block",
|
|
100
|
-
function ({ content }) {
|
|
101
|
-
return content;
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
type: "Block"
|
|
105
|
-
}
|
|
106
|
-
);
|
|
107
|
-
// Quote
|
|
108
|
-
HTML.register(["quote", "blockquote"], function ({ content }) {
|
|
109
|
-
return this.tag("blockquote").body(content);
|
|
110
|
-
}, { type: "Block" });
|
|
111
|
-
// Raw Content Blocks
|
|
112
|
-
HTML.register(["raw", "mdx"], function ({ content }) {
|
|
113
|
-
return content;
|
|
114
|
-
}, { type: "Block" });
|
|
115
|
-
// Bold
|
|
116
|
-
HTML.register("bold", function ({ content }) {
|
|
117
|
-
return this.tag("strong").body(content);
|
|
118
|
-
}, { type: "any" });
|
|
119
|
-
// Strike
|
|
120
|
-
HTML.register("strike", function ({ content }) {
|
|
121
|
-
return this.tag("s").body(content);
|
|
122
|
-
}, { type: "any" });
|
|
123
|
-
// Italic
|
|
124
|
-
HTML.register("italic", function ({ content }) {
|
|
125
|
-
return this.tag("i").body(content);
|
|
126
|
-
}, { type: "any" });
|
|
127
|
-
// Emphasis
|
|
128
|
-
HTML.register("emphasis", function ({ content }) {
|
|
129
|
-
return this.tag("span").attributes({ style: "font-weight:bold; font-style: italic;" }).body(content);
|
|
130
|
-
}, { type: "any" });
|
|
131
|
-
// Colored Text
|
|
132
|
-
HTML.register("color", function ({ args, content }) {
|
|
133
|
-
const color = this.safeArg(args, 0, undefined, null, null, "none");
|
|
134
|
-
return this.tag("span")
|
|
135
|
-
.attributes({ style: `color:${color}` })
|
|
136
|
-
.body(content);
|
|
137
|
-
}, { type: "any" });
|
|
138
|
-
// Code
|
|
139
|
-
HTML.register(
|
|
140
|
-
"Code",
|
|
141
|
-
function ({ args, content }) {
|
|
142
|
-
const lang = this.safeArg(args, 0, "lang", null, null, "text");
|
|
143
|
-
const code = content || "";
|
|
144
|
-
const code_element = this.tag("code");
|
|
145
|
-
|
|
146
|
-
code_element.attributes({ class: `language-${lang}` });
|
|
147
|
-
|
|
148
|
-
return this.tag("pre").body(code_element.body(code));
|
|
149
|
-
},
|
|
150
|
-
{ escape: false, type: ["AtBlock", "Block"] }
|
|
151
|
-
);
|
|
152
|
-
// List
|
|
153
|
-
HTML.register(
|
|
154
|
-
"list",
|
|
155
|
-
function ({ content }) {
|
|
156
|
-
return list(content, "ul", this.escapeHTML);
|
|
157
|
-
},
|
|
158
|
-
{ escape: false, type: "any" }
|
|
159
|
-
);
|
|
160
|
-
HTML.register(
|
|
161
|
-
"Table",
|
|
162
|
-
function ({ content, args }) {
|
|
163
|
-
return htmlTable(content.split(/\n/), args, this.escapeHTML);
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
escape: false,
|
|
167
|
-
type: "AtBlock"
|
|
168
|
-
}
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
// Todo
|
|
172
|
-
HTML.register("todo", function ({ args, content }) {
|
|
173
|
-
const isPlaceholder = content.includes("__SOMMARK_BODY_PLACEHOLDER_");
|
|
174
|
-
if (isPlaceholder) {
|
|
175
|
-
return `@@TODO_BLOCK:${content}:${args[0] || ""}@@`;
|
|
176
|
-
}
|
|
177
|
-
const statusMarkers = ["done", "x", "X", "-", ""];
|
|
178
|
-
const isInline = !isPlaceholder && statusMarkers.includes(content.trim().toLowerCase()) && args.length > 0;
|
|
179
|
-
const status = isInline ? content : (args[0] || "");
|
|
180
|
-
const label = isInline ? (args[0] || "") : content;
|
|
181
|
-
const checked = todo(status);
|
|
182
|
-
return this.tag("div").body(this.tag("input").attributes({ type: "checkbox", disabled: true, checked }).selfClose() + " " + (label || ""));
|
|
183
|
-
}, { type: "any" });
|
|
184
|
-
|
|
185
|
-
HTML_TAGS.forEach(tagName => {
|
|
186
|
-
const idsToRegister = [tagName].filter(id => {
|
|
187
|
-
const existing = HTML.get(id);
|
|
188
|
-
if (!existing || !existing.id) return true;
|
|
189
|
-
return Array.isArray(existing.id) ? !existing.id.includes(id) : existing.id !== id;
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
idsToRegister.forEach(id => {
|
|
193
|
-
const isAtBlock = ["style", "script"].includes(id.toLowerCase());
|
|
194
|
-
|
|
195
|
-
HTML.register(
|
|
196
|
-
id,
|
|
197
|
-
function ({ args, content, textContent }) {
|
|
198
|
-
const element = this.tag(id);
|
|
199
|
-
let inline_style = args.style ? (args.style.endsWith(";") ? args.style : args.style + ";") : "";
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const keys = Object.keys(args).filter(arg => isNaN(arg));
|
|
203
|
-
keys.forEach(key => {
|
|
204
|
-
if (key === "style") return; // Already handled
|
|
205
|
-
|
|
206
|
-
const isDimensionAttributeSupported = ["img", "video", "svg", "canvas", "iframe", "object", "embed"].includes(id.toLowerCase());
|
|
207
|
-
const isWidthOrHeight = key === "width" || key === "height";
|
|
208
|
-
const isEvent = key.toLowerCase().startsWith("on");
|
|
209
|
-
|
|
210
|
-
const k = isEvent ? key.toLowerCase() : (HTML_PROPS.has(key) || this.extraProps.has(key)) ? key : kebabize(key);
|
|
211
|
-
|
|
212
|
-
if (isEvent || ((HTML_PROPS.has(key) || this.extraProps.has(key)) && (!isWidthOrHeight || isDimensionAttributeSupported)) || k.startsWith("data-") || k.startsWith("aria-")) {
|
|
213
|
-
element.attributes({ [k]: args[key] });
|
|
214
|
-
} else {
|
|
215
|
-
inline_style += `${k}:${args[key]};`;
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
if (inline_style) {
|
|
220
|
-
element.attributes({ style: inline_style });
|
|
221
|
-
}
|
|
222
|
-
// Self-Closing Element
|
|
223
|
-
if (VOID_ELEMENTS.has(id.toLowerCase())) {
|
|
224
|
-
return element.selfClose();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return element.body(content);
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
type: isAtBlock ? "AtBlock" : "Block",
|
|
231
|
-
escape: !isAtBlock,
|
|
232
|
-
rules: {
|
|
233
|
-
is_self_closing: VOID_ELEMENTS.has(id.toLowerCase())
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
131
|
+
registerSharedOutputs(HTML);
|
|
240
132
|
|
|
241
133
|
export default HTML;
|
|
@@ -1,191 +1,142 @@
|
|
|
1
1
|
import Mapper from "../mapper.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import { getPositionalArgs, matchedValue, safeArg } from "../../helpers/utils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JSON Mapper - Creates JSON output.
|
|
6
|
+
* It manages the structure manually using 'handleAst: true'.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns a string representing the specified indentation level.
|
|
11
|
+
*/
|
|
12
|
+
function getIndent(depth) {
|
|
13
|
+
return " ".repeat(depth);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
return "";
|
|
16
|
+
/**
|
|
17
|
+
* Escapes a string for use in a JSON property or value.
|
|
18
|
+
* @param {string} str - The string to escape.
|
|
19
|
+
* @param {boolean} [trim=false] - Whether to trim the string.
|
|
20
|
+
*/
|
|
21
|
+
function escapeString(str, trim = false) {
|
|
22
|
+
let out = String(str);
|
|
23
|
+
if (trim) out = out.trim();
|
|
24
|
+
return JSON.stringify(out);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!isRoot) {
|
|
37
|
-
if (parentType === "object") {
|
|
38
|
-
key = node.args && node.args[0] ? escapeString(node.args[0]) : null;
|
|
39
|
-
if (!key) {
|
|
40
|
-
key = '"unknown_key"';
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ========================================================================== //
|
|
46
|
-
// Children //
|
|
47
|
-
// ========================================================================== //
|
|
48
|
-
let children = [];
|
|
49
|
-
if (node.body && node.body.length > 0) {
|
|
50
|
-
for (const child of node.body) {
|
|
51
|
-
const childOutput = processNode(child, type);
|
|
52
|
-
if (childOutput) {
|
|
53
|
-
children.push(childOutput);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
27
|
+
/**
|
|
28
|
+
* Recursively extracts text content from a node, ignoring structural metadata.
|
|
29
|
+
*/
|
|
30
|
+
function getNodeText(node) {
|
|
31
|
+
if (!node.body) return "";
|
|
32
|
+
let text = "";
|
|
33
|
+
for (const child of node.body) {
|
|
34
|
+
if (child.type === "Text") text += child.text;
|
|
35
|
+
else if (child.type === "Block") text += getNodeText(child);
|
|
56
36
|
}
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
57
39
|
|
|
58
|
-
|
|
59
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the key-value pairing for a JSON member.
|
|
42
|
+
*/
|
|
43
|
+
function renderMember(args, value) {
|
|
44
|
+
const posArgs = getPositionalArgs(args);
|
|
45
|
+
const key = args.key || posArgs[0]; // The 'key' rule determines the member name
|
|
60
46
|
|
|
61
47
|
if (key) {
|
|
62
|
-
return `${key}
|
|
63
|
-
} else {
|
|
64
|
-
if (parentType === "object") {
|
|
65
|
-
if (!node.args || !node.args[0]) {
|
|
66
|
-
transpilerError([`{line}<$red:JSON Error:$> <$yellow:Blocks inside an Object must have a key argument.$>{line}`]);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return wrapper;
|
|
48
|
+
return `${escapeString(key)}: ${value}`;
|
|
70
49
|
}
|
|
50
|
+
return value;
|
|
71
51
|
}
|
|
72
52
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
value = node.args[0];
|
|
90
|
-
} else if (node.id === "bool") {
|
|
91
|
-
if (!node.args || (node.args[0] !== "true" && node.args[0] !== "false")) {
|
|
92
|
-
transpilerError([`{line}<$red:JSON Error:$> <$yellow:Bool inline must be 'true' or 'false'.$>{line}`]);
|
|
93
|
-
}
|
|
94
|
-
value = node.args[0] === "true" ? "true" : "false";
|
|
95
|
-
} else if (node.id === "null") {
|
|
96
|
-
value = "null";
|
|
97
|
-
} else if (node.id === "array") {
|
|
98
|
-
// ========================================================================== //
|
|
99
|
-
// Inline array //
|
|
100
|
-
// ========================================================================== //
|
|
101
|
-
// (data)->(array: 1, 2, 3)
|
|
102
|
-
// args = ["1", " 2", " 3"]
|
|
103
|
-
const items = node.args.map(arg => {
|
|
104
|
-
const trimmed = arg.trim();
|
|
105
|
-
if (trimmed === "null") return "null";
|
|
106
|
-
if (trimmed === "true" || trimmed === "false") return trimmed;
|
|
107
|
-
if (!isNaN(parseFloat(trimmed)) && isFinite(trimmed)) return trimmed;
|
|
108
|
-
return escapeString(trimmed);
|
|
109
|
-
});
|
|
110
|
-
value = `[${items.join(",")}]`;
|
|
111
|
-
} else if (node.id === "none") {
|
|
112
|
-
// Special case: (-)->(none: val)
|
|
113
|
-
if (parentType === "object") {
|
|
114
|
-
transpilerError([
|
|
115
|
-
`{line}<$red:JSON Error:$> <$yellow:'none' inline is not allowed directly inside an Object. It must be inside an Array.$>{line}`
|
|
116
|
-
]);
|
|
117
|
-
return "";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// (-)->(none: 1, 2, null) -> [1, 2, null] -> args.length > 1
|
|
121
|
-
// (-)->(none: true) -> true -> args.length == 1
|
|
122
|
-
|
|
123
|
-
if (node.args.length > 1) {
|
|
124
|
-
const items = node.args.map(arg => {
|
|
125
|
-
const trimmed = arg.trim();
|
|
126
|
-
if (trimmed === "null") return "null";
|
|
127
|
-
if (trimmed === "true" || trimmed === "false") return trimmed;
|
|
128
|
-
if (!isNaN(parseFloat(trimmed)) && isFinite(trimmed)) return trimmed;
|
|
129
|
-
return escapeString(trimmed);
|
|
130
|
-
});
|
|
131
|
-
value = `[${items.join(",")}]`;
|
|
132
|
-
} else {
|
|
133
|
-
const arg = node.args[0] || "";
|
|
134
|
-
const trimmed = arg.trim();
|
|
135
|
-
if (trimmed === "null") value = "null";
|
|
136
|
-
else if (trimmed === "true" || trimmed === "false") value = trimmed;
|
|
137
|
-
else if (!isNaN(parseFloat(trimmed)) && isFinite(trimmed)) value = trimmed;
|
|
138
|
-
else value = escapeString(trimmed);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (parentType === "object") {
|
|
143
|
-
if (node.id === "none") return "";
|
|
144
|
-
|
|
145
|
-
if (!node.value) {
|
|
146
|
-
transpilerError([
|
|
147
|
-
`{line}<$red:JSON Error:$> <$yellow:Inline elements inside an Object must have an identifier (key).$>{line}`
|
|
148
|
-
]);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
key = escapeString(node.value);
|
|
152
|
-
return `${key}:${value}`;
|
|
153
|
-
} else {
|
|
154
|
-
return value;
|
|
155
|
-
}
|
|
53
|
+
/**
|
|
54
|
+
* Formats a given node and tracks its indentation.
|
|
55
|
+
*/
|
|
56
|
+
async function renderNode(node, mapper, depth = 0) {
|
|
57
|
+
const target = matchedValue(mapper.outputs, node.id) || mapper.getUnknownTag(node);
|
|
58
|
+
if (!target) return "";
|
|
59
|
+
|
|
60
|
+
const textContent = getNodeText(node);
|
|
61
|
+
return await target.render.call(mapper, {
|
|
62
|
+
nodeType: node.type,
|
|
63
|
+
args: node.args,
|
|
64
|
+
content: "",
|
|
65
|
+
textContent,
|
|
66
|
+
ast: node,
|
|
67
|
+
depth
|
|
68
|
+
});
|
|
156
69
|
}
|
|
157
70
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return output;
|
|
71
|
+
/**
|
|
72
|
+
* Formats the children of a node into a neat list.
|
|
73
|
+
*/
|
|
74
|
+
async function renderChildren(node, mapper, depth = 0) {
|
|
75
|
+
let results = [];
|
|
76
|
+
const childIndent = getIndent(depth + 1);
|
|
77
|
+
|
|
78
|
+
for (const child of node.body) {
|
|
79
|
+
if (child.type === "Block") {
|
|
80
|
+
const output = await renderNode(child, mapper, depth + 1);
|
|
81
|
+
if (output) {
|
|
82
|
+
results.push(childIndent + output);
|
|
83
|
+
}
|
|
172
84
|
}
|
|
173
85
|
}
|
|
86
|
+
return results.join(",\n");
|
|
174
87
|
}
|
|
175
88
|
|
|
176
|
-
const Json =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
89
|
+
const Json = Mapper.define({});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The JSON object node rule.
|
|
93
|
+
*/
|
|
94
|
+
Json.register(["Object", "object"], async ({ args, ast, depth = 0 }) => {
|
|
95
|
+
if (ast.body.length === 0) return renderMember(args, "{}");
|
|
96
|
+
const content = await renderChildren(ast, Json, depth);
|
|
97
|
+
const val = `{\n${content}\n${getIndent(depth)}}`;
|
|
98
|
+
return renderMember(args, val);
|
|
99
|
+
}, { type: "Block", handleAst: true });
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The JSON array node rule.
|
|
103
|
+
*/
|
|
104
|
+
Json.register(["Array", "array"], async ({ args, ast, depth = 0 }) => {
|
|
105
|
+
if (ast.body.length === 0) return renderMember(args, "[]");
|
|
106
|
+
const content = await renderChildren(ast, Json, depth);
|
|
107
|
+
const val = `[\n${content}\n${getIndent(depth)}]`;
|
|
108
|
+
return renderMember(args, val);
|
|
109
|
+
}, { type: "Block", handleAst: true });
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* JSON Primitives
|
|
113
|
+
*/
|
|
114
|
+
Json.register("string", ({ args, textContent }) => {
|
|
115
|
+
const trim = safeArg({
|
|
116
|
+
args,
|
|
117
|
+
key: "trim",
|
|
118
|
+
type: "boolean",
|
|
119
|
+
setType: v => v === "true" || v === true,
|
|
120
|
+
fallBack: false
|
|
121
|
+
});
|
|
122
|
+
const val = escapeString(textContent, trim);
|
|
123
|
+
return renderMember(args, val);
|
|
124
|
+
}, { type: "Block", handleAst: true });
|
|
125
|
+
|
|
126
|
+
Json.register("number", ({ args, textContent }) => {
|
|
127
|
+
const raw = textContent.trim();
|
|
128
|
+
const val = (isNaN(Number(raw)) || raw === "") ? "0" : raw;
|
|
129
|
+
return renderMember(args, val);
|
|
130
|
+
}, { type: "Block", handleAst: true });
|
|
131
|
+
|
|
132
|
+
Json.register("bool", ({ args, textContent }) => {
|
|
133
|
+
const raw = textContent.trim().toLowerCase();
|
|
134
|
+
const val = (raw === "true" || raw === "1") ? "true" : "false";
|
|
135
|
+
return renderMember(args, val);
|
|
136
|
+
}, { type: "Block", handleAst: true });
|
|
137
|
+
|
|
138
|
+
Json.register("null", ({ args }) => {
|
|
139
|
+
return renderMember(args, "null");
|
|
140
|
+
}, { type: "Block", handleAst: true });
|
|
190
141
|
|
|
191
142
|
export default Json;
|