sommark 2.1.1 → 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.
@@ -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;
@@ -1,24 +1,38 @@
1
1
  import Mapper from "../mapper.js";
2
2
  const MARKDOWN = new Mapper();
3
- const { md } = MARKDOWN;
3
+ const { md, safeArg } = MARKDOWN;
4
4
  // Block
5
- MARKDOWN.register("Block", ({ content }) => {
6
- return content;
7
- });
8
- // Section
9
- MARKDOWN.register("Section", ({ content }) => {
5
+ MARKDOWN.register(["Block", "block", "Section", "section"], ({ content }) => {
10
6
  return content;
11
7
  });
12
8
  // Headings
13
- MARKDOWN.register("Heading", ({ args, content }) => {
14
- return md.heading(args[1], args[0]) + content;
15
- });
9
+ MARKDOWN.register(
10
+ ["Heading", "heading"],
11
+ ({ args, content }) => {
12
+ const level = safeArg(args, 0, "level", "number", Number, 1);
13
+ const title = safeArg(args, 1, "title", null, null, "");
14
+ return md.heading(title, level) + content;
15
+ },
16
+ {
17
+ rules: {
18
+ type: "Block"
19
+ }
20
+ }
21
+ );
16
22
  // Inline Headings
17
- ["h1", "h2", "h3", "h4", "h5", "h6"].forEach(heading => {
18
- MARKDOWN.register(heading, ({ content }) => {
19
- return md.heading(content, heading.replace("h", ""));
20
- });
21
- });
23
+ ["h1", "h2", "h3", "h4", "h5", "h6"].forEach(
24
+ heading => {
25
+ MARKDOWN.register(heading, ({ content }) => {
26
+ const lvl = heading[1] && typeof Number(heading[1]) === "number" ? heading[1] : 1;
27
+ return md.heading(content, lvl);
28
+ });
29
+ },
30
+ {
31
+ rules: {
32
+ type: "Inline"
33
+ }
34
+ }
35
+ );
22
36
  // Bold
23
37
  MARKDOWN.register(["bold", "b"], ({ content }) => {
24
38
  return md.bold(content);
@@ -35,26 +49,59 @@ MARKDOWN.register(["emphasis", "e"], ({ content }) => {
35
49
  MARKDOWN.register(
36
50
  ["code", "Code", "codeBlock", "CodeBlock"],
37
51
  ({ args, content }) => {
38
- return md.codeBlock(content, args[0]);
52
+ const lang = safeArg(args, 0, "lang", null, null, "text");
53
+ return md.codeBlock(content, lang);
39
54
  },
40
- { escape: false }
55
+ {
56
+ escape: false,
57
+ rules: {
58
+ type: "AtBlock"
59
+ }
60
+ }
41
61
  );
42
62
  // Link
43
- MARKDOWN.register("link", ({ args, content }) => {
44
- const title = args[1] ? `"${args[1].trim()}"` : "";
45
- return md.url("link", content, args[0].trim(), title);
46
- });
63
+ MARKDOWN.register(
64
+ ["link", "Link"],
65
+ ({ args, content }) => {
66
+ const url = safeArg(args, 0, "url", null, null, "");
67
+ const title = safeArg(args, 1, "title", null, null, "");
68
+ return md.url("link", content, url, title);
69
+ },
70
+ {
71
+ rules: {
72
+ type: "Inline"
73
+ }
74
+ }
75
+ );
47
76
  // Image
48
- MARKDOWN.register("image", ({ args, content }) => {
49
- const title = args[1] ? `"${args[1].trim()}"` : "";
50
- return md.url("image", content, args[0].trim(), title);
51
- });
77
+ MARKDOWN.register(
78
+ ["image", "Image"],
79
+ ({ args, content }) => {
80
+ const url = safeArg(args, 0, "url", null, null, "");
81
+ const title = safeArg(args, 1, "title", null, null, "");
82
+ return md.url("image", content, url, title);
83
+ },
84
+ {
85
+ rules: {
86
+ type: "Inline"
87
+ }
88
+ }
89
+ );
52
90
  // Horizontal Rule
53
- MARKDOWN.register(["horizontal", "h"], ({ content }) => {
54
- return md.horizontal(content);
55
- });
91
+ MARKDOWN.register(
92
+ ["horizontal", "hr", "h"],
93
+ ({ args }) => {
94
+ const fmt = safeArg(args, 0, undefined, null, null, "*");
95
+ return md.horizontal(fmt);
96
+ },
97
+ {
98
+ rules: {
99
+ type: "Block"
100
+ }
101
+ }
102
+ );
56
103
  // Escape Characters
57
- MARKDOWN.register(["escape", "s"], ({ content }) => {
104
+ MARKDOWN.register(["escape", "Escape", "s"], ({ content }) => {
58
105
  return md.escape(content);
59
106
  });
60
107
  // Table
@@ -70,7 +117,7 @@ MARKDOWN.register(
70
117
  .map(line => line.trim())
71
118
  );
72
119
  },
73
- { escape: false }
120
+ { escape: false, rules: { type: "AtBlock" } }
74
121
  );
75
122
  // List
76
123
  MARKDOWN.register(
@@ -78,11 +125,12 @@ MARKDOWN.register(
78
125
  ({ content }) => {
79
126
  return content;
80
127
  },
81
- { escape: false }
128
+ { escape: false, rules: { type: "AtBlock" } }
82
129
  );
83
130
  // Todo
84
131
  MARKDOWN.register("todo", ({ args, content }) => {
85
132
  const checked = content === "x" ? true : false;
86
- return md.todo(checked, args && args[0] ? args[0].trim() : "");
133
+ const task = safeArg(args, 0, "task", null, null, "");
134
+ return md.todo(checked, task);
87
135
  });
88
136
  export default MARKDOWN;
package/mappers/mapper.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import TagBuilder from "../formatter/tag.js";
2
2
  import MarkdownBuilder from "../formatter/mark.js";
3
- import { highlightCode } from "../lib/highlight.js";
4
3
  import escapeHTML from "../helpers/escapeHTML.js";
5
4
  import atomOneDark from "../helpers/defaultTheme.js";
6
5
  import loadCss from "../helpers/loadCss.js";
6
+ import { sommarkError } from "../core/errors.js";
7
7
 
8
8
  class Mapper {
9
9
  #customHeaderContent;
@@ -28,15 +28,16 @@ class Mapper {
28
28
 
29
29
  this.#customHeaderContent = "";
30
30
 
31
- this.highlightCode = highlightCode;
31
+ this.highlightCode = null;
32
32
  this.escapeHTML = escapeHTML;
33
33
  this.styles = [];
34
34
  this.env = "node";
35
35
  // Theme Registry
36
36
  this.themes = {
37
+ "sommark-default": "pre{padding: 5px; background-color: #f6f8fa; font-family: monospace}",
37
38
  "atom-one-dark": atomOneDark
38
39
  };
39
- this.currentTheme = "atom-one-dark";
40
+ this.currentTheme = this.highlightCode && typeof this.highlightCode === "function" ? "atom-one-dark" : "sommark-default";
40
41
  this.enable_table_styles = true;
41
42
  }
42
43
 
@@ -171,12 +172,15 @@ class Mapper {
171
172
  // Formatters //
172
173
  // ========================================================================== //
173
174
  code = (args, content) => {
174
- const value = highlightCode(content, args && args[0] ? args[0].trim() : "text");
175
- return this.tag("pre").body(
176
- this.tag("code")
177
- .attributes({ class: `hljs language-${args && args[0] ? args[0].trim() : "text"}` })
178
- .body(value)
179
- );
175
+ const lang = this.safeArg(args, 0, "lang", null, null, "text");
176
+ const code = content || "";
177
+ let value = content;
178
+ const code_element = this.tag("code");
179
+ if (this.highlightCode && typeof this.highlightCode === "function") {
180
+ code_element.attributes({ class: `hljs language-${lang}` });
181
+ value = this.highlightCode(code, lang);
182
+ }
183
+ return this.tag("pre").body(code_element.body(value));
180
184
  };
181
185
  htmlTable = (data, headers, defaultStyle = true) => {
182
186
  const isAddedStyle = this.styles.some(s => s.includes(".sommark-table"));
@@ -312,5 +316,47 @@ class Mapper {
312
316
  todo(checked = false) {
313
317
  return checked.trim() === "x" || checked.trim().toLowerCase() === "done" ? true : false;
314
318
  }
319
+ safeArg = (args, index, key, type = null, setType = null, fallBack = null) => {
320
+ if (!Array.isArray(args)) {
321
+ sommarkError([`{line}<$red:TypeError:$> <$yellow:args must be an array$>{line}`]);
322
+ }
323
+
324
+ if (index === undefined && key === undefined) {
325
+ sommarkError([`{line}<$red:ReferenceError:> <$yellow:At least one of 'index' or 'key' must be provided$>{line}`]);
326
+ }
327
+
328
+ if (index !== undefined && typeof index !== "number") {
329
+ sommarkError([`{line}<$red:TypeError:$> <$yellow:index must be a number$>{line}`]);
330
+ }
331
+
332
+ if (key !== undefined && typeof key !== "string") {
333
+ sommarkError([`{line}<$red:TypeError:$> <$yellow:key must be a string$>{line}`]);
334
+ }
335
+
336
+ if (type !== null && typeof type !== "string") {
337
+ sommarkError([`{line}<$red:TypeError:$> <$yellow:type must be a string$>{line}`]);
338
+ }
339
+
340
+ if (setType !== null && typeof setType !== "function") {
341
+ sommarkError([`{line}<$red:TypeError:$> <$yellow:setType must be a function$>{line}`]);
342
+ }
343
+
344
+ const validate = value => {
345
+ if (value === undefined) return false;
346
+ if (!type) return true;
347
+ const evaluated = setType ? setType(value) : value;
348
+ return typeof evaluated === type;
349
+ };
350
+
351
+ if (index !== undefined && validate(args[index])) {
352
+ return args[index];
353
+ }
354
+
355
+ if (key !== undefined && validate(args[key])) {
356
+ return args[key];
357
+ }
358
+
359
+ return fallBack;
360
+ };
315
361
  }
316
362
  export default Mapper;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sommark",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "SomMark is a structural markup language for writing structured documents and converting them into HTML or Markdown or MDX(only ready components).",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -42,10 +42,7 @@
42
42
  "url": "https://github.com/Adam-Elmi/SomMark/issues"
43
43
  },
44
44
  "homepage": "https://github.com/Adam-Elmi/SomMark#readme",
45
- "dependencies": {
46
- "highlight.js": "^11.11.1"
47
- },
48
45
  "devDependencies": {
49
46
  "vitest": "^4.0.16"
50
47
  }
51
- }
48
+ }
package/lib/highlight.js DELETED
@@ -1,11 +0,0 @@
1
- import hljs from "highlight.js";
2
-
3
- function highlightCode(code, language = "plaintext") {
4
- if (!hljs.getLanguage(language)) {
5
- language = "plaintext";
6
- }
7
- return hljs.highlight(code, { language }).value;
8
- }
9
- export {
10
- highlightCode,
11
- };