sommark 2.1.2 → 2.2.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/CHANGELOG.md +10 -0
- package/README.md +3 -299
- package/cli/cli.mjs +15 -1
- package/cli/commands/help.js +2 -0
- package/cli/commands/init.js +64 -0
- package/cli/commands/show.js +46 -0
- package/cli/commands/version.js +1 -1
- package/cli/helpers/config.js +14 -5
- package/core/formats.js +3 -2
- package/core/lexer.js +20 -13
- package/core/parser.js +18 -9
- package/core/transpiler.js +34 -20
- package/formatter/mark.js +3 -6
- package/grammar.ebnf +0 -1
- package/index.js +6 -4
- package/mappers/languages/html.js +71 -26
- package/mappers/languages/json.js +172 -0
- package/mappers/languages/markdown.js +79 -31
- package/mappers/mapper.js +55 -9
- package/package.json +2 -5
- package/lib/highlight.js +0 -11
package/core/parser.js
CHANGED
|
@@ -102,7 +102,6 @@ const updateData = (tokens, i) => {
|
|
|
102
102
|
}
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
-
|
|
106
105
|
const errorMessage = (tokens, i, expectedValue, behindValue, frontText) => {
|
|
107
106
|
const tokensUntilError = tokens.slice(0, i);
|
|
108
107
|
const contextText = tokensUntilError.map(t => t.value).join("");
|
|
@@ -123,7 +122,6 @@ const errorMessage = (tokens, i, expectedValue, behindValue, frontText) => {
|
|
|
123
122
|
// ========================================================================== //
|
|
124
123
|
function parseKey(tokens, i) {
|
|
125
124
|
let key = current_token(tokens, i).value.trim();
|
|
126
|
-
validateName(key);
|
|
127
125
|
// ========================================================================== //
|
|
128
126
|
// consume Key //
|
|
129
127
|
// ========================================================================== //
|
|
@@ -261,7 +259,12 @@ function parseBlock(tokens, i) {
|
|
|
261
259
|
let [key, keyIndex] = parseKey(tokens, i);
|
|
262
260
|
k = key;
|
|
263
261
|
i = keyIndex;
|
|
262
|
+
const prev = current_token(tokens, i);
|
|
264
263
|
i = parseColon(tokens, i, block_id);
|
|
264
|
+
if (current_token(tokens, i).type !== TOKEN_TYPES.VALUE && current_token(tokens, i).type !== TOKEN_TYPES.ESCAPE) {
|
|
265
|
+
parserError(errorMessage(tokens, i, block_value, ":"));
|
|
266
|
+
}
|
|
267
|
+
validateName(k);
|
|
265
268
|
continue;
|
|
266
269
|
} else if (
|
|
267
270
|
current_token(tokens, i) &&
|
|
@@ -403,6 +406,7 @@ function parseInline(tokens, i) {
|
|
|
403
406
|
parserError(errorMessage(tokens, i, inline_value, "("));
|
|
404
407
|
}
|
|
405
408
|
inlineNode.value = current_token(tokens, i).value;
|
|
409
|
+
inlineNode.depth = current_token(tokens, i).depth;
|
|
406
410
|
// ========================================================================== //
|
|
407
411
|
// consume Inline Value //
|
|
408
412
|
// ========================================================================== //
|
|
@@ -467,11 +471,12 @@ function parseInline(tokens, i) {
|
|
|
467
471
|
parserError(errorMessage(tokens, i, inline_value, ":"));
|
|
468
472
|
}
|
|
469
473
|
let v = "";
|
|
470
|
-
|
|
471
474
|
const pushArg = () => {
|
|
472
475
|
if (v !== "") {
|
|
473
476
|
inlineNode.args.push(v);
|
|
474
|
-
|
|
477
|
+
if (!Number.isInteger(Number(v))) {
|
|
478
|
+
inlineNode.args[v] = v;
|
|
479
|
+
}
|
|
475
480
|
v = "";
|
|
476
481
|
}
|
|
477
482
|
};
|
|
@@ -519,11 +524,11 @@ function parseInline(tokens, i) {
|
|
|
519
524
|
if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.COMMA) {
|
|
520
525
|
parserError(errorMessage(tokens, i, ",", "", "Found extra"));
|
|
521
526
|
}
|
|
522
|
-
if (
|
|
523
|
-
current_token(tokens, i)
|
|
524
|
-
current_token(tokens, i)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
+
if (
|
|
528
|
+
!current_token(tokens, i) ||
|
|
529
|
+
(current_token(tokens, i) &&
|
|
530
|
+
current_token(tokens, i).type !== TOKEN_TYPES.VALUE &&
|
|
531
|
+
current_token(tokens, i).type !== TOKEN_TYPES.ESCAPE)
|
|
527
532
|
) {
|
|
528
533
|
parserError(errorMessage(tokens, i, inline_value, ","));
|
|
529
534
|
}
|
|
@@ -649,6 +654,10 @@ function parseAtBlock(tokens, i) {
|
|
|
649
654
|
k = key;
|
|
650
655
|
i = keyIndex;
|
|
651
656
|
i = parseColon(tokens, i, at_id);
|
|
657
|
+
if (current_token(tokens, i).type !== TOKEN_TYPES.VALUE && current_token(tokens, i).type !== TOKEN_TYPES.ESCAPE) {
|
|
658
|
+
parserError(errorMessage(tokens, i, at_value, ":"));
|
|
659
|
+
}
|
|
660
|
+
validateName(k);
|
|
652
661
|
continue;
|
|
653
662
|
} else if (current_token(tokens, i) && current_token(tokens, i).type === TOKEN_TYPES.ESCAPE) {
|
|
654
663
|
let [escape_character, escapeIndex] = parseEscape(tokens, i);
|
package/core/transpiler.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { BLOCK, TEXT, INLINE, ATBLOCK, COMMENT } from "./labels.js";
|
|
2
2
|
import escapeHTML from "../helpers/escapeHTML.js";
|
|
3
3
|
import { transpilerError } from "./errors.js";
|
|
4
|
-
import { textFormat, htmlFormat, markdownFormat, mdxFormat } from "./formats.js";
|
|
4
|
+
import { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat } from "./formats.js";
|
|
5
|
+
|
|
6
|
+
const expose_for_fmts = [jsonFormat];
|
|
5
7
|
|
|
6
8
|
// ========================================================================== //
|
|
7
9
|
// Extracting target identifier //
|
|
@@ -26,7 +28,7 @@ function matchedValue(outputs, targetId) {
|
|
|
26
28
|
return result;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
function validateRules(target, args, content) {
|
|
31
|
+
function validateRules(target, args, content, type = null) {
|
|
30
32
|
if (!target || !target.options || !target.options.rules) {
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
@@ -96,6 +98,15 @@ function validateRules(target, args, content) {
|
|
|
96
98
|
]);
|
|
97
99
|
}
|
|
98
100
|
}
|
|
101
|
+
// Validate element type
|
|
102
|
+
if (id && rules.type && type) {
|
|
103
|
+
if (rules.type !== type) {
|
|
104
|
+
transpilerError([
|
|
105
|
+
"{line}<$red:Validation Error:$> ",
|
|
106
|
+
`<$yellow:Identifier$> <$blue:'${Array.isArray(id) ? id.join(" | ") : id}'$> <$yellow:is expected to be type$> <$green:'${rules.type}'$>{N}<$cyan:Received type: $> <$magenta:'${type}'$>{line}`
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
99
110
|
}
|
|
100
111
|
// ========================================================================== //
|
|
101
112
|
// +++++++++++++++++++++++++++++ //
|
|
@@ -104,14 +115,16 @@ async function generateOutput(ast, i, format, mapper_file) {
|
|
|
104
115
|
const node = Array.isArray(ast) ? ast[i] : ast;
|
|
105
116
|
let result = "";
|
|
106
117
|
let context = "";
|
|
107
|
-
let target = mapper_file ? matchedValue(mapper_file.outputs, node.id) :
|
|
118
|
+
let target = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
|
|
119
|
+
const block_formats = [htmlFormat, mdxFormat, jsonFormat];
|
|
108
120
|
if (target) {
|
|
109
|
-
validateRules(target, node.args, "");
|
|
110
|
-
result +=
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
for (
|
|
121
|
+
validateRules(target, node.args, "", node.type);
|
|
122
|
+
result += block_formats.includes(format)
|
|
123
|
+
? `${target.render({ args: node.args, content: "<%smark>", ast: expose_for_fmts.includes(format) ? ast[i] : null })}`
|
|
124
|
+
: target.render({ args: node.args, content: "" });
|
|
125
|
+
// Body nodes
|
|
126
|
+
for (let j = 0; j < node.body.length; j++) {
|
|
127
|
+
const body_node = node.body[j];
|
|
115
128
|
switch (body_node.type) {
|
|
116
129
|
// ========================================================================== //
|
|
117
130
|
// Text //
|
|
@@ -119,8 +132,7 @@ async function generateOutput(ast, i, format, mapper_file) {
|
|
|
119
132
|
case TEXT:
|
|
120
133
|
validateRules(target, body_node.args, body_node.text);
|
|
121
134
|
const shouldEscape = target && target.options && target.options.escape === false ? false : true;
|
|
122
|
-
context +=
|
|
123
|
-
(format === htmlFormat || format === mdxFormat) && shouldEscape ? escapeHTML(body_node.text) : body_node.text;
|
|
135
|
+
context += [htmlFormat, mdxFormat].includes(format) && shouldEscape ? escapeHTML(body_node.text) : body_node.text;
|
|
124
136
|
break;
|
|
125
137
|
// ========================================================================== //
|
|
126
138
|
// Inline //
|
|
@@ -128,13 +140,11 @@ async function generateOutput(ast, i, format, mapper_file) {
|
|
|
128
140
|
case INLINE:
|
|
129
141
|
target = matchedValue(mapper_file.outputs, body_node.id);
|
|
130
142
|
if (target) {
|
|
131
|
-
validateRules(target, body_node.args, body_node.value);
|
|
132
|
-
context +=
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
content: format === htmlFormat || format === mdxFormat ? escapeHTML(body_node.value) : body_node.value
|
|
137
|
-
});
|
|
143
|
+
validateRules(target, body_node.args, body_node.value, body_node.type);
|
|
144
|
+
context += target.render({
|
|
145
|
+
args: body_node.args.length > 0 ? body_node.args : [],
|
|
146
|
+
content: format === htmlFormat || format === mdxFormat ? escapeHTML(body_node.value) : body_node.value
|
|
147
|
+
});
|
|
138
148
|
}
|
|
139
149
|
break;
|
|
140
150
|
// ========================================================================== //
|
|
@@ -143,7 +153,7 @@ async function generateOutput(ast, i, format, mapper_file) {
|
|
|
143
153
|
case ATBLOCK:
|
|
144
154
|
target = matchedValue(mapper_file.outputs, body_node.id);
|
|
145
155
|
if (target) {
|
|
146
|
-
validateRules(target, body_node.args, body_node.content);
|
|
156
|
+
validateRules(target, body_node.args, body_node.content, body_node.type);
|
|
147
157
|
// Escape logic: fallback to options.escape, default true
|
|
148
158
|
const shouldEscape = target.options?.escape ?? true;
|
|
149
159
|
if (shouldEscape) {
|
|
@@ -171,7 +181,8 @@ async function generateOutput(ast, i, format, mapper_file) {
|
|
|
171
181
|
break;
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
|
-
|
|
184
|
+
|
|
185
|
+
if (format === htmlFormat || format === mdxFormat || format === jsonFormat) {
|
|
175
186
|
result = result.replace("<%smark>", context);
|
|
176
187
|
} else {
|
|
177
188
|
result += context;
|
|
@@ -245,6 +256,9 @@ async function transpiler({ ast, format, mapperFile, includeDocument = true }) {
|
|
|
245
256
|
const document = `<!DOCTYPE html>\n<html>\n${finalHeader}\n<body>\n${output}\n</body>\n</html>\n`;
|
|
246
257
|
return document;
|
|
247
258
|
}
|
|
259
|
+
if (format === jsonFormat) {
|
|
260
|
+
output = JSON.parse(JSON.stringify(output));
|
|
261
|
+
}
|
|
248
262
|
return output;
|
|
249
263
|
}
|
|
250
264
|
|
package/formatter/mark.js
CHANGED
|
@@ -18,7 +18,7 @@ class MarkdownBuilder {
|
|
|
18
18
|
} else if (level < min) {
|
|
19
19
|
level = min;
|
|
20
20
|
}
|
|
21
|
-
return
|
|
21
|
+
return `\n${"#".repeat(level)} ${text}\n`;
|
|
22
22
|
}
|
|
23
23
|
return text;
|
|
24
24
|
}
|
|
@@ -29,7 +29,7 @@ class MarkdownBuilder {
|
|
|
29
29
|
if (!text && !url) {
|
|
30
30
|
return "";
|
|
31
31
|
}
|
|
32
|
-
return ` ${type === "image" ? "!" : ""}[${text}](${url + (title ? " " : "")}${title}) `;
|
|
32
|
+
return ` ${type === "image" ? "!" : ""}[${text}](${url + (title ? " " : "")}${title ? JSON.stringify(title) : ""}) `;
|
|
33
33
|
}
|
|
34
34
|
// ========================================================================== //
|
|
35
35
|
// Bold //
|
|
@@ -91,10 +91,7 @@ class MarkdownBuilder {
|
|
|
91
91
|
// Horizontal rule //
|
|
92
92
|
// ========================================================================== //
|
|
93
93
|
horizontal(format = "*") {
|
|
94
|
-
|
|
95
|
-
return "\n***\n";
|
|
96
|
-
}
|
|
97
|
-
return format === "*" ? "\n***\n" : format === "_" ? "___" : format === "*" ? "***" : "";
|
|
94
|
+
return `\n${format.repeat(3)}\n`;
|
|
98
95
|
}
|
|
99
96
|
// ========================================================================== //
|
|
100
97
|
// Escape //
|
package/grammar.ebnf
CHANGED
|
@@ -12,7 +12,6 @@ EscapeChar = "\";
|
|
|
12
12
|
(* 1. Identifiers can be only letters and numbers. *)
|
|
13
13
|
(* 2. Semi-colon is only recognized in Atblock arguments. *)
|
|
14
14
|
(* 3. Colon is a separator in Block and Atblock arguments; must be escaped if part of value. *)
|
|
15
|
-
(* 4. Bodies and Values cannot be empty. *)
|
|
16
15
|
|
|
17
16
|
(* Comments *)
|
|
18
17
|
Comment = "#", { ? any character except "\n" ? }, "\n";
|
package/index.js
CHANGED
|
@@ -5,10 +5,11 @@ import Mapper from "./mappers/mapper.js";
|
|
|
5
5
|
import HTML from "./mappers/languages/html.js";
|
|
6
6
|
import MARKDOWN from "./mappers/languages/markdown.js";
|
|
7
7
|
import MDX from "./mappers/languages/mdx.js";
|
|
8
|
+
import Json from "./mappers/languages/json.js";
|
|
8
9
|
import TagBuilder from "./formatter/tag.js";
|
|
9
10
|
import MarkdownBuilder from "./formatter/mark.js";
|
|
10
11
|
import { runtimeError } from "./core/errors.js";
|
|
11
|
-
import FORMATS, { textFormat, htmlFormat, markdownFormat, mdxFormat } from "./core/formats.js";
|
|
12
|
+
import FORMATS, { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat } from "./core/formats.js";
|
|
12
13
|
import TOKEN_TYPES from "./core/tokenTypes.js";
|
|
13
14
|
import * as labels from "./core/labels.js";
|
|
14
15
|
class SomMark {
|
|
@@ -19,7 +20,7 @@ class SomMark {
|
|
|
19
20
|
|
|
20
21
|
this.Mapper = Mapper;
|
|
21
22
|
this.includeDocument = includeDocument;
|
|
22
|
-
const accepted_formats = [textFormat, htmlFormat, markdownFormat, mdxFormat];
|
|
23
|
+
const accepted_formats = [textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat];
|
|
23
24
|
if (!this.format) {
|
|
24
25
|
runtimeError(["{line}<$red:Undefined Format$>: <$yellow:Format argument is not defined.$>{line}"]);
|
|
25
26
|
}
|
|
@@ -29,7 +30,7 @@ class SomMark {
|
|
|
29
30
|
`{N}<$yellow:Accepted formats are:$> [<$cyan: ${accepted_formats.join(", ")}$>]{line}`
|
|
30
31
|
]);
|
|
31
32
|
}
|
|
32
|
-
const mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [mdxFormat]: MDX };
|
|
33
|
+
const mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [mdxFormat]: MDX, [jsonFormat]: Json};
|
|
33
34
|
if (!this.mapperFile && this.format) {
|
|
34
35
|
this.mapperFile = mapperFiles[this.format];
|
|
35
36
|
}
|
|
@@ -95,7 +96,8 @@ async function transpile(options = {}) {
|
|
|
95
96
|
export {
|
|
96
97
|
HTML,
|
|
97
98
|
MARKDOWN,
|
|
98
|
-
|
|
99
|
+
MDX,
|
|
100
|
+
Json,
|
|
99
101
|
Mapper,
|
|
100
102
|
TagBuilder,
|
|
101
103
|
MarkdownBuilder,
|
|
@@ -1,15 +1,56 @@
|
|
|
1
1
|
import Mapper from "../mapper.js";
|
|
2
2
|
const HTML = new Mapper();
|
|
3
|
-
const { tag, code, list } = HTML;
|
|
3
|
+
const { tag, code, list, safeArg } = HTML;
|
|
4
|
+
|
|
5
|
+
HTML.register(
|
|
6
|
+
["Html", "html"],
|
|
7
|
+
({ args }) => {
|
|
8
|
+
HTML.pageProps.pageTitle = safeArg(args, undefined, "title", null, null, HTML.pageProps.pageTitle);
|
|
9
|
+
HTML.pageProps.charset = safeArg(args, undefined, "charset", null, null, HTML.pageProps.charset);
|
|
10
|
+
HTML.pageProps.tabIcon.src = safeArg(args, undefined, "iconSrc", null, null, HTML.pageProps.tabIcon.src);
|
|
11
|
+
HTML.pageProps.tabIcon.type = safeArg(args, undefined, "iconType", null, null, HTML.pageProps.tabIcon.type);
|
|
12
|
+
HTML.pageProps.httpEquiv["X-UA-Compatible"] = safeArg(
|
|
13
|
+
args,
|
|
14
|
+
undefined,
|
|
15
|
+
"httpEquiv",
|
|
16
|
+
null,
|
|
17
|
+
null,
|
|
18
|
+
HTML.pageProps.httpEquiv["X-UA-Compatible"]
|
|
19
|
+
);
|
|
20
|
+
HTML.pageProps.viewport = safeArg(args, undefined, "viewport", null, null, HTML.pageProps.viewport);
|
|
21
|
+
return "";
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
rules: {
|
|
25
|
+
type: "Block"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
);
|
|
4
29
|
|
|
5
30
|
// Block
|
|
6
|
-
HTML.register(
|
|
7
|
-
|
|
8
|
-
})
|
|
31
|
+
HTML.register(
|
|
32
|
+
["Block", "block"],
|
|
33
|
+
({ content }) => {
|
|
34
|
+
return content;
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
rules: {
|
|
38
|
+
type: "Block"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
);
|
|
9
42
|
// Section
|
|
10
|
-
HTML.register(
|
|
11
|
-
|
|
12
|
-
})
|
|
43
|
+
HTML.register(
|
|
44
|
+
["Section", "section"],
|
|
45
|
+
({ content }) => {
|
|
46
|
+
return tag("section").body(content);
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
rules: {
|
|
50
|
+
type: "Block"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
);
|
|
13
54
|
// Headings
|
|
14
55
|
["h1", "h2", "h3", "h4", "h5", "h6"].forEach(heading => {
|
|
15
56
|
HTML.register(heading, ({ content }) => {
|
|
@@ -17,44 +58,44 @@ HTML.register("Section", ({ content }) => {
|
|
|
17
58
|
});
|
|
18
59
|
});
|
|
19
60
|
// Bold
|
|
20
|
-
HTML.register(["bold", "b"], ({ content }) => {
|
|
61
|
+
HTML.register(["bold", "Bold", "b"], ({ content }) => {
|
|
21
62
|
return tag("strong").body(content);
|
|
22
63
|
});
|
|
23
64
|
// Italic
|
|
24
65
|
HTML.register(["italic", "i"], ({ content }) => {
|
|
25
66
|
return tag("i").body(content);
|
|
26
67
|
});
|
|
27
|
-
//
|
|
68
|
+
// Emphasis
|
|
28
69
|
HTML.register(["emphasis", "e"], ({ content }) => {
|
|
29
70
|
return tag("span").attributes({ style: "font-weight:bold; font-style: italic;" }).body(content);
|
|
30
71
|
});
|
|
31
72
|
// Colored Text
|
|
32
|
-
HTML.register("color", ({ args, content }) => {
|
|
73
|
+
HTML.register(["color", "Color"], ({ args, content }) => {
|
|
74
|
+
const color = safeArg(args, 0, undefined, null, null, "none");
|
|
33
75
|
return tag("span")
|
|
34
|
-
.attributes({ style: `color:${
|
|
76
|
+
.attributes({ style: `color:${color}` })
|
|
35
77
|
.body(content);
|
|
36
78
|
});
|
|
37
79
|
// Link
|
|
38
|
-
HTML.register("link", ({ args, content }) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
80
|
+
HTML.register(["link", "Link"], ({ args, content }) => {
|
|
81
|
+
const url = safeArg(args, 0, "url", null, null, "");
|
|
82
|
+
const title = safeArg(args, 1, "title", null, null, "");
|
|
83
|
+
return tag("a").attributes({ href: url.trim(), title: title.trim(), target: "_blank" }).body(content);
|
|
42
84
|
});
|
|
43
85
|
// Image
|
|
44
86
|
HTML.register(
|
|
45
87
|
["image", "Image"],
|
|
46
88
|
({ args }) => {
|
|
47
|
-
const src = args
|
|
48
|
-
const alt = args
|
|
49
|
-
const width = args
|
|
50
|
-
|
|
89
|
+
const src = safeArg(args, undefined, "src", null, null, "");
|
|
90
|
+
const alt = safeArg(args, undefined, "alt", null, null, "");
|
|
91
|
+
const width = safeArg(args, undefined, "width", null, null, "");
|
|
92
|
+
const height = safeArg(args, undefined, "height", null, null, "");
|
|
51
93
|
return tag("img").attributes({ src, alt, width, height }).selfClose();
|
|
52
94
|
},
|
|
53
95
|
{
|
|
54
96
|
rules: {
|
|
55
|
-
is_Self_closing: true,
|
|
56
97
|
args: {
|
|
57
|
-
|
|
98
|
+
required: ["src"]
|
|
58
99
|
}
|
|
59
100
|
}
|
|
60
101
|
}
|
|
@@ -65,7 +106,7 @@ HTML.register(
|
|
|
65
106
|
({ args, content }) => {
|
|
66
107
|
return code(args, content);
|
|
67
108
|
},
|
|
68
|
-
{ escape: false }
|
|
109
|
+
{ escape: false, rules: { type: "AtBlock" } }
|
|
69
110
|
);
|
|
70
111
|
// List
|
|
71
112
|
HTML.register(
|
|
@@ -81,7 +122,12 @@ HTML.register(
|
|
|
81
122
|
({ content, args }) => {
|
|
82
123
|
return HTML.htmlTable(content.split(/\n/), args);
|
|
83
124
|
},
|
|
84
|
-
{
|
|
125
|
+
{
|
|
126
|
+
escape: false,
|
|
127
|
+
rules: {
|
|
128
|
+
type: "AtBlock"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
85
131
|
);
|
|
86
132
|
// Horizontal Rule
|
|
87
133
|
HTML.register(
|
|
@@ -98,9 +144,8 @@ HTML.register(
|
|
|
98
144
|
// Todo
|
|
99
145
|
HTML.register("todo", ({ args, content }) => {
|
|
100
146
|
const checked = HTML.todo(content);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
147
|
+
const task = safeArg(args, 0, undefined, null, null, "");
|
|
148
|
+
return tag("div").body(tag("input").attributes({ type: "checkbox", disabled: true, checked }).selfClose() + task);
|
|
104
149
|
});
|
|
105
150
|
|
|
106
151
|
export default HTML;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import Mapper from "../mapper.js";
|
|
2
|
+
import { TEXT } from "../../core/labels.js";
|
|
3
|
+
import { transpilerError } from "../../core/errors.js";
|
|
4
|
+
|
|
5
|
+
const Json = new Mapper();
|
|
6
|
+
|
|
7
|
+
// ========================================================================== //
|
|
8
|
+
// Helpers //
|
|
9
|
+
// ========================================================================== //
|
|
10
|
+
|
|
11
|
+
function escapeString(str) {
|
|
12
|
+
return JSON.stringify(str);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function processNode(node, parentType = null) {
|
|
16
|
+
if (!node) return "";
|
|
17
|
+
|
|
18
|
+
if (node.id === "Object" || node.id === "Array" || node.id === "Json") {
|
|
19
|
+
return renderBlock(node, parentType);
|
|
20
|
+
} else if (["string", "number", "bool", "null", "array", "none"].includes(node.id)) {
|
|
21
|
+
return renderInline(node, parentType);
|
|
22
|
+
} else if (node.type === TEXT) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderBlock(node, parentType) {
|
|
29
|
+
let output = "";
|
|
30
|
+
let key = "";
|
|
31
|
+
let isRoot = node.id === "Json";
|
|
32
|
+
let type = node.id === "Array" || (isRoot && node.args.includes("array")) ? "array" : "object";
|
|
33
|
+
|
|
34
|
+
// ========================================================================== //
|
|
35
|
+
// Key //
|
|
36
|
+
// ========================================================================== //
|
|
37
|
+
if (!isRoot) {
|
|
38
|
+
if (parentType === "object") {
|
|
39
|
+
key = node.args && node.args[0] ? escapeString(node.args[0]) : null;
|
|
40
|
+
if (!key) {
|
|
41
|
+
key = '"unknown_key"';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ========================================================================== //
|
|
47
|
+
// Children //
|
|
48
|
+
// ========================================================================== //
|
|
49
|
+
let children = [];
|
|
50
|
+
if (node.body && node.body.length > 0) {
|
|
51
|
+
for (const child of node.body) {
|
|
52
|
+
const childOutput = processNode(child, type);
|
|
53
|
+
if (childOutput) {
|
|
54
|
+
children.push(childOutput);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let content = children.join(",");
|
|
60
|
+
let wrapper = type === "array" ? `[${content}]` : `{${content}}`;
|
|
61
|
+
|
|
62
|
+
if (key) {
|
|
63
|
+
return `${key}:${wrapper}`;
|
|
64
|
+
} else {
|
|
65
|
+
if (parentType === "object") {
|
|
66
|
+
if (!node.args || !node.args[0]) {
|
|
67
|
+
transpilerError([`{line}<$red:JSON Error:$> <$yellow:Blocks inside an Object must have a key argument.$>{line}`]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return wrapper;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderInline(node, parentType) {
|
|
75
|
+
let key = null;
|
|
76
|
+
let value = "";
|
|
77
|
+
|
|
78
|
+
// ========================================================================== //
|
|
79
|
+
// Value //
|
|
80
|
+
// ========================================================================== //
|
|
81
|
+
if (node.id === "string") {
|
|
82
|
+
if (!node.args || node.args.length === 0) {
|
|
83
|
+
transpilerError([`{line}<$red:JSON Error:$> <$yellow:String inline must have a value.$>{line}`]);
|
|
84
|
+
}
|
|
85
|
+
value = escapeString(node.args[0] || "");
|
|
86
|
+
} else if (node.id === "number") {
|
|
87
|
+
if (!node.args || node.args.length === 0 || isNaN(Number(node.args[0]))) {
|
|
88
|
+
transpilerError([`{line}<$red:JSON Error:$> <$yellow:Invalid or missing number value for inline.$>{line}`]);
|
|
89
|
+
}
|
|
90
|
+
value = node.args[0];
|
|
91
|
+
} else if (node.id === "bool") {
|
|
92
|
+
if (!node.args || (node.args[0] !== "true" && node.args[0] !== "false")) {
|
|
93
|
+
transpilerError([`{line}<$red:JSON Error:$> <$yellow:Bool inline must be 'true' or 'false'.$>{line}`]);
|
|
94
|
+
}
|
|
95
|
+
value = node.args[0] === "true" ? "true" : "false";
|
|
96
|
+
} else if (node.id === "null") {
|
|
97
|
+
value = "null";
|
|
98
|
+
} else if (node.id === "array") {
|
|
99
|
+
// ========================================================================== //
|
|
100
|
+
// Inline array //
|
|
101
|
+
// ========================================================================== //
|
|
102
|
+
// (data)->(array: 1, 2, 3)
|
|
103
|
+
// args = ["1", " 2", " 3"]
|
|
104
|
+
const items = node.args.map(arg => {
|
|
105
|
+
const trimmed = arg.trim();
|
|
106
|
+
if (trimmed === "null") return "null";
|
|
107
|
+
if (trimmed === "true" || trimmed === "false") return trimmed;
|
|
108
|
+
if (!isNaN(parseFloat(trimmed)) && isFinite(trimmed)) return trimmed;
|
|
109
|
+
return escapeString(trimmed);
|
|
110
|
+
});
|
|
111
|
+
value = `[${items.join(",")}]`;
|
|
112
|
+
} else if (node.id === "none") {
|
|
113
|
+
// Special case: (-)->(none: val)
|
|
114
|
+
if (parentType === "object") {
|
|
115
|
+
transpilerError([
|
|
116
|
+
`{line}<$red:JSON Error:$> <$yellow:'none' inline is not allowed directly inside an Object. It must be inside an Array.$>{line}`
|
|
117
|
+
]);
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// (-)->(none: 1, 2, null) -> [1, 2, null] -> args.length > 1
|
|
122
|
+
// (-)->(none: true) -> true -> args.length == 1
|
|
123
|
+
|
|
124
|
+
if (node.args.length > 1) {
|
|
125
|
+
const items = node.args.map(arg => {
|
|
126
|
+
const trimmed = arg.trim();
|
|
127
|
+
if (trimmed === "null") return "null";
|
|
128
|
+
if (trimmed === "true" || trimmed === "false") return trimmed;
|
|
129
|
+
if (!isNaN(parseFloat(trimmed)) && isFinite(trimmed)) return trimmed;
|
|
130
|
+
return escapeString(trimmed);
|
|
131
|
+
});
|
|
132
|
+
value = `[${items.join(",")}]`;
|
|
133
|
+
} else {
|
|
134
|
+
const arg = node.args[0] || "";
|
|
135
|
+
const trimmed = arg.trim();
|
|
136
|
+
if (trimmed === "null") value = "null";
|
|
137
|
+
else if (trimmed === "true" || trimmed === "false") value = trimmed;
|
|
138
|
+
else if (!isNaN(parseFloat(trimmed)) && isFinite(trimmed)) value = trimmed;
|
|
139
|
+
else value = escapeString(trimmed);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (parentType === "object") {
|
|
144
|
+
if (node.id === "none") return "";
|
|
145
|
+
|
|
146
|
+
if (!node.value) {
|
|
147
|
+
transpilerError([
|
|
148
|
+
`{line}<$red:JSON Error:$> <$yellow:Inline elements inside an Object must have an identifier (key).$>{line}`
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
key = escapeString(node.value);
|
|
153
|
+
return `${key}:${value}`;
|
|
154
|
+
} else {
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ========================================================================== //
|
|
160
|
+
// Main Registration //
|
|
161
|
+
// ========================================================================== //
|
|
162
|
+
|
|
163
|
+
const noop = () => "";
|
|
164
|
+
Json.register(["Object", "Array"], noop);
|
|
165
|
+
Json.register(["string", "number", "bool", "null", "array", "none"], noop);
|
|
166
|
+
|
|
167
|
+
Json.register("Json", ({ args, content, ast }) => {
|
|
168
|
+
if (!ast) return "";
|
|
169
|
+
return processNode(ast, null);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export default Json;
|