sommark 2.3.2 → 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 -11
  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 +27 -32
  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 +100 -84
  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 -119
  45. package/helpers/loadCss.js +0 -46
@@ -2,7 +2,6 @@ import Mapper from "../mapper.js";
2
2
  import { TEXT } from "../../core/labels.js";
3
3
  import { transpilerError } from "../../core/errors.js";
4
4
 
5
- const Json = new Mapper();
6
5
 
7
6
  // ========================================================================== //
8
7
  // Helpers //
@@ -156,17 +155,37 @@ function renderInline(node, parentType) {
156
155
  }
157
156
  }
158
157
 
158
+ class JsonMapper extends Mapper {
159
+ constructor() {
160
+ super();
161
+ }
162
+
163
+ formatOutput(output) {
164
+ try {
165
+ return JSON.parse(JSON.stringify(output));
166
+ } catch (e) {
167
+ transpilerError([
168
+ "{line}<$red:JSON Format Error:$> ",
169
+ `<$yellow:Failed to parse generated JSON output.$>{N}<$cyan:Reason: $> <$magenta:'${e.message}'$>{line}`
170
+ ]);
171
+ return output;
172
+ }
173
+ }
174
+ }
175
+
176
+ const Json = new JsonMapper();
177
+
159
178
  // ========================================================================== //
160
179
  // Main Registration //
161
180
  // ========================================================================== //
162
181
 
163
182
  const noop = () => "";
164
- Json.register(["Object", "Array"], noop);
165
- Json.register(["string", "number", "bool", "null", "array", "none"], noop);
183
+ Json.register(["Object", "Array"], noop, { type: "Block" });
184
+ Json.register(["string", "number", "bool", "null", "array", "none"], noop, { type: "Inline" });
166
185
 
167
186
  Json.register("Json", ({ args, content, ast }) => {
168
187
  if (!ast) return "";
169
188
  return processNode(ast, null);
170
- });
189
+ }, { type: "Block" });
171
190
 
172
191
  export default Json;
@@ -1,112 +1,126 @@
1
1
  import Mapper from "../mapper.js";
2
- const MARKDOWN = new Mapper();
2
+ import HTML from "./html.js";
3
+ import { todo } from "../../helpers/utils.js";
4
+
5
+ class MarkdownMapper extends Mapper {
6
+ constructor() {
7
+ super();
8
+ }
9
+ comment(text) {
10
+ return `<!--${text.replace("#", "")}-->\n`;
11
+ }
12
+ formatOutput(output, includeDocument) {
13
+ const todoRegex = /@@TODO_BLOCK:([\s\S]*?):([\s\S]*?)@@/g;
14
+ const statusMarkers = ["done", "x", "X", "-", ""];
15
+ return output.replace(todoRegex, (match, body, arg0) => {
16
+ const bodyTrimmed = body.trim().toLowerCase();
17
+ const arg0Trimmed = arg0.trim().toLowerCase();
18
+
19
+ const bodyIsStatus = statusMarkers.includes(bodyTrimmed);
20
+ const arg0IsStatus = statusMarkers.includes(arg0Trimmed);
21
+
22
+ let finalStatus = arg0; // Default: arg is status
23
+ let finalTask = body; // Default: body is task
24
+
25
+ if (bodyIsStatus && !arg0IsStatus) {
26
+ finalStatus = body;
27
+ finalTask = arg0;
28
+ }
29
+
30
+ return this.md.todo(todo(finalStatus), finalTask);
31
+ });
32
+ }
33
+ }
34
+
35
+ const MARKDOWN = new MarkdownMapper();
36
+ MARKDOWN.inherit(HTML);
3
37
  const { md, safeArg } = MARKDOWN;
4
38
  // Block
5
- MARKDOWN.register(["Block", "block", "Section", "section"], ({ content }) => {
39
+ MARKDOWN.register("Block", ({ content }) => {
6
40
  return content;
41
+ }, { type: "Block" });
42
+ // Quote
43
+ MARKDOWN.register(["quote", "blockquote"], ({ content }) => {
44
+ return "\n" + content.trimEnd()
45
+ .split("\n")
46
+ .map(line => `> ${line}`)
47
+ .join("\n");
48
+ }, { type: "Block" });
49
+ // Headings (Block only, like HTML mapper)
50
+ ["h1", "h2", "h3", "h4", "h5", "h6"].forEach(heading => {
51
+ MARKDOWN.register(heading, ({ content }) => {
52
+ const lvl = heading[1] && typeof Number(heading[1]) === "number" ? heading[1] : 1;
53
+ return md.heading(content, lvl);
54
+ }, { type: "Block" });
7
55
  });
8
- // Headings
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
- );
22
- // Inline Headings
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
- );
36
56
  // Bold
37
- MARKDOWN.register(["bold", "b"], ({ content }) => {
57
+ MARKDOWN.register("bold", ({ content }) => {
38
58
  return md.bold(content);
39
- });
59
+ }, { type: "any" });
40
60
  // Italic
