sommark 2.3.1 → 3.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 +47 -42
- package/SOMMARK-SPEC.md +483 -0
- package/cli/cli.mjs +42 -2
- package/cli/commands/color.js +36 -0
- package/cli/commands/help.js +7 -0
- package/cli/commands/init.js +2 -0
- package/cli/commands/list.js +119 -0
- package/cli/commands/print.js +61 -6
- package/cli/commands/show.js +24 -27
- package/cli/constants.js +1 -1
- package/cli/helpers/config.js +14 -4
- package/cli/helpers/transpile.js +28 -33
- package/constants/html_props.js +100 -0
- package/constants/html_tags.js +146 -0
- package/constants/void_elements.js +26 -0
- package/core/lexer.js +70 -39
- package/core/parser.js +102 -81
- package/core/pluginManager.js +139 -0
- package/core/plugins/comment-remover.js +47 -0
- package/core/plugins/module-system.js +137 -0
- package/core/plugins/quote-escaper.js +37 -0
- package/core/plugins/raw-content-plugin.js +72 -0
- package/core/plugins/rules-validation-plugin.js +197 -0
- package/core/plugins/sommark-format.js +211 -0
- package/core/transpiler.js +65 -198
- package/debug.js +9 -4
- package/format.js +23 -0
- package/formatter/mark.js +3 -3
- package/formatter/tag.js +6 -2
- package/grammar.ebnf +5 -5
- package/helpers/camelize.js +2 -0
- package/helpers/colorize.js +20 -14
- package/helpers/kebabize.js +2 -0
- package/helpers/utils.js +161 -0
- package/index.js +243 -44
- package/mappers/languages/html.js +200 -105
- package/mappers/languages/json.js +23 -4
- package/mappers/languages/markdown.js +88 -67
- package/mappers/languages/mdx.js +130 -2
- package/mappers/mapper.js +77 -246
- package/package.json +7 -5
- package/unformatted.smark +90 -0
- package/v3-todo.smark +75 -0
- package/CHANGELOG.md +0 -113
- package/helpers/loadCss.js +0 -46
package/core/transpiler.js
CHANGED
|
@@ -3,210 +3,106 @@ import escapeHTML from "../helpers/escapeHTML.js";
|
|
|
3
3
|
import { transpilerError } from "./errors.js";
|
|
4
4
|
import { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat } from "./formats.js";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const BODY_PLACEHOLDER = `__SOMMARK_BODY_PLACEHOLDER_${Math.random().toString(36).slice(2)}__`;
|
|
7
7
|
|
|
8
8
|
// ========================================================================== //
|
|
9
9
|
// Extracting target identifier //
|
|
10
10
|
// ========================================================================== //
|
|
11
11
|
function matchedValue(outputs, targetId) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
result = outputValue;
|
|
17
|
-
break;
|
|
18
|
-
}
|
|
19
|
-
} else if (Array.isArray(outputValue.id)) {
|
|
20
|
-
for (const id of outputValue.id) {
|
|
21
|
-
if (id === targetId) {
|
|
22
|
-
result = outputValue;
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
12
|
+
if (!outputs || !targetId) return undefined;
|
|
13
|
+
return outputs.find(output => {
|
|
14
|
+
if (Array.isArray(output.id)) {
|
|
15
|
+
return output.id.some(id => id === targetId);
|
|
26
16
|
}
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
return typeof output.id === "string" && output.id === targetId;
|
|
18
|
+
});
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
function validateRules(target, args, content, type = null) {
|
|
32
|
-
if (!target || !target.options || !target.options.rules) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const { rules } = target.options;
|
|
36
|
-
const { id } = target;
|
|
37
|
-
|
|
38
|
-
// Validate Args
|
|
39
|
-
if (args && rules.args) {
|
|
40
|
-
const { min, max, required, includes } = rules.args;
|
|
41
|
-
const argKeys = Object.keys(args).filter(key => isNaN(parseInt(key))); // Get named keys
|
|
42
|
-
const argValues = Object.values(args);
|
|
43
|
-
const argCount = args.length;
|
|
44
|
-
|
|
45
|
-
// Min Check
|
|
46
|
-
if (min && argCount < min) {
|
|
47
|
-
transpilerError([
|
|
48
|
-
"{line}<$red:Validation Error:$> ",
|
|
49
|
-
`<$yellow:Identifier$> <$blue:'${Array.isArray(id) ? id.join(" | ") : id}'$> <$yellow:requires at least$> <$green:${min}$> <$yellow:argument(s). Found$> <$red:${argCount}$>{line}`
|
|
50
|
-
]);
|
|
51
|
-
}
|
|
52
|
-
// Max Check
|
|
53
|
-
if (max && argCount > max) {
|
|
54
|
-
transpilerError([
|
|
55
|
-
"{line}<$red:Validation Error:$> ",
|
|
56
|
-
`<$yellow:Identifier$> <$blue:'${Array.isArray(id) ? id.join(" | ") : id}'$> <$yellow:accepts at most$> <$green:${max}$> <$yellow:argument(s). Found$> <$red:${argCount}$>{line}`
|
|
57
|
-
]);
|
|
58
|
-
}
|
|
59
|
-
// Required Keys Check
|
|
60
|
-
if (required && Array.isArray(required)) {
|
|
61
|
-
const missingKeys = required.filter(key => !Object.prototype.hasOwnProperty.call(args, key));
|
|
62
|
-
if (missingKeys.length > 0) {
|
|
63
|
-
transpilerError([
|
|
64
|
-
"{line}<$red:Validation Error:$> ",
|
|
65
|
-
`<$yellow:Identifier$> <$blue:'${Array.isArray(id) ? id.join(" | ") : id}'$> <$yellow:is missing required argument(s):$> <$red:${missingKeys.join(", ")}$>{line}`
|
|
66
|
-
]);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// Includes Keys Check
|
|
70
|
-
if (includes && Array.isArray(includes)) {
|
|
71
|
-
const invalidKeys = argKeys.filter(key => !includes.includes(key));
|
|
72
|
-
if (invalidKeys.length > 0) {
|
|
73
|
-
transpilerError([
|
|
74
|
-
"{line}<$red:Validation Error:$> ",
|
|
75
|
-
`<$yellow:Identifier$> <$blue:'${Array.isArray(id) ? id.join(" | ") : id}'$> <$yellow:contains invalid argument key(s):$> <$red:${invalidKeys.join(", ")}$>`,
|
|
76
|
-
`{N}<$yellow:Allowed keys are:$> <$green:${includes.join(", ")}$>{line}`
|
|
77
|
-
]);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Validate Content
|
|
83
|
-
if (content && rules.content) {
|
|
84
|
-
const { maxLength } = rules.content;
|
|
85
|
-
if (maxLength && content.length > maxLength) {
|
|
86
|
-
transpilerError([
|
|
87
|
-
"{line}<$red:Validation Error:$> ",
|
|
88
|
-
`<$yellow:Identifier$> <$blue:'${Array.isArray(id) ? id.join(" | ") : id}'$> <$yellow:content exceeds maximum length of$> <$green:${maxLength}$> <$yellow:characters. Found$> <$red:${content.length}$>{line}`
|
|
89
|
-
]);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
// Validate is_Self_closing
|
|
93
|
-
if (id && rules.is_Self_closing) {
|
|
94
|
-
if (content) {
|
|
95
|
-
transpilerError([
|
|
96
|
-
"{line}<$red:Validation Error:$> ",
|
|
97
|
-
`<$yellow:Identifier$> <$blue:'${Array.isArray(id) ? id.join(" | ") : id}'$> <$yellow:is self-closing tag and is not allowed to have a content | children$>{line}`
|
|
98
|
-
]);
|
|
99
|
-
}
|
|
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
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// ========================================================================== //
|
|
112
|
-
// +++++++++++++++++++++++++++++ //
|
|
113
|
-
// ========================================================================== //
|
|
114
21
|
async function generateOutput(ast, i, format, mapper_file) {
|
|
115
22
|
const node = Array.isArray(ast) ? ast[i] : ast;
|
|
116
23
|
let result = "";
|
|
117
24
|
let context = "";
|
|
118
25
|
let target = mapper_file ? matchedValue(mapper_file.outputs, node.id) : null;
|
|
119
|
-
|
|
26
|
+
|
|
27
|
+
if (!target) {
|
|
28
|
+
target = mapper_file.getUnknownTag(node);
|
|
29
|
+
}
|
|
30
|
+
|
|
120
31
|
if (target) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
32
|
+
// ========================================================================== //
|
|
33
|
+
// Always use placeholders for blocks to support wrapping //
|
|
34
|
+
// ========================================================================== //
|
|
35
|
+
const placeholder = format === mdxFormat && node.body.length > 0 ? `\n${BODY_PLACEHOLDER}\n` : BODY_PLACEHOLDER;
|
|
36
|
+
|
|
37
|
+
result += target.render.call(mapper_file, { args: node.args, content: placeholder, ast: node });
|
|
38
|
+
if (format === mdxFormat) result = "\n" + result + "\n";
|
|
39
|
+
|
|
40
|
+
// ========================================================================== //
|
|
41
|
+
// Body nodes //
|
|
42
|
+
// ========================================================================== //
|
|
127
43
|
for (let j = 0; j < node.body.length; j++) {
|
|
128
44
|
const body_node = node.body[j];
|
|
129
45
|
switch (body_node.type) {
|
|
130
|
-
// ========================================================================== //
|
|
131
|
-
// Text //
|
|
132
|
-
// ========================================================================== //
|
|
133
46
|
case TEXT:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
context += [htmlFormat, mdxFormat].includes(format) && shouldEscape ? escapeHTML(body_node.text) : body_node.text;
|
|
47
|
+
const shouldEscapeText = target.options?.escape !== false;
|
|
48
|
+
context += (format === htmlFormat || format === mdxFormat) && shouldEscapeText ? escapeHTML(body_node.text) : body_node.text;
|
|
137
49
|
break;
|
|
138
|
-
|
|
139
|
-
// Inline //
|
|
140
|
-
// ========================================================================== //
|
|
50
|
+
|
|
141
51
|
case INLINE:
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
52
|
+
let inlineTarget = matchedValue(mapper_file.outputs, body_node.id);
|
|
53
|
+
if (!inlineTarget) {
|
|
54
|
+
inlineTarget = mapper_file.getUnknownTag(body_node);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (inlineTarget) {
|
|
58
|
+
const shouldEscapeInline = inlineTarget.options?.escape !== false;
|
|
145
59
|
context +=
|
|
146
|
-
|
|
60
|
+
inlineTarget.render.call(mapper_file, {
|
|
147
61
|
args: body_node.args.length > 0 ? body_node.args : [],
|
|
148
|
-
content: format === htmlFormat || format === mdxFormat ? escapeHTML(body_node.value) : body_node.value
|
|
62
|
+
content: (format === htmlFormat || format === mdxFormat) && shouldEscapeInline ? escapeHTML(body_node.value) : body_node.value,
|
|
63
|
+
ast: body_node
|
|
149
64
|
}) + (format === mdxFormat ? "\n" : "");
|
|
150
65
|
}
|
|
151
66
|
break;
|
|
152
|
-
|
|
153
|
-
// Atblock //
|
|
154
|
-
// ========================================================================== //
|
|
67
|
+
|
|
155
68
|
case ATBLOCK:
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
69
|
+
let atTarget = matchedValue(mapper_file.outputs, body_node.id);
|
|
70
|
+
if (!atTarget) {
|
|
71
|
+
atTarget = mapper_file.getUnknownTag(body_node);
|
|
72
|
+
}
|
|
73
|
+
if (atTarget) {
|
|
74
|
+
const shouldEscapeAt = atTarget.options?.escape !== false;
|
|
75
|
+
let content = body_node.content;
|
|
76
|
+
if (shouldEscapeAt && (format === htmlFormat || format === mdxFormat)) {
|
|
77
|
+
content = escapeHTML(content);
|
|
163
78
|
}
|
|
164
|
-
|
|
79
|
+
const rendered = atTarget.render.call(mapper_file, { args: body_node.args, content, ast: body_node }).trimEnd() + "\n";
|
|
80
|
+
context = context.trim() ? context.trimEnd() + "\n" + rendered : context + rendered;
|
|
165
81
|
}
|
|
166
82
|
break;
|
|
167
|
-
|
|
168
|
-
// Comment //
|
|
169
|
-
// ========================================================================== //
|
|
83
|
+
|
|
170
84
|
case COMMENT:
|
|
171
|
-
|
|
172
|
-
context += " ".repeat(body_node.depth) + `\n<!--${body_node.text.replace("#", "")}-->\n`;
|
|
173
|
-
} else if (format === mdxFormat) {
|
|
174
|
-
context += " ".repeat(body_node.depth) + `\n{/*${body_node.text.replace("#", "")} */}\n`;
|
|
175
|
-
}
|
|
85
|
+
context += " ".repeat(body_node.depth) + `\n${mapper_file.comment(body_node.text)}\n`;
|
|
176
86
|
break;
|
|
177
|
-
|
|
178
|
-
// Block //
|
|
179
|
-
// ========================================================================== //
|
|
87
|
+
|
|
180
88
|
case BLOCK:
|
|
181
|
-
|
|
182
|
-
context
|
|
89
|
+
const blockOutput = await generateOutput(body_node, i, format, mapper_file);
|
|
90
|
+
context = context.trim() ? context.trimEnd() + "\n" + blockOutput : context + blockOutput;
|
|
183
91
|
break;
|
|
184
92
|
}
|
|
185
93
|
}
|
|
186
94
|
|
|
187
|
-
|
|
188
|
-
result = result.replace("<%smark>", context);
|
|
189
|
-
} else {
|
|
190
|
-
result += context;
|
|
191
|
-
}
|
|
95
|
+
result = result.replaceAll(BODY_PLACEHOLDER, context);
|
|
192
96
|
}
|
|
193
|
-
// ========================================================================== //
|
|
194
|
-
// Text //
|
|
195
|
-
// ========================================================================== //
|
|
196
97
|
else if (format === textFormat) {
|
|
197
98
|
for (const body_node of node.body) {
|
|
198
99
|
switch (body_node.type) {
|
|
199
|
-
case TEXT:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
case INLINE:
|
|
203
|
-
context += body_node.value + " ";
|
|
204
|
-
break;
|
|
205
|
-
case ATBLOCK:
|
|
206
|
-
context += body_node.content;
|
|
207
|
-
break;
|
|
100
|
+
case TEXT: context += body_node.text; break;
|
|
101
|
+
case INLINE: context += body_node.value + " "; break;
|
|
102
|
+
case ATBLOCK: context += body_node.content.trimEnd() + "\n"; break;
|
|
208
103
|
case BLOCK:
|
|
209
|
-
|
|
104
|
+
const textBlockOutput = await generateOutput(body_node, i, format, mapper_file);
|
|
105
|
+
context = context.trim() ? context.trimEnd() + "\n" + textBlockOutput : context + textBlockOutput;
|
|
210
106
|
break;
|
|
211
107
|
}
|
|
212
108
|
}
|
|
@@ -217,7 +113,7 @@ async function generateOutput(ast, i, format, mapper_file) {
|
|
|
217
113
|
`<$yellow:Identifier$> <$blue:'${node.id}'$> <$yellow: is not found in mapping outputs$>{line}`
|
|
218
114
|
]);
|
|
219
115
|
}
|
|
220
|
-
return result;
|
|
116
|
+
return result.trimEnd() + "\n";
|
|
221
117
|
}
|
|
222
118
|
|
|
223
119
|
async function transpiler({ ast, format, mapperFile, includeDocument = true }) {
|
|
@@ -226,42 +122,13 @@ async function transpiler({ ast, format, mapperFile, includeDocument = true }) {
|
|
|
226
122
|
if (ast[i].type === BLOCK) {
|
|
227
123
|
output += await generateOutput(ast, i, format, mapperFile);
|
|
228
124
|
} else if (ast[i].type === COMMENT) {
|
|
229
|
-
|
|
230
|
-
output += `<!--${ast[i].text.replace("#", "")}-->\n`;
|
|
231
|
-
} else if (format === mdxFormat) {
|
|
232
|
-
output += `{/*${ast[i].text.replace("#", "")} */}\n`;
|
|
233
|
-
}
|
|
125
|
+
output += mapperFile.comment(ast[i].text);
|
|
234
126
|
}
|
|
235
127
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (style) {
|
|
241
|
-
const styleTag = `<style>\n${style}\n</style>`;
|
|
242
|
-
if (!finalHeader.includes(styleTag)) {
|
|
243
|
-
finalHeader += styleTag + "\n";
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
// Inject Style Tag if code blocks exist
|
|
249
|
-
if (mapperFile.enable_highlightTheme && (output.includes("<pre") || output.includes("<code"))) {
|
|
250
|
-
mapperFile.addStyle(mapperFile.themes[mapperFile.currentTheme]);
|
|
251
|
-
styleContent = mapperFile.styles.join("\n");
|
|
252
|
-
updateStyleTag(styleContent);
|
|
253
|
-
} else {
|
|
254
|
-
styleContent = mapperFile.styles.join("\n");
|
|
255
|
-
updateStyleTag(styleContent);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const document = `<!DOCTYPE html>\n<html>\n${finalHeader}\n<body>\n${output}\n</body>\n</html>\n`;
|
|
259
|
-
return document;
|
|
260
|
-
}
|
|
261
|
-
if (format === jsonFormat) {
|
|
262
|
-
output = JSON.parse(JSON.stringify(output));
|
|
263
|
-
}
|
|
264
|
-
return output;
|
|
128
|
+
// ========================================================================== //
|
|
129
|
+
// Final Post-Processing (Dynamic Formats) //
|
|
130
|
+
// ========================================================================== //
|
|
131
|
+
return mapperFile.formatOutput(output, includeDocument);
|
|
265
132
|
}
|
|
266
133
|
|
|
267
134
|
export default transpiler;
|
package/debug.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import SomMark from "./index.js";
|
|
3
3
|
|
|
4
|
-
const buffer = await fs.readFile("./
|
|
4
|
+
const buffer = await fs.readFile("./examples/markdown/tasks.smark");
|
|
5
5
|
const file_content = buffer.toString();
|
|
6
|
-
let smark = new SomMark({
|
|
6
|
+
let smark = new SomMark({
|
|
7
|
+
src: file_content,
|
|
8
|
+
format: "markdown",
|
|
9
|
+
includeDocument: true,
|
|
10
|
+
});
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
// console.log(smark.
|
|
12
|
+
|
|
13
|
+
// console.log(JSON.stringify(await smark.parse(), null, 2));
|
|
14
|
+
// console.log(await smark.lex());
|
|
10
15
|
console.log(await smark.transpile());
|
package/format.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import SomMark from "./index.js";
|
|
3
|
+
const rawSource = await fs.readFile("./unformatted.smark", "utf8");
|
|
4
|
+
|
|
5
|
+
const smark = new SomMark({
|
|
6
|
+
src: rawSource,
|
|
7
|
+
format: "html",
|
|
8
|
+
plugins: [
|
|
9
|
+
{
|
|
10
|
+
name: "sommark-format",
|
|
11
|
+
options: { indentString: " " } // 2 spaces instead of default "\t"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// 1. Run parse()
|
|
17
|
+
await smark.parse();
|
|
18
|
+
|
|
19
|
+
// 2. Get the formatted string from the "sommark-format" plugin
|
|
20
|
+
const formatPlugin = smark.plugins.find(p => p.name === "sommark-format");
|
|
21
|
+
|
|
22
|
+
console.log("--- Formatted Output ---");
|
|
23
|
+
console.log(formatPlugin.formattedSource);
|
package/formatter/mark.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
class MarkdownBuilder {
|
|
2
|
-
constructor() {}
|
|
2
|
+
constructor() { }
|
|
3
3
|
// ========================================================================== //
|
|
4
4
|
// Headings //
|
|
5
5
|
// ========================================================================== //
|
|
@@ -119,9 +119,9 @@ class MarkdownBuilder {
|
|
|
119
119
|
rows = rows.map(row => {
|
|
120
120
|
let columns;
|
|
121
121
|
if (typeof row === "string") {
|
|
122
|
-
columns = row.split(
|
|
122
|
+
columns = row.split(/(?<!\\),/).map(c => c.trim().replace(/\\(.)/g, "$1"));
|
|
123
123
|
} else if (Array.isArray(row)) {
|
|
124
|
-
columns = row.map(c => String(c).trim());
|
|
124
|
+
columns = row.map(c => String(c).trim().replace(/\\(.)/g, "$1"));
|
|
125
125
|
} else {
|
|
126
126
|
return "";
|
|
127
127
|
}
|
package/formatter/tag.js
CHANGED
|
@@ -21,7 +21,11 @@ class TagBuilder {
|
|
|
21
21
|
if (value === true) {
|
|
22
22
|
this.#attr.push(`${key}`);
|
|
23
23
|
} else if (value !== false) {
|
|
24
|
-
|
|
24
|
+
let val = value ?? "";
|
|
25
|
+
if (key === "style" && typeof val === "string") {
|
|
26
|
+
val = val.replace(/(^|[^\w\-_$])(--[\w\-_$]+)(?![\w\-_$]|:)/g, "$1var($2)");
|
|
27
|
+
}
|
|
28
|
+
this.#attr.push(`${key}="${escapeHTML(val)}"`);
|
|
25
29
|
}
|
|
26
30
|
});
|
|
27
31
|
}
|
|
@@ -38,7 +42,7 @@ class TagBuilder {
|
|
|
38
42
|
props(propsList) {
|
|
39
43
|
const list = Array.isArray(propsList) ? propsList : [propsList];
|
|
40
44
|
if (list.length > 0) {
|
|
41
|
-
|
|
45
|
+
for (const propEntry of list) {
|
|
42
46
|
if (typeof propEntry !== "object" || propEntry === null) {
|
|
43
47
|
throw new TypeError("prop expects an object with property { __type__ }");
|
|
44
48
|
}
|
package/grammar.ebnf
CHANGED
|
@@ -5,12 +5,12 @@ WhiteSpace = " " | "\t" | "\n" | "\r";
|
|
|
5
5
|
Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
|
|
6
6
|
Letter = "A" | ... | "Z" | "a" | ... | "z";
|
|
7
7
|
SpecialChar = "[" | "]" | "(" | ")" | ":" | "-" | ">" | "@" | "_" | "=" | "," | ";";
|
|
8
|
-
Identifier = (Letter | Digit), { Letter | Digit };
|
|
8
|
+
Identifier = (Letter | Digit | "_" | "$" | "-"), { Letter | Digit | "_" | "$" | "-" };
|
|
9
9
|
EscapeChar = "\";
|
|
10
10
|
|
|
11
11
|
(* General Rules *)
|
|
12
|
-
(* 1. Identifiers can
|
|
13
|
-
(* 2. Semi-colon is
|
|
12
|
+
(* 1. Identifiers can include letters, numbers, underscores (_), dollar signs ($), and hyphens (-). *)
|
|
13
|
+
(* 2. Semi-colon is mandatory for ending Atblock headers. *)
|
|
14
14
|
(* 3. Colon is a separator in Block and Atblock arguments; must be escaped if part of value. *)
|
|
15
15
|
|
|
16
16
|
(* Comments *)
|
|
@@ -61,7 +61,7 @@ InlineArg = { ? any char except ",", ")" ? | EscapedChar };
|
|
|
61
61
|
(* ========================================== *)
|
|
62
62
|
|
|
63
63
|
AtBlock = AtBlockStart, AtBlockContent, AtBlockEnd;
|
|
64
|
-
AtBlockStart = "@_", Identifier, "_@", [ ":", AtBlockArgs, ";"
|
|
64
|
+
AtBlockStart = "@_", Identifier, "_@", [ ":", AtBlockArgs ], ";";
|
|
65
65
|
AtBlockEnd = "@_", "end", "_@";
|
|
66
66
|
|
|
67
67
|
(* Atblock Arguments: Key-Value or Value only, terminated by semicolon *)
|
|
@@ -72,7 +72,7 @@ AtBlockValue = { ? any char except ",", ";" ? | EscapedChar };
|
|
|
72
72
|
AtBlockContent = { ? any character ? };
|
|
73
73
|
|
|
74
74
|
(* Example: @_Identifier_@: arg1, arg2; Content... @_end_@ *)
|
|
75
|
-
(* Example: @_Identifier_
|
|
75
|
+
(* Example: @_Identifier_@; Content... @_end_@ *)
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
(* Text Content *)
|
package/helpers/colorize.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
const colors = {
|
|
2
|
+
red: "\x1b[31m",
|
|
3
|
+
green: "\x1b[32m",
|
|
4
|
+
yellow: "\x1b[33m",
|
|
5
|
+
blue: "\x1b[34m",
|
|
6
|
+
magenta: "\x1b[35m",
|
|
7
|
+
cyan: "\x1b[36m",
|
|
8
|
+
reset: "\x1b[0m"
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export let useColor = false;
|
|
12
|
+
|
|
13
|
+
export function enableColor(enabled = true) {
|
|
14
|
+
useColor = enabled;
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
export default function colorize(color, text) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
green: "\x1b[32m",
|
|
5
|
-
yellow: "\x1b[33m",
|
|
6
|
-
blue: "\x1b[34m",
|
|
7
|
-
magenta: "\x1b[35m",
|
|
8
|
-
cyan: "\x1b[36m",
|
|
9
|
-
reset: "\x1b[0m"
|
|
10
|
-
};
|
|
11
|
-
if (!text) throw new Error(`${colors["red"]}argument 'text' is not defined.${colors["reset"]}`);
|
|
12
|
-
if (color) {
|
|
18
|
+
if (!text) throw new Error("argument 'text' is not defined.");
|
|
19
|
+
if (useColor && color && colors[color]) {
|
|
13
20
|
return colors[color] + text + colors["reset"];
|
|
14
|
-
} else {
|
|
15
|
-
return text;
|
|
16
21
|
}
|
|
17
|
-
|
|
22
|
+
return text;
|
|
23
|
+
}
|
package/helpers/utils.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { sommarkError } from "../core/errors.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a todo item should be checked.
|
|
5
|
+
* @param {string} status
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
export function todo(status = "") {
|
|
9
|
+
return (status || "").trim() === "x" || (status || "").trim().toLowerCase() === "done";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Matches a target ID within an array of output registration objects.
|
|
14
|
+
*/
|
|
15
|
+
export function matchedValue(outputs, targetId) {
|
|
16
|
+
let result;
|
|
17
|
+
for (const outputValue of outputs) {
|
|
18
|
+
if (typeof outputValue.id === "string") {
|
|
19
|
+
if (outputValue.id === targetId) {
|
|
20
|
+
result = outputValue;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
} else if (Array.isArray(outputValue.id)) {
|
|
24
|
+
for (const id of outputValue.id) {
|
|
25
|
+
if (id === targetId) {
|
|
26
|
+
result = outputValue;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Safe argument retrieval with validation.
|
|
37
|
+
*/
|
|
38
|
+
export function safeArg(args, index, key, type = null, setType = null, fallBack = null) {
|
|
39
|
+
if (!Array.isArray(args)) {
|
|
40
|
+
sommarkError([`{line}<$red:TypeError:$> <$yellow:args must be an array$>{line}`]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (index === undefined && key === undefined) {
|
|
44
|
+
sommarkError([`{line}<$red:ReferenceError:> <$yellow:At least one of 'index' or 'key' must be provided$>{line}`]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const validate = value => {
|
|
48
|
+
if (value === undefined) return false;
|
|
49
|
+
if (!type) return true;
|
|
50
|
+
const evaluated = setType ? setType(value) : value;
|
|
51
|
+
return typeof evaluated === type;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (index !== undefined && validate(args[index])) {
|
|
55
|
+
return args[index];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (key !== undefined && validate(args[key])) {
|
|
59
|
+
return args[key];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return fallBack;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Renders an HTML table.
|
|
67
|
+
*/
|
|
68
|
+
export function htmlTable(data, headers, escapeFn = (t) => t) {
|
|
69
|
+
if (!data) return "";
|
|
70
|
+
|
|
71
|
+
if (typeof data === "string") {
|
|
72
|
+
data = data.split(/\r?\n/);
|
|
73
|
+
} else if (!Array.isArray(data) || data.length === 0) {
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let tableHTML = `<table class="sommark-table">\n<thead>\n<tr>`;
|
|
78
|
+
for (const header of headers) {
|
|
79
|
+
tableHTML += `<th>${escapeFn(header)}</th>`;
|
|
80
|
+
}
|
|
81
|
+
tableHTML += "</tr>\n</thead>\n<tbody>\n";
|
|
82
|
+
|
|
83
|
+
for (const row of data) {
|
|
84
|
+
const trimmedRow = row.trim();
|
|
85
|
+
if (!trimmedRow) continue;
|
|
86
|
+
|
|
87
|
+
const rowData = trimmedRow.split(/(?<!\\),/).map(cell => {
|
|
88
|
+
let text = cell.trim();
|
|
89
|
+
if (text.endsWith(";")) text = text.slice(0, -1);
|
|
90
|
+
return text.replace(/\\(.)/g, "$1");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
tableHTML += "<tr>";
|
|
94
|
+
for (const cell of rowData) {
|
|
95
|
+
tableHTML += `<td>${escapeFn(cell.trim())}</td>`;
|
|
96
|
+
}
|
|
97
|
+
tableHTML += "</tr>\n";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
tableHTML += "</tbody>\n</table>";
|
|
101
|
+
return tableHTML;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parses a hierarchical list.
|
|
106
|
+
*/
|
|
107
|
+
export function parseList(data, indentSize = 2) {
|
|
108
|
+
if (typeof data === "string") {
|
|
109
|
+
data = data.split("\n");
|
|
110
|
+
}
|
|
111
|
+
const root = { level: -1, children: [] };
|
|
112
|
+
const stack = [root];
|
|
113
|
+
|
|
114
|
+
const getLevel = line => {
|
|
115
|
+
const spaces = line.match(/^\s*/)[0].length;
|
|
116
|
+
return Math.floor(spaces / indentSize);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
for (const raw of data) {
|
|
120
|
+
if (!raw.trim()) continue;
|
|
121
|
+
const level = getLevel(raw);
|
|
122
|
+
const text = raw.trim();
|
|
123
|
+
|
|
124
|
+
const node = { text, children: [] };
|
|
125
|
+
|
|
126
|
+
while (stack.length && stack[stack.length - 1].level >= level) {
|
|
127
|
+
stack.pop();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
stack[stack.length - 1].children.push(node);
|
|
131
|
+
stack.push({ ...node, level });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return root.children;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Renders a list (ul/ol).
|
|
139
|
+
*/
|
|
140
|
+
export function list(data, as = "ul", escapeFn = (t) => t) {
|
|
141
|
+
const nodes = parseList(data);
|
|
142
|
+
if (!Array.isArray(nodes) || nodes.length === 0) return "";
|
|
143
|
+
|
|
144
|
+
const tag = as === "ol" ? "ol" : "ul";
|
|
145
|
+
|
|
146
|
+
const renderItems = items => {
|
|
147
|
+
let html = `<${tag}>`;
|
|
148
|
+
for (const item of items) {
|
|
149
|
+
html += `<li>`;
|
|
150
|
+
html += escapeFn(item.text);
|
|
151
|
+
if (item.children && item.children.length > 0) {
|
|
152
|
+
html += renderItems(item.children);
|
|
153
|
+
}
|
|
154
|
+
html += `</li>`;
|
|
155
|
+
}
|
|
156
|
+
html += `</${tag}>`;
|
|
157
|
+
return html;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return renderItems(nodes);
|
|
161
|
+
}
|