sommark 3.3.4 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +98 -82
  2. package/assets/logo.json +28 -0
  3. package/assets/smark.logo.png +0 -0
  4. package/assets/smark.logo.svg +21 -0
  5. package/cli/cli.mjs +7 -17
  6. package/cli/commands/build.js +26 -6
  7. package/cli/commands/color.js +22 -26
  8. package/cli/commands/help.js +10 -10
  9. package/cli/commands/init.js +20 -31
  10. package/cli/commands/print.js +18 -16
  11. package/cli/commands/show.js +4 -0
  12. package/cli/commands/version.js +6 -0
  13. package/cli/constants.js +9 -5
  14. package/cli/helpers/config.js +11 -0
  15. package/cli/helpers/file.js +17 -6
  16. package/cli/helpers/transpile.js +15 -17
  17. package/core/errors.js +49 -25
  18. package/core/formats.js +7 -3
  19. package/core/formatter.js +215 -0
  20. package/core/helpers/config-loader.js +40 -75
  21. package/core/labels.js +21 -9
  22. package/core/lexer.js +491 -212
  23. package/core/modules.js +164 -0
  24. package/core/parser.js +516 -389
  25. package/core/tokenTypes.js +36 -1
  26. package/core/transpiler.js +238 -154
  27. package/core/validator.js +79 -0
  28. package/formatter/mark.js +203 -43
  29. package/formatter/tag.js +202 -32
  30. package/grammar.ebnf +57 -50
  31. package/helpers/colorize.js +26 -13
  32. package/helpers/dedent.js +19 -0
  33. package/helpers/escapeHTML.js +13 -6
  34. package/helpers/kebabize.js +6 -0
  35. package/helpers/peek.js +9 -0
  36. package/helpers/removeChar.js +26 -13
  37. package/helpers/safeDataParser.js +114 -0
  38. package/helpers/utils.js +140 -158
  39. package/index.js +186 -188
  40. package/mappers/languages/html.js +105 -213
  41. package/mappers/languages/json.js +122 -171
  42. package/mappers/languages/markdown.js +355 -108
  43. package/mappers/languages/mdx.js +76 -120
  44. package/mappers/languages/xml.js +114 -0
  45. package/mappers/mapper.js +152 -123
  46. package/mappers/shared/index.js +22 -0
  47. package/package.json +26 -6
  48. package/SOMMARK-SPEC.md +0 -481
  49. package/cli/commands/list.js +0 -124
  50. package/constants/html_tags.js +0 -146
  51. package/core/pluginManager.js +0 -149
  52. package/core/plugins/comment-remover.js +0 -47
  53. package/core/plugins/module-system.js +0 -176
  54. package/core/plugins/raw-content-plugin.js +0 -78
  55. package/core/plugins/rules-validation-plugin.js +0 -231
  56. package/core/plugins/sommark-format.js +0 -244
  57. package/coverage_test.js +0 -21
  58. package/debug.js +0 -15
  59. package/helpers/camelize.js +0 -2
  60. package/helpers/defaultTheme.js +0 -3
  61. package/test_format_fix.js +0 -42
  62. package/v3-todo.smark +0 -73
