sommark 3.3.3 → 4.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 (61) hide show
  1. package/README.md +98 -82
  2. package/assets/logo.json +28 -0
  3. package/assets/smark.logo.png +0 -0
  4. package/assets/smark.logo.svg +21 -0
  5. package/cli/cli.mjs +8 -16
  6. package/cli/commands/build.js +24 -4
  7. package/cli/commands/color.js +22 -26
  8. package/cli/commands/help.js +10 -10
  9. package/cli/commands/init.js +19 -42
  10. package/cli/commands/print.js +20 -12
  11. package/cli/commands/show.js +4 -0
  12. package/cli/commands/version.js +6 -0
  13. package/cli/constants.js +9 -5
  14. package/cli/helpers/config.js +11 -0
  15. package/cli/helpers/file.js +17 -6
  16. package/cli/helpers/transpile.js +7 -8
  17. package/core/errors.js +49 -25
  18. package/core/formats.js +7 -3
  19. package/core/formatter.js +215 -0
  20. package/core/helpers/config-loader.js +37 -56
  21. package/core/labels.js +21 -9
  22. package/core/lexer.js +491 -212
  23. package/core/modules.js +164 -0
  24. package/core/parser.js +516 -389
  25. package/core/tokenTypes.js +36 -1
  26. package/core/transpiler.js +237 -151
  27. package/core/validator.js +79 -0
  28. package/formatter/mark.js +203 -43
  29. package/formatter/tag.js +202 -32
  30. package/grammar.ebnf +57 -50
  31. package/helpers/colorize.js +26 -13
  32. package/helpers/escapeHTML.js +13 -6
  33. package/helpers/kebabize.js +6 -0
  34. package/helpers/peek.js +9 -0
  35. package/helpers/removeChar.js +26 -13
  36. package/helpers/safeDataParser.js +114 -0
  37. package/helpers/utils.js +140 -158
  38. package/index.js +198 -188
  39. package/mappers/languages/html.js +105 -213
  40. package/mappers/languages/json.js +122 -171
  41. package/mappers/languages/markdown.js +355 -108
  42. package/mappers/languages/mdx.js +76 -114
  43. package/mappers/languages/xml.js +114 -0
  44. package/mappers/mapper.js +152 -123
  45. package/mappers/shared/index.js +22 -0
  46. package/package.json +26 -6
  47. package/SOMMARK-SPEC.md +0 -481
  48. package/cli/commands/list.js +0 -124
  49. package/constants/html_tags.js +0 -146
  50. package/core/pluginManager.js +0 -149
  51. package/core/plugins/comment-remover.js +0 -47
  52. package/core/plugins/module-system.js +0 -176
  53. package/core/plugins/raw-content-plugin.js +0 -78
  54. package/core/plugins/rules-validation-plugin.js +0 -231
  55. package/core/plugins/sommark-format.js +0 -244
  56. package/coverage_test.js +0 -21
  57. package/debug.js +0 -15
  58. package/helpers/camelize.js +0 -2
  59. package/helpers/defaultTheme.js +0 -3
  60. package/test_format_fix.js +0 -42
  61. package/v3-todo.smark +0 -73
@@ -1,157 +1,404 @@
1
1
  import Mapper from "../mapper.js";
2
2
  import HTML from "./html.js";
3
- import { todo } from "../../helpers/utils.js";
3
+ import { registerSharedOutputs } from "../shared/index.js";
4
+ import { BLOCK, TEXT} from "../../core/labels.js";
5
+ import transpiler from "../../core/transpiler.js";
6
+ import { VOID_ELEMENTS } from "../../constants/void_elements.js";
4
7
 