41
- MARKDOWN.register(["italic", "i"], ({ content }) => {
61
+ MARKDOWN.register("italic", ({ content }) => {
42
62
  return md.italic(content);
43
- });
63
+ }, { type: "any" });
44
64
  // Bold and Italic (emphasis)
45
- MARKDOWN.register(["emphasis", "e"], ({ content }) => {
65
+ MARKDOWN.register("emphasis", ({ content }) => {
46
66
  return md.emphasis(content);
47
- });
67
+ }, { type: "any" });
48
68
  // Code Blocks
49
69
  MARKDOWN.register(
50
- ["code", "Code", "codeBlock", "CodeBlock"],
70
+ "Code",
51
71
  ({ args, content }) => {
52
72
  const lang = safeArg(args, 0, "lang", null, null, "text");
53
73
  return md.codeBlock(content, lang);
54
74
  },
55
75
  {
56
76
  escape: false,
57
- rules: {
58
- type: "AtBlock"
59
- }
77
+ type: "AtBlock"
60
78
  }
61
79
  );
62
80
  // Link
63
81
  MARKDOWN.register(
64
- ["link", "Link"],
65
- ({ args, content }) => {
66
- const url = safeArg(args, 0, "url", null, null, "");
82
+ "link",
83
+ ({ args }) => {
84
+ const url = safeArg(args, 0, "src", null, null, "");
67
85
  const title = safeArg(args, 1, "title", null, null, "");
68
- return md.url("link", content, url, title);
86
+ const text = safeArg(args, 2, "alt", null, null, "");
87
+ return md.url("link", text, url, title);
69
88
  },
70
89
  {
71
- rules: {
72
- type: "Inline"
73
- }
90
+ type: "Block"
74
91
  }
75
92
  );
76
93
  // Image
77
94
  MARKDOWN.register(
78
- ["image", "Image"],
79
- ({ args, content }) => {
80
- const url = safeArg(args, 0, "url", null, null, "");
95
+ "image",
96
+ ({ args }) => {
97
+ const url = safeArg(args, 0, "src", null, null, "");
81
98
  const title = safeArg(args, 1, "title", null, null, "");
82
- return md.url("image", content, url, title);
99
+ const alt = safeArg(args, 2, "alt", null, null, "");
100
+ return md.url("image", alt, url, title);
83
101
  },
84
102
  {
85
- rules: {
86
- type: "Inline"
87
- }
103
+ type: "Block"
88
104
  }
89
105
  );
90
106
  // Horizontal Rule
91
107
  MARKDOWN.register(
92
- ["horizontal", "hr", "h"],
108
+ "hr",
93
109
  ({ args }) => {
94
110
  const fmt = safeArg(args, 0, undefined, null, null, "*");
95
111
  return md.horizontal(fmt);
96
112
  },
97
113
  {
98
- rules: {
99
- type: "Block"
100
- }
114
+ type: "Block"
101
115
  }
102
116
  );
103
117
  // Escape Characters
104
- MARKDOWN.register(["escape", "Escape", "s"], ({ content }) => {
118
+ MARKDOWN.register("escape", ({ content }) => {
105
119
  return md.escape(content);
106
- });
120
+ }, { type: "any" });
107
121
  // Table
108
122
  MARKDOWN.register(
109
- "table",
123
+ "Table",
110
124
  ({ args, content }) => {
111
125
  return md.table(
112
126
  args,
@@ -117,20 +131,27 @@ MARKDOWN.register(
117
131
  .map(line => line.trim())
118
132
  );
119
133
  },
120
- { escape: false, rules: { type: "AtBlock" } }
134
+ { escape: false, type: "AtBlock" }
121
135
  );
122
136
  // List
123
137
  MARKDOWN.register(
124
- ["list", "List"],
138
+ "list",
125
139
  ({ content }) => {
126
140
  return content;
127
141
  },
128
- { escape: false, rules: { type: "AtBlock" } }
142
+ { escape: false, type: "AtBlock" }
129
143
  );
130
144
  // Todo
131
145
  MARKDOWN.register("todo", ({ args, content }) => {
132
- const checked = content === "x" ? true : false;
133
- const task = safeArg(args, 0, "task", null, null, "");
146
+ const isPlaceholder = content.includes("__SOMMARK_BODY_PLACEHOLDER_");
147
+ if (isPlaceholder) {
148
+ return `@@TODO_BLOCK:${content}:${args[0] || ""}@@`;
149
+ }
150
+ const statusMarkers = ["done", "x", "X", "-", ""];
151
+ const isInline = !isPlaceholder && statusMarkers.includes(content.trim().toLowerCase()) && args.length > 0;
152
+ const status = isInline ? content : (args[0] || "");
153
+ const task = isInline ? (args[0] || "") : content;
154
+ const checked = todo(status);
134
155
  return md.todo(checked, task);
135
- });
156
+ }, { type: "any" });
136
157
  export default MARKDOWN;
@@ -1,5 +1,133 @@
1
1
  import Mapper from "../mapper.js";
2
2
  import MARKDOWN from "./markdown.js";
3
- const MDX = new Mapper();
4
- MDX.outputs = MARKDOWN.outputs;
3
+ import { HTML_TAGS } from "../../constants/html_tags.js";
4
+ import { HTML_PROPS } from "../../constants/html_props.js";
5
+ import { VOID_ELEMENTS } from "../../constants/void_elements.js";
6
+ import kebabize from "../../helpers/kebabize.js";
7
+
8
+ class MdxMapper extends Mapper {
9
+ constructor() {
10
+ super();
11
+ }
12
+ comment(text) {
13
+ return `{/*${text.replace("#", "")} */}\n`;
14
+ }
15
+ getUnknownTag(node) {
16
+ const tagName = node.id;
17
+ return {
18
+ render: ({ args, content }) => {
19
+ const element = this.tag(tagName);
20
+ element.props(this.jsxProps(args, tagName));
21
+ return element.body(content);
22
+ }
23
+ };
24
+ }
25
+
26
+ jsxProps(args, tagName = "div") {
27
+ const jsxProps = [];
28
+ const styleObj = {};
29
+ const isHtmlTag = HTML_TAGS.has(tagName.toLowerCase());
30
+
31
+ const keys = Object.keys(args).filter(arg => isNaN(arg));
32
+ keys.forEach(key => {
33
+ let val = args[key];
34
+ const isEvent = key.toLowerCase().startsWith("on");
35
+
36
+ let k = key;
37
+ if (k === "class") k = "className";
38
+
39
+ // Quote stripping
40
+ if (typeof val === "string" && ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")))) {
41
+ val = val.slice(1, -1);
42
+ }
43
+
44
+ if (k === "style") {
45
+ if (typeof val === "string") {
46
+ const pairs = val.includes(";") ? val.split(";") : val.split(",");
47
+ pairs.forEach(pair => {
48
+ let [prop, value] = pair.split(":").map(s => s.trim());
49
+ if (prop && value) {
50
+ const camelProp = prop.replace(/-([a-z])/g, g => g[1].toUpperCase());
51
+ styleObj[camelProp] = value;
52
+ }
53
+ });
54
+ } else if (typeof val === "object") {
55
+ Object.assign(styleObj, val);
56
+ }
57
+ } else {
58
+ // Detection for expressions
59
+ const isBoolean = val === "true" || val === "false" || typeof val === "boolean";
60
+ const isNumeric = val !== "" && !isNaN(val) && typeof val !== "boolean";
61
+ const looksLikeExpression = typeof val === "string" &&
62
+ (/[0-9]/.test(val) && /[+\-*/%()]/.test(val)); // Math expression detection
63
+
64
+ const shouldBeJSXExpression = isEvent || isBoolean || isNumeric || looksLikeExpression;
65
+
66
+ let finalVal = val;
67
+ if (val === "true") finalVal = true;
68
+ if (val === "false") finalVal = false;
69
+ if (isNumeric && typeof val === "string") finalVal = Number(val);
70
+
71
+ jsxProps.push({
72
+ __type__: shouldBeJSXExpression ? "other" : "string",
73
+ [k]: finalVal
74
+ });
75
+ }
76
+ });
77
+
78
+ if (Object.keys(styleObj).length > 0) {
79
+ const styleStr = JSON.stringify(styleObj).replace(/"/g, "'").replace(/'([^']+)':/g, '$1:');
80
+ jsxProps.push({ __type__: "other", style: styleStr });
81
+ }
82
+
83
+ return jsxProps;
84
+ }
85
+ }
86
+
87
+ const MDX = new MdxMapper();
88
+ const { tag } = MDX;
89
+
90
+ MDX.inherit(MARKDOWN);
91
+
92
+ // Block for raw MDX content (ESM, etc.)
93
+ MDX.register("mdx", ({ content }) => content, { escape: false, type: "Block" });
94
+
95
+ // Re-register HTML tags to use jsxProps
96
+ HTML_TAGS.forEach(tagName => {
97
+ const capitalized = tagName.charAt(0).toUpperCase() + tagName.slice(1);
98
+
99
+ // Register even if it exists in MARKDOWN to override it with JSX version
100
+ const idsToRegister = [tagName, capitalized];
101
+
102
+ MDX.register(
103
+ idsToRegister,
104
+ ({ args, content }) => {
105
+ const element = tag(tagName);
106
+
107
+ // Auto-ID for Headings
108
+ if (/^h[1-6]$/i.test(tagName) && !args.id && content && /^[A-Za-z0-9]/.test(content)) {
109
+ const id = content
110
+ .toString()
111
+ .toLowerCase()
112
+ .replace(/[^\w\s-]/g, "")
113
+ .replace(/\s+/g, "-");
114
+ args.id = id;
115
+ }
116
+
117
+ element.props(MDX.jsxProps(args, tagName));
118
+
119
+ if (VOID_ELEMENTS.has(tagName)) {
120
+ return element.selfClose();
121
+ }
122
+ return element.body(content);
123
+ },
124
+ {
125
+ type: VOID_ELEMENTS.has(tagName) ? "Block" : ["Block", "Inline"],
126
+ rules: {
127
+ is_self_closing: VOID_ELEMENTS.has(tagName)
128
+ }
129
+ }
130
+ );
131
+ });
132
+
5
133
  export default MDX;