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.
Files changed (45) hide show
  1. package/README.md +47 -42
  2. package/SOMMARK-SPEC.md +483 -0
  3. package/cli/cli.mjs +42 -2
  4. package/cli/commands/color.js +36 -0
  5. package/cli/commands/help.js +7 -0
  6. package/cli/commands/init.js +2 -0
  7. package/cli/commands/list.js +119 -0
  8. package/cli/commands/print.js +61 -6
  9. package/cli/commands/show.js +24 -27
  10. package/cli/constants.js +1 -1
  11. package/cli/helpers/config.js +14 -4
  12. package/cli/helpers/transpile.js +28 -33
  13. package/constants/html_props.js +100 -0
  14. package/constants/html_tags.js +146 -0
  15. package/constants/void_elements.js +26 -0
  16. package/core/lexer.js +70 -39
  17. package/core/parser.js +102 -81
  18. package/core/pluginManager.js +139 -0
  19. package/core/plugins/comment-remover.js +47 -0
  20. package/core/plugins/module-system.js +137 -0
  21. package/core/plugins/quote-escaper.js +37 -0
  22. package/core/plugins/raw-content-plugin.js +72 -0
  23. package/core/plugins/rules-validation-plugin.js +197 -0
  24. package/core/plugins/sommark-format.js +211 -0
  25. package/core/transpiler.js +65 -198
  26. package/debug.js +9 -4
  27. package/format.js +23 -0
  28. package/formatter/mark.js +3 -3
  29. package/formatter/tag.js +6 -2
  30. package/grammar.ebnf +5 -5
  31. package/helpers/camelize.js +2 -0
  32. package/helpers/colorize.js +20 -14
  33. package/helpers/kebabize.js +2 -0
  34. package/helpers/utils.js +161 -0
  35. package/index.js +243 -44
  36. package/mappers/languages/html.js +200 -105
  37. package/mappers/languages/json.js +23 -4
  38. package/mappers/languages/markdown.js +88 -67
  39. package/mappers/languages/mdx.js +130 -2
  40. package/mappers/mapper.js +77 -246
  41. package/package.json +7 -5
  42. package/unformatted.smark +90 -0
  43. package/v3-todo.smark +75 -0
  44. package/CHANGELOG.md +0 -113
  45. package/helpers/loadCss.js +0 -46
@@ -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 expose_for_fmts = [jsonFormat];
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
- let result;
13
- for (const outputValue of outputs) {
14
- if (typeof outputValue.id === "string") {
15
- if (outputValue.id === targetId) {
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
- return result;
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
- const block_formats = [htmlFormat, mdxFormat, jsonFormat];
26
+
27
+ if (!target) {
28
+ target = mapper_file.getUnknownTag(node);
29
+ }
30
+
120
31
  if (target) {
121
- validateRules(target, node.args, "", node.type);
122
- const placeholder = format === mdxFormat && node.body.length > 0 ? "\n<%smark>\n" : "<%smark>";
123
- result += block_formats.includes(format)
124
- ? `${format === mdxFormat ? "\n" : ""}${target.render({ args: node.args, content: placeholder, ast: expose_for_fmts.includes(format) ? ast[i] : null }) + (format === mdxFormat ? "\n" : "")}`
125
- : target.render({ args: node.args, content: "" }) + (format === mdxFormat ? "\n" : "");
126
- // Body nodes
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
- validateRules(target, body_node.args, body_node.text);
135
- const shouldEscape = target && target.options && target.options.escape === false ? false : true;
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
- target = matchedValue(mapper_file.outputs, body_node.id);
143
- if (target) {
144
- validateRules(target, body_node.args, body_node.value, body_node.type);
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
- target.render({
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
- target = matchedValue(mapper_file.outputs, body_node.id);
157
- if (target) {
158
- validateRules(target, body_node.args, body_node.content, body_node.type);
159
- // Escape logic: fallback to options.escape, default true
160
- const shouldEscape = target.options?.escape ?? true;
161
- if (shouldEscape) {
162
- body_node.content = escapeHTML(body_node.content);
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
- context += target.render({ args: body_node.args, content: body_node.content }) + (format === mdxFormat ? "\n" : "");
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
- if (format === htmlFormat || format === markdownFormat) {
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
- target = matchedValue(mapper_file.outputs, body_node.id);
182
- context += await generateOutput(body_node, i, format, mapper_file);
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
- if (format === htmlFormat || format === mdxFormat || format === jsonFormat) {
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
- context += body_node.text;
201
- break;
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
- context += await generateOutput(body_node, i, format, mapper_file);
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
- if (format === htmlFormat || format === markdownFormat) {
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
- if (includeDocument && format === htmlFormat) {
237
- let finalHeader = mapperFile.header;
238
- let styleContent = "";
239
- const updateStyleTag = style => {
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("./example.smark");
4
+ const buffer = await fs.readFile("./examples/markdown/tasks.smark");
5
5
  const file_content = buffer.toString();
6
- let smark = new SomMark({ src: file_content, format: "html", includeDocument: true });
6
+ let smark = new SomMark({
7
+ src: file_content,
8
+ format: "markdown",
9
+ includeDocument: true,
10
+ });
7
11
 
8
- // console.log(JSON.stringify(smark.parse(), null, 2));
9
- // console.log(smark.lex());
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(",").map(c => c.trim());
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
- this.#attr.push(`${key}="${escapeHTML(value ?? "")}"`);
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
- for (const propEntry of list) {
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 be only letters and numbers. *)
13
- (* 2. Semi-colon is only recognized in Atblock arguments. *)
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_@ Content... @_end_@ *)
75
+ (* Example: @_Identifier_@; Content... @_end_@ *)
76
76
 
77
77
 
78
78
  (* Text Content *)
@@ -0,0 +1,2 @@
1
+ const camelize = str => str.replace(/-./g, x => x[1].toUpperCase());
2
+ export default camelize;
@@ -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
- const colors = {
3
- red: "\x1b[31m",
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
+ }
@@ -0,0 +1,2 @@
1
+ const kebabize = str => str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
2
+ export default kebabize;
@@ -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
+ }