@@ -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 kebabize from "../../helpers/kebabize.js";
6
- import { todo, list, htmlTable } from "../../helpers/utils.js";
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
- class HtmlMapper extends Mapper {
9
- constructor() {
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.replace(/^#/, "").trim()} -->`;
14
- }
40
+ return `<!-- ${text} -->`;
41
+ },
15
42
 
16
- formatOutput(output, includeDocument) {
17
- const todoRegex = /@@TODO_BLOCK:([\s\S]*?):([\s\S]*?)@@/g;
18
- const statusMarkers = ["done", "x", "X", "-", ""];
19
- output = output.replace(todoRegex, (match, body, arg0) => {
20
- const bodyTrimmed = body.trim().toLowerCase();
21
- const arg0Trimmed = arg0.trim().toLowerCase();
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
- const bodyIsStatus = statusMarkers.includes(bodyTrimmed);
24
- const arg0IsStatus = statusMarkers.includes(arg0Trimmed);
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
- let finalStatus = arg0; // Default: arg is status
27
- let finalTask = body; // Default: body is task
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
- if (bodyIsStatus && !arg0IsStatus) {
30
- finalStatus = body;
31
- finalTask = arg0;
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
- const checked = todo(finalStatus);
35
- return this.tag("div").body(this.tag("input").attributes({ type: "checkbox", disabled: true, checked }).selfClose() + " " + (finalTask || ""));
36
- });
37
- if (includeDocument) {
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
- const HTML = new HtmlMapper();
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
- "Html",
116
+ ["Root", "root"],
63
117
  function ({ args }) {
64
- this.pageProps.pageTitle = this.safeArg(args, undefined, "title", null, null, this.pageProps.pageTitle);
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
- cssVars += `${key}:${args[key]};`;
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
- // Block
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 { TEXT } from "../../core/labels.js";
3
- import { transpilerError } from "../../core/errors.js";
4
-
5
-
6
- // ========================================================================== //
7
- // Helpers //
8
- // ========================================================================== //
9
-
10
- function escapeString(str) {
11
- return JSON.stringify(str);
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
- function processNode(node, parentType = null) {
15
- if (!node) return "";
16
-
17
- if (node.id === "Object" || node.id === "Array" || node.id === "Json") {
18
- return renderBlock(node, parentType);
19
- } else if (["string", "number", "bool", "null", "array", "none"].includes(node.id)) {
20
- return renderInline(node, parentType);
21
- } else if (node.type === TEXT) {
22
- return "";
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
- function renderBlock(node, parentType) {
28
- let output = "";
29
- let key = "";
30
- let isRoot = node.id === "Json";
31
- let type = node.id === "Array" || (isRoot && node.args.includes("array")) ? "array" : "object";
32
-
33
- // ========================================================================== //
34
- // Key //
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
- let content = children.join(",");
59
- let wrapper = type === "array" ? `[${content}]` : `{${content}}`;
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}:${wrapper}`;
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
- function renderInline(node, parentType) {
74
- let key = null;
75
- let value = "";
76
-
77
- // ========================================================================== //
78
- // Value //
79
- // ========================================================================== //
80
- if (node.id === "string") {
81
- if (!node.args || node.args.length === 0) {
82
- transpilerError([`{line}<$red:JSON Error:$> <$yellow:String inline must have a value.$>{line}`]);
83
- }
84
- value = escapeString(node.args[0] || "");
85
- } else if (node.id === "number") {
86
- if (!node.args || node.args.length === 0 || isNaN(Number(node.args[0]))) {
87
- transpilerError([`{line}<$red:JSON Error:$> <$yellow:Invalid or missing number value for inline.$>{line}`]);
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
- class JsonMapper extends Mapper {
159
- constructor() {
160
- super();
161
- }
162
-
163
- formatOutput(output) {
164
- try {
165
- return JSON.parse(JSON.stringify(output));
166
- } catch (e) {
167
- transpilerError([
168
- "{line}<$red:JSON Format Error:$> ",
169
- `<$yellow:Failed to parse generated JSON output.$>{N}<$cyan:Reason: $> <$magenta:'${e.message}'$>{line}`
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 = new JsonMapper();
177
-
178
- // ========================================================================== //
179
- // Main Registration //
180
- // ========================================================================== //
181
-
182
- const noop = () => "";
183
- Json.register(["Object", "Array"], noop, { type: "Block" });
184
- Json.register(["string", "number", "bool", "null", "array", "none"], noop, { type: "Inline" });
185
-
186
- Json.register("Json", ({ args, content, ast }) => {
187
- if (!ast) return "";
188
- return processNode(ast, null);
189
- }, { type: "Block" });
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;