5
- class MarkdownMapper extends Mapper {
6
- constructor() {
7
- super();
8
- }
8
+ /**
9
+ * The Markdown Mapper used for generating Markdown text.
10
+ */
11
+ const MARKDOWN = Mapper.define({
12
+ options: {
13
+ trimAndWrapBlocks: false
14
+ },
15
+ /**
16
+ * Renders an HTML-style comment in Markdown output.
17
+ */
9
18
  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;
19
+ return `<!-- ${text} -->`;
20
+ },
21
+ /**
22
+ * Formats a plain text node with Markdown escaping only.
23
+ */
24
+ text(text, options) {
25
+ if (options?.escape === false) return text;
26
+ // Use smartEscaper to protect special characters like math
27
+ let out = text;
28
+ if (this.md && this.md.smartEscaper) out = this.md.smartEscaper(out);
29
+ return out;
30
+ },
31
+
32
+ /**
33
+ * Formats inline content before rendering.
34
+ */
35
+ inlineText(text, options) {
36
+ if (options?.escape !== false) {
37
+ // Use smartEscaper to protect special characters
38
+ let out = text;
39
+ if (this.md && this.md.smartEscaper) out = this.md.smartEscaper(out);
40
+ return out;
41
+ }
42
+ return text;
43
+ },
44
+
45
+ /**
46
+ * Formats the literal content inside AtBlocks.
47
+ */
48
+ atBlockBody(text, options) {
49
+ if (options?.escape === false) return text;
50
+ // Escaping with smartEscaper
51
+ let out = text;
52
+ if (this.md && this.md.smartEscaper) out = this.md.smartEscaper(out);
53
+ return out;
54
+ },
55
+
56
+ /**
57
+ * Provides a fallback for unknown tags by using the HTML mapper instead.
58
+ */
59
+ getUnknownTag(node) {
60
+ const isBlock = node.type === BLOCK;
61
+ const id = node.id.toLowerCase();
62
+
63
+ return {
64
+ render: async (ctx) => {
65
+ const { args, ast } = ctx;
66
+ const body = ast && ast.body ? ast.body : [];
67
+ const meaningful = body.filter(c => c.type !== TEXT || c.text.trim());
68
+ const childCount = meaningful.length;
69
+ const element = this.tag(id).smartAttributes(args, this.customProps);
70
+
71
+ // Use the transpiler to format the children if any, otherwise use direct content
72
+ let rawContent;
73
+ if (node.type === "AtBlock") {
74
+ rawContent = node.content || "";
75
+ rawContent = this.atBlockBody(rawContent, ctx);
76
+ } else if (node.type === "Inline") {
77
+ rawContent = node.value || "";
78
+ rawContent = this.inlineText(rawContent, ctx);
79
+ } else {
80
+ rawContent = (await transpiler({ ast: body, mapperFile: this })).trim();
81
+ }
82
+
83
+ if (VOID_ELEMENTS.has(id)) {
84
+ return element.selfClose();
85
+ }
86
+
87
+ let finalContent;
88
+ if (childCount <= 1) {
89
+ // COMPACT PASS: Single child or empty
90
+ finalContent = rawContent;
91
+ } else {
92
+ // MULTILINE PASS: Enforce \n prefix/suffix for multiple children
93
+ finalContent = `\n${rawContent}\n`;
94
+ }
95
+
96
+ return element.body(finalContent);
97
+ },
98
+ options: {
99
+ type: isBlock ? "Block" : (node.type === "AtBlock" ? "AtBlock" : "Inline"),
100
+ handleAst: true
28
101
  }
29
-
30
- return this.md.todo(todo(finalStatus), finalTask);
31
- });
102
+ };
32
103
  }
33
- }
104
+ });
34
105
 
35
- const MARKDOWN = new MarkdownMapper();
36
106
  MARKDOWN.inherit(HTML);
37
107
  const { md, safeArg } = MARKDOWN;
38
- // Block
39
- MARKDOWN.register("Block", ({ content }) => {
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)
108
+ registerSharedOutputs(MARKDOWN);
109
+
110
+ /**
111
+ * Quote - Renders blockquote content or GFM alerts.
112
+ */
113
+ MARKDOWN.register("quote", ({ args, content }) => {
114
+ const type = safeArg({ args, index: 0, key: "type", fallBack: "" });
115
+ return md.quote(content, type);
116
+ }, { type: "Block", resolve: true });
117
+
118
+ /**
119
+ * Unified heading renderer for Markdown and MDX mappers.
120
+ * @param {Object} options - Mapper context and args.
121
+ * @param {string} defaultFormat - Default format ("markdown" or "html").
122
+ * @returns {string} - Rendered heading.
123
+ */
124
+ export function renderHeading({ args, content, ast }, defaultFormat = "markdown") {
125
+ const heading = ast.id;
126
+ const format = safeArg({ args, index: 0, key: "format", type: "string", fallBack: defaultFormat });
127
+ const lvl = heading[1] && !isNaN(Number(heading[1])) ? Number(heading[1]) : 1;
128
+
129
+ // Remove formatting arguments before checking for attributes
130
+ const cleanArgs = { ...args };
131
+ delete cleanArgs.format;
132
+ delete cleanArgs["0"]; // Clean positional 'format'
133
+
134
+ const hasAttributes = Object.keys(cleanArgs).length > 0;
135
+
136
+ // Hybrid Dispatch: Switch to HTML if format is requested OR if attributes are present
137
+ if (format === "html" || hasAttributes) {
138
+ let htmlTarget = HTML.get(heading);
139
+ if (!htmlTarget) {
140
+ htmlTarget = HTML.getUnknownTag(ast);
141
+ }
142
+
143
+ if (htmlTarget) {
144
+ return htmlTarget.render.call(this, { args: cleanArgs, content, ast, nodeType: ast.type });
145
+ }
146
+ }
147
+
148
+ return md.heading(content, lvl);
149
+ }
150
+
151
+ /**
152
+ * Headings - Renders H1-H6 block headings.
153
+ */
50
154
  ["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);
155
+ MARKDOWN.register(heading, function (ctx) {
156
+ return renderHeading.call(this, ctx, "markdown");
54
157
  }, { type: "Block" });
55
158
  });
56
- // Bold
57
- MARKDOWN.register("bold", ({ content }) => {
159
+
160
+ /**
161
+ * Bold - Renders bold text (**text**).
162
+ */
163
+ MARKDOWN.register(["bold", "b"], ({ content }) => {
58
164
  return md.bold(content);
59
- }, { type: "any" });
60
- // Italic
61
- MARKDOWN.register("italic", ({ content }) => {
165
+ }, { type: ["Block", "Inline"] });
166
+
167
+ /**
168
+ * Italic - Renders italic text (*text*).
169
+ */
170
+ MARKDOWN.register(["italic", "i"], ({ content }) => {
62
171
  return md.italic(content);
63
- }, { type: "any" });
64
- // Bold and Italic (emphasis)
65
- MARKDOWN.register("emphasis", ({ content }) => {
172
+ }, { type: ["Block", "Inline"] });
173
+
174
+ /**
175
+ * Emphasis - Renders bold-italic text (***text***).
176
+ */
177
+ MARKDOWN.register(["emphasis", "em"], ({ content }) => {
66
178
  return md.emphasis(content);
67
- }, { type: "any" });
68
- // Code Blocks
179
+ }, { type: ["Block", "Inline"] });
180
+
181
+ /**
182
+ * Strike - Renders strikethrough text (~~text~~).
183
+ */
184
+ MARKDOWN.register(["strike", "s"], ({ content }) => {
185
+ return md.strike(content);
186
+ }, { type: ["Block", "Inline"] });
187
+
188
+ /**
189
+ * Code - Renders inline or fenced code blocks.
190
+ */
69
191
  MARKDOWN.register(
70
- "Code",
71
- ({ args, content }) => {
72
- const lang = safeArg(args, 0, "lang", null, null, "text");
192
+ ["Code", "code"],
193
+ ({ args, content, nodeType }) => {
194
+ if (nodeType === "Inline") {
195
+ return `\`${content}\``;
196
+ }
197
+ const lang = safeArg({ args, index: 0, key: "lang", fallBack: "text" });
73
198
  return md.codeBlock(content, lang);
74
199
  },
75
200
  {
76
201
  escape: false,
77
- type: ["AtBlock", "Block"]
202
+ type: ["AtBlock", "Inline"]
78
203
  }
79
204
  );
80
- // Link
205
+
206
+ /**
207
+ * Link - Renders Markdown links [text](url).
208
+ */
81
209
  MARKDOWN.register(
82
210
  "link",
83
- ({ args }) => {
84
- const url = safeArg(args, 0, "src", null, null, "");
85
- const title = safeArg(args, 1, "title", null, null, "");
86
- const text = safeArg(args, 2, "alt", null, null, "");
87
- return md.url("link", text, url, title);
211
+ ({ args, content }) => {
212
+ const src = safeArg({ args, index: 0, key: "src", fallBack: "" });
213
+ const title = safeArg({ args, index: 1, key: "title", fallBack: "" });
214
+ return md.url("link", content, src, title);
88
215
  },
89
216
  {
90
- type: "Block"
217
+ type: ["Block", "Inline"],
218
+ rules: { is_self_closing: false }
91
219
  }
92
220
  );
93
- // Image
221
+
222
+ /**
223
+ * Image - Renders Markdown images ![alt](url).
224
+ */
94
225
  MARKDOWN.register(
95
226
  "image",
96
227
  ({ args }) => {
97
- const url = safeArg(args, 0, "src", null, null, "");
98
- const title = safeArg(args, 1, "title", null, null, "");
99
- const alt = safeArg(args, 2, "alt", null, null, "");
100
- return md.url("image", alt, url, title);
228
+ const alt = safeArg({ args, index: 0, key: "alt", fallBack: "" });
229
+ const src = safeArg({ args, index: 1, key: "src", fallBack: "" });
230
+ const title = safeArg({ args, index: 2, key: "title", fallBack: "" });
231
+ return md.url("image", alt, src, title);
101
232
  },
102
233
  {
103
- type: "Block"
234
+ type: "Block",
235
+ rules: { is_self_closing: true }
104
236
  }
105
237
  );
106
- // Horizontal Rule
238
+
239
+ /**
240
+ * HR (Horizontal Rule) - Renders a thematic break (---).
241
+ */
107
242
  MARKDOWN.register(
108
243
  "hr",
109
244
  ({ args }) => {
110
- const fmt = safeArg(args, 0, undefined, null, null, "*");
245
+ const fmt = safeArg({ args, index: 0, fallBack: "-" });
111
246
  return md.horizontal(fmt);
112
247
  },
113
248
  {
114
- type: "Block"
249
+ type: "Block",
250
+ rules: { is_self_closing: true }
115
251
  }
116
252
  );
117
- // Escape Characters
118
- MARKDOWN.register("escape", ({ content }) => {
119
- return md.escape(content);
120
- }, { type: "any" });
121
- // Table
253
+
254
+ /**
255
+ * Escape - Escapes special Markdown characters.
256
+ */
257
+ MARKDOWN.register(["escape", "e"], function ({ content }) {
258
+ return this.md.escape(content);
259
+ }, { type: ["Block", "Inline"], resolve: true });
260
+
261
+ /**
262
+ * Table - Authoritative Native AST Table resolution.
263
+ * Processes Header/Body sections with Row/Cell nesting.
264
+ */
122
265
  MARKDOWN.register(
123
266
  "Table",
124
- ({ args, content }) => {
125
- return md.table(
126
- args,
127
- content
128
- .trim()
129
- .split("\n")
130
- .filter(line => line !== "")
131
- .map(line => line.trim())
132
- );
133
- },
134
- { escape: false, type: "AtBlock" }
135
- );
136
- // List
137
- MARKDOWN.register(
138
- "list",
139
- ({ content }) => {
140
- return content;
267
+ async function ({ ast }) {
268
+ const headers = [];
269
+ const rows = [];
270
+
271
+ const extractCells = async (node) => {
272
+ const cells = [];
273
+ if (!node || !node.body) return cells;
274
+ // Trim empty spaces while keeping line breaks
275
+ const cellAst = node.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
276
+ .filter(n => n.type !== TEXT || n.text);
277
+
278
+ for (const child of cellAst) {
279
+ if (child.type === BLOCK && (child.id.toLowerCase() === "cell" || child.id.toLowerCase() === "th" || child.id.toLowerCase() === "td")) {
280
+ const cellContent = await transpiler({ ast: child.body, mapperFile: this });
281
+ cells.push(cellContent.trim());
282
+ }
283
+ }
284
+ return cells;
285
+ };
286
+
287
+ const extractRows = async (sectionNode) => {
288
+ if (!sectionNode || !sectionNode.body) return [];
289
+ const sectionRows = [];
290
+ // Trim empty spaces while keeping line breaks
291
+ const rowAst = sectionNode.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
292
+ .filter(n => n.type !== TEXT || n.text);
293
+ for (const rowNode of rowAst) {
294
+ if (rowNode.type === BLOCK && rowNode.id.toLowerCase() === "row") {
295
+ const rowData = await extractCells(rowNode);
296
+ if (rowData.length > 0) sectionRows.push(rowData);
297
+ }
298
+ }
299
+ return sectionRows;
300
+ };
301
+
302
+ const processTable = async () => {
303
+ // Remove empty text blocks
304
+ const tableNodes = ast.body.filter(n => n.type !== TEXT || n.text.trim());
305
+ for (const node of tableNodes) {
306
+ if (node.type !== BLOCK) continue;
307
+
308
+ const id = node.id.toLowerCase();
309
+ if (id === "header") {
310
+ const headerRows = await extractRows(node);
311
+ if (headerRows.length > 0) {
312
+ headers.push(...headerRows[0]);
313
+ }
314
+ } else if (id === "body") {
315
+ const bodyRows = await extractRows(node);
316
+ rows.push(...bodyRows);
317
+ }
318
+ }
319
+ return md.table(headers, rows);
320
+ };
321
+
322
+ return processTable();
141
323
  },
142
- { escape: false, type: "AtBlock" }
324
+ {
325
+ escape: true,
326
+ type: "Block",
327
+ handleAst: true,
328
+ trimAndWrapBlocks: false
329
+ }
143
330
  );
144
- // Todo
145
- MARKDOWN.register("todo", ({ args, content }) => {
146
- const isPlaceholder = content.includes("__SOMMARK_BODY_PLACEHOLDER_");
147
- if (isPlaceholder) {
148
- return `@@TODO_BLOCK:${content}:${args[0] || ""}@@`;
331
+
332
+ /**
333
+ * Table Helpers - Internal tags for table structural organization.
334
+ */
335
+ MARKDOWN.register(["header", "body", "row", "cell"], ({ content }) => content);
336
+
337
+ /**
338
+ * Lists - Authoritative Native AST List resolution.
339
+ * Supports Ordered (Number) and Unordered (Dotlex) lists with deep nesting.
340
+ */
341
+ MARKDOWN.register(["list", "List"], async function ({ ast, args }) {
342
+ const items = [];
343
+
344
+ // Determine list type (dot/unordered vs number/ordered)
345
+ const indicator = safeArg({ args, index: 0, fallBack: "dot" });
346
+ const isOrdered = indicator === "number" || indicator === "ol";
347
+ let marker = "-";
348
+
349
+ if (!isOrdered) {
350
+ if (indicator === "dot") marker = "-";
351
+ else marker = indicator; // Custom symbol like "*" or "+"
149
352
  }
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);
155
- return md.todo(checked, task);
156
- }, { type: "any" });
353
+
354
+ // Remove empty spaces from the start and end of strings
355
+ const itemNodes = ast.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
356
+ .filter(n => n.type !== TEXT || n.text);
357
+
358
+ for (const node of itemNodes) {
359
+ const id = node.id?.toLowerCase();
360
+ if (node.type === BLOCK && (id === "item")) {
361
+ // Trim spaces inside the list item
362
+ const itemBody = node.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
363
+ .filter(n => n.type !== TEXT || n.text);
364
+ const itemContent = await transpiler({ ast: itemBody, mapperFile: this });
365
+ items.push(itemContent.trim());
366
+ } else if (node.type === BLOCK && (id === "list")) {
367
+ // Add nested lists to the latest item
368
+ if (items.length > 0) {
369
+ const listContent = await transpiler({ ast: [node], mapperFile: this });
370
+ items[items.length - 1] += "\n" + listContent;
371
+ }
372
+ }
373
+ }
374
+
375
+ const result = isOrdered
376
+ ? md.orderedList(items, 0)
377
+ : md.unorderedList(items, 0, marker);
378
+
379
+ return result;
380
+ }, { type: "Block", handleAst: true, trimAndWrapBlocks: false });
381
+
382
+ /**
383
+ * List Helpers - Internal tags for list structural organization.
384
+ */
385
+ MARKDOWN.register(["item", "Item"], async function ({ ast }) {
386
+ // Trim whitespace but keep line breaks
387
+ const bodyAst = ast.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
388
+ .filter(n => n.type !== TEXT || n.text);
389
+ return await transpiler({ ast: bodyAst, mapperFile: this });
390
+ }, { type: "Block", handleAst: true, trimAndWrapBlocks: false });
391
+
392
+ /**
393
+ * Todo - Renders task list items with status markers.
394
+ */
395
+ MARKDOWN.register("todo", ({ args, content }) => {
396
+ const statusMarkers = ["done", "x", "-", ""].map(s => s.toLowerCase());
397
+
398
+ const isInlineStatus = statusMarkers.includes(content.toLowerCase());
399
+ const status = isInlineStatus ? content : (args[0] || "");
400
+ const task = isInlineStatus ? (args[0] || "") : content;
401
+
402
+ return md.todo(status, task);
403
+ }, { type: "Block", trimAndWrapBlocks: false });
157
404
  export default MARKDOWN;