sommark 4.5.3 → 5.0.1

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 (41) hide show
  1. package/README.md +315 -179
  2. package/cli/cli.mjs +1 -1
  3. package/cli/commands/color.js +36 -14
  4. package/cli/commands/help.js +3 -0
  5. package/cli/commands/init.js +1 -3
  6. package/cli/constants.js +5 -2
  7. package/constants/html_props.js +0 -5
  8. package/core/errors.js +5 -4
  9. package/core/evaluator.js +1 -2
  10. package/core/formats.js +7 -1
  11. package/core/helpers/config-loader.js +2 -4
  12. package/core/helpers/lib.js +1 -1
  13. package/core/labels.js +2 -15
  14. package/core/lexer.js +197 -313
  15. package/core/modules.js +13 -13
  16. package/core/parser.js +226 -535
  17. package/core/tokenTypes.js +6 -15
  18. package/core/transpiler.js +129 -110
  19. package/core/validator.js +6 -26
  20. package/dist/sommark.browser.js +1781 -2172
  21. package/dist/sommark.browser.lite.js +1779 -2169
  22. package/dist/sommark.lexer.js +392 -544
  23. package/dist/sommark.parser.js +604 -1200
  24. package/formatter/mark.js +34 -0
  25. package/formatter/tag.js +7 -33
  26. package/helpers/utils.js +15 -16
  27. package/index.js +9 -1
  28. package/index.shared.js +26 -16
  29. package/mappers/languages/csv.js +62 -0
  30. package/mappers/languages/html.js +12 -66
  31. package/mappers/languages/json.js +74 -156
  32. package/mappers/languages/jsonc.js +21 -63
  33. package/mappers/languages/markdown.js +159 -276
  34. package/mappers/languages/mdx.js +7 -62
  35. package/mappers/languages/text.js +2 -19
  36. package/mappers/languages/toml.js +231 -0
  37. package/mappers/languages/xml.js +25 -25
  38. package/mappers/languages/yaml.js +323 -0
  39. package/mappers/mapper.js +1 -22
  40. package/mappers/shared/index.js +3 -16
  41. package/package.json +5 -2
@@ -1,68 +1,8 @@
1
1
  import Mapper from "../mapper.js";
2
2
  import HTML from "./html.js";
3
3
  import { registerSharedOutputs } from "../shared/index.js";
4
- import { BLOCK, TEXT, INLINE, STATIC_LOGIC } from "../../core/labels.js";
4
+ import { BLOCK, TEXT, FOR_EACH } from "../../core/labels.js";
5
5
  import { VOID_ELEMENTS } from "../../constants/void_elements.js";
6
- import evaluator from "../../core/evaluator.js";
7
- import { matchedValue } from "../../helpers/utils.js";
8
-
9
- /**
10
- * Helper to manually render AST children inside handleAst blocks,
11
- * avoiding the need to call the core transpiler recursively.
12
- */
13
- async function renderNodeAst(astArray, mapperFile) {
14
- if (!astArray || !Array.isArray(astArray)) return "";
15
- let result = "";
16
- for (const node of astArray) {
17
- if (node.type === TEXT) {
18
- const text = String(node.text || "");
19
- result += mapperFile.text(text);
20
- } else if (node.type === INLINE) {
21
- let target = matchedValue(mapperFile.outputs, node.id) || mapperFile.getUnknownTag(node);
22
- if (target) {
23
- let inlineValue = String(node.value || "").trim();
24
- inlineValue = mapperFile.inlineText(inlineValue, target.options);
25
- result += await target.render.call(mapperFile, {
26
- nodeType: node.type,
27
- args: node.args || {},
28
- content: inlineValue,
29
- ast: node
30
- });
31
- } else {
32
- result += mapperFile.inlineText(node.value || "", {});
33
- }
34
- } else if (node.type === STATIC_LOGIC) {
35
- try {
36
- const val = await evaluator.execute(node.code);
37
- if (val !== undefined && typeof val !== "object") {
38
- result += mapperFile.text(String(val));
39
- }
40
- } catch (e) {
41
- console.error(`\x1b[31mLogic Error in Markdown mapper:\x1b[0m ${e.message}`);
42
- }
43
- } else if (node.type === BLOCK) {
44
- let target = matchedValue(mapperFile.outputs, node.id) || mapperFile.getUnknownTag(node);
45
- if (target) {
46
- const isSelfClosing = node.isSelfClosing || false;
47
- let content = "";
48
- evaluator.pushScope();
49
- if (!target.options?.handleAst && node.body) {
50
- content = await renderNodeAst(node.body, mapperFile);
51
- }
52
- const output = await target.render.call(mapperFile, {
53
- nodeType: node.type,
54
- args: node.args || {},
55
- content,
56
- ast: node,
57
- isSelfClosing
58
- });
59
- await evaluator.popScope();
60
- result += output;
61
- }
62
- }
63
- }
64
- return result;
65
- }
66
6
 
67
7
  /**
68
8
  * The Markdown Mapper used for generating Markdown text.
@@ -88,76 +28,29 @@ const MARKDOWN = Mapper.define({
88
28
  return out;
89
29
  },
90
30
 
91
- /**
92
- * Formats inline content before rendering.
93
- */
94
- inlineText(text, options) {
95
- if (options?.escape !== false) {
96
- // Use smartEscaper to protect special characters
97
- let out = text;
98
- if (this.md && this.md.smartEscaper) out = this.md.smartEscaper(out);
99
- return out;
100
- }
101
- return text;
102
- },
103
-
104
- /**
105
- * Formats the literal content inside AtBlocks.
106
- */
107
- atBlockBody(text, options) {
108
- if (options?.escape === false) return text;
109
- // Escaping with smartEscaper
110
- let out = text;
111
- if (this.md && this.md.smartEscaper) out = this.md.smartEscaper(out);
112
- return out;
113
- },
114
-
115
31
  /**
116
32
  * Provides a fallback for unknown tags by using the HTML mapper instead.
117
33
  */
118
34
  getUnknownTag(node) {
119
- const isBlock = node.type === BLOCK;
120
35
  const id = node.id.toLowerCase();
121
36
 
122
37
  return {
123
- render: async (ctx) => {
124
- const { args, ast, isSelfClosing } = ctx;
125
- const body = ast && ast.body ? ast.body : [];
126
- const meaningful = body.filter(c => c.type !== TEXT || c.text.trim());
127
- const childCount = meaningful.length;
128
- const element = this.tag(id).smartAttributes(args, this.customProps, this.options);
129
-
130
- // Use the transpiler to format the children if any, otherwise use direct content
131
- let rawContent;
132
- if (node.type === "AtBlock") {
133
- rawContent = node.content || "";
134
- rawContent = this.atBlockBody(rawContent, ctx);
135
- } else if (node.type === "Inline") {
136
- rawContent = node.value || "";
137
- rawContent = this.inlineText(rawContent, ctx);
138
- } else {
139
- rawContent = (await renderNodeAst(body, this)).trim();
140
- }
141
-
142
- if (isSelfClosing || VOID_ELEMENTS.has(id)) {
143
- return element.selfClose();
144
- }
145
-
146
- let finalContent;
147
- if (childCount <= 1) {
148
- // COMPACT PASS: Single child or empty
149
- finalContent = rawContent;
150
- } else {
151
- // MULTILINE PASS: Enforce \n prefix/suffix for multiple children
152
- finalContent = `\n${rawContent}\n`;
38
+ render: async ({ props, ast, isSelfClosing, renderChild }) => {
39
+ const element = this.tag(id).smartAttributes(props, this.customProps, this.options);
40
+ if (isSelfClosing || VOID_ELEMENTS.has(id)) return element.selfClose();
41
+
42
+ let rawContent = "";
43
+ for (const child of (ast.body || [])) {
44
+ if (child.type === TEXT) rawContent += this.text(child.text);
45
+ else if (child.type === BLOCK) rawContent += await renderChild(child);
153
46
  }
47
+ rawContent = rawContent.trim();
154
48
 
49
+ const meaningful = (ast.body || []).filter(c => c.type !== TEXT || c.text.trim());
50
+ const finalContent = meaningful.length <= 1 ? rawContent : `\n${rawContent}\n`;
155
51
  return element.body(finalContent);
156
52
  },
157
- options: {
158
- type: isBlock ? "Block" : (node.type === "AtBlock" ? "AtBlock" : "Inline"),
159
- handleAst: true
160
- }
53
+ options: { handleAst: true }
161
54
  };
162
55
  }
163
56
  });
@@ -169,105 +62,114 @@ registerSharedOutputs(MARKDOWN);
169
62
  /**
170
63
  * Quote - Renders blockquote content or GFM alerts.
171
64
  */
172
- MARKDOWN.register("quote", ({ args, content }) => {
173
- const type = safeArg({ args, index: 0, key: "type", fallBack: "" });
65
+ MARKDOWN.register("quote", ({ props, content }) => {
66
+ const type = safeArg({ props, index: 0, key: "type", fallBack: "" });
174
67
  return md.quote(content, type);
175
- }, { type: "Block", resolve: true });
68
+ }, { resolve: true });
176
69
 
177
70
  /**
178
71
  * Headings - Renders H1-H6 block headings.
179
72
  */
180
73
  ["h1", "h2", "h3", "h4", "h5", "h6"].forEach(heading => {
181
- MARKDOWN.register(heading, function ({ args, content, isSelfClosing }) {
182
- const format = safeArg({ args, key: "format", type: "string", fallBack: "" });
74
+ MARKDOWN.register(heading, function ({ props, content, isSelfClosing }) {
75
+ const format = safeArg({ props, key: "format", type: "string", fallBack: "" });
183
76
  const lvl = heading[1] && !isNaN(Number(heading[1])) ? Number(heading[1]) : 1;
184
77
  if (format.toLowerCase() === "html") {
185
- delete args.format;
186
- const el = this.tag(heading).smartAttributes(args);
78
+ delete props.format;
79
+ const el = this.tag(heading).smartAttributes(props);
187
80
  if (isSelfClosing) return el.selfClose();
188
81
  return el.body(content);
189
82
  }
190
83
  return this.md.heading(content, lvl);
191
- }, { type: "Block" });
84
+ });
192
85
  });
193
86
 
194
87
  /**
195
88
  * Bold - Renders bold text (**text**).
89
+ * Self-closing: [bold = "text" !] or [bold = text: "text" !]
196
90
  */
197
- MARKDOWN.register(["bold", "b"], ({ content }) => {
198
- return md.bold(content);
199
- }, { type: ["Block", "Inline"] });
91
+ MARKDOWN.register(["bold", "b"], ({ props, content, isSelfClosing }) => {
92
+ const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
93
+ return md.bold(text);
94
+ });
200
95
 
201
96
  /**
202
97
  * Italic - Renders italic text (*text*).
98
+ * Self-closing: [italic = "text" !] or [italic = text: "text" !]
203
99
  */
204
- MARKDOWN.register(["italic", "i"], ({ content }) => {
205
- return md.italic(content);
206
- }, { type: ["Block", "Inline"] });
100
+ MARKDOWN.register(["italic", "i"], ({ props, content, isSelfClosing }) => {
101
+ const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
102
+ return md.italic(text);
103
+ });
207
104
 
208
105
  /**
209
106
  * Emphasis - Renders bold-italic text (***text***).
107
+ * Self-closing: [emphasis = "text" !] or [emphasis = text: "text" !]
210
108
  */
211
- MARKDOWN.register(["emphasis", "em"], ({ content }) => {
212
- return md.emphasis(content);
213
- }, { type: ["Block", "Inline"] });
109
+ MARKDOWN.register(["emphasis", "em"], ({ props, content, isSelfClosing }) => {
110
+ const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
111
+ return md.emphasis(text);
112
+ });
214
113
 
215
114
  /**
216
115
  * Strike - Renders strikethrough text (~~text~~).
116
+ * Self-closing: [strike = "text" !] or [strike = text: "text" !]
217
117
  */
218
- MARKDOWN.register(["strike", "s"], ({ content }) => {
219
- return md.strike(content);
220
- }, { type: ["Block", "Inline"] });
118
+ MARKDOWN.register(["strike", "s"], ({ props, content, isSelfClosing }) => {
119
+ const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
120
+ return md.strike(text);
121
+ });
221
122
 
222
123
  /**
223
124
  * Code - Renders inline or fenced code blocks.
224
125
  */
225
126
  MARKDOWN.register(
226
127
  ["Code", "code"],
227
- ({ args, content, nodeType }) => {
228
- if (nodeType === "Inline") {
229
- return `\`${content}\``;
128
+ ({ props, content, isSelfClosing }) => {
129
+ if (isSelfClosing) {
130
+ const text = safeArg({ props, index: 0, key: "text", fallBack: "" });
131
+ return `\`${text}\``;
230
132
  }
231
- const lang = safeArg({ args, index: 0, key: "lang", fallBack: "text" });
133
+ const lang = safeArg({ props, index: 0, key: "lang", fallBack: "" });
232
134
  return md.codeBlock(content, lang);
233
135
  },
234
- {
235
- escape: false,
236
- type: ["AtBlock", "Inline"]
237
- }
136
+ { escape: false }
238
137
  );
239
138
 
240
139
  /**
241
140
  * Link - Renders Markdown links [text](url).
141
+ * Body form: [link = src: "url", title: "..."]text[end]
142
+ * Self-closing: [link = "text", "url" !] or [link = text: "...", src: "...", title: "..." !]
242
143
  */
243
144
  MARKDOWN.register(
244
145
  "link",
245
- ({ args, content }) => {
246
- const src = safeArg({ args, index: 0, key: "src", fallBack: "" });
247
- const title = safeArg({ args, index: 1, key: "title", fallBack: "" });
146
+ ({ props, content, isSelfClosing }) => {
147
+ if (isSelfClosing) {
148
+ const text = safeArg({ props, index: 0, key: "text", fallBack: "" });
149
+ const src = safeArg({ props, index: 1, key: "src", fallBack: "" });
150
+ const title = safeArg({ props, index: 2, key: "title", fallBack: "" });
151
+ return md.url("link", text, src, title);
152
+ }
153
+ const src = safeArg({ props, index: 0, key: "src", fallBack: "" });
154
+ const title = safeArg({ props, index: 1, key: "title", fallBack: "" });
248
155
  return md.url("link", content, src, title);
249
156
  },
250
- {
251
- type: ["Block", "Inline"],
252
- rules: { is_empty_body: false }
253
- }
157
+ { rules: { is_empty_body: false } }
254
158
  );
255
159
 
256
160
  /**
257
161
  * Image - Renders Markdown images ![alt](url).
162
+ * [image = "alt", "src", "title" !] or [image = alt: "...", src: "...", title: "..." !]
258
163
  */
259
164
  MARKDOWN.register(
260
165
  "image",
261
- ({ args }) => {
262
- const alt = safeArg({ args, index: 0, key: "alt", fallBack: "" });
263
- const src = safeArg({ args, index: 1, key: "src", fallBack: "" });
264
- const title = safeArg({ args, index: 2, key: "title", fallBack: "" });
166
+ ({ props }) => {
167
+ const alt = safeArg({ props, index: 0, key: "alt", fallBack: "" });
168
+ const src = safeArg({ props, index: 1, key: "src", fallBack: "" });
169
+ const title = safeArg({ props, index: 2, key: "title", fallBack: "" });
265
170
  return md.url("image", alt, src, title);
266
171
  },
267
- {
268
- type: "Block",
269
- rules: { is_empty_body: true }
270
- }
172
+ { rules: { is_empty_body: true } }
271
173
  );
272
174
 
273
175
  /**
@@ -275,170 +177,151 @@ MARKDOWN.register(
275
177
  */
276
178
  MARKDOWN.register(
277
179
  "hr",
278
- ({ args }) => {
279
- const fmt = safeArg({ args, index: 0, fallBack: "-" });
180
+ ({ props }) => {
181
+ const fmt = safeArg({ props, index: 0, fallBack: "-" });
280
182
  return md.horizontal(fmt);
281
183
  },
282
- {
283
- type: "Block",
284
- rules: { is_empty_body: true }
285
- }
184
+ { rules: { is_empty_body: true } }
286
185
  );
287
186
 
288
187
  /**
289
188
  * Escape - Escapes special Markdown characters.
189
+ * Self-closing: [escape = "text" !] or [escape = text: "text" !]
290
190
  */
291
- MARKDOWN.register(["escape", "e"], function ({ content }) {
292
- return this.md.escape(content);
293
- }, { type: ["Block", "Inline"], resolve: true });
191
+ MARKDOWN.register(["escape", "e"], function ({ props, content, isSelfClosing }) {
192
+ const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
193
+ return this.md.escape(text);
194
+ }, { resolve: true });
195
+
196
+ const ROW_SEP = "\x1E";
197
+ const CELL_SEP = "\x1F";
294
198
 
295
199
  /**
296
200
  * Table - Authoritative Native AST Table resolution.
297
201
  * Processes Header/Body sections with Row/Cell nesting.
202
+ * Supports [for-each] inside [body] for dynamic rows.
298
203
  */
299
204
  MARKDOWN.register(
300
205
  "Table",
301
- async function ({ ast }) {
206
+ async function ({ ast, renderChild }) {
302
207
  const headers = [];
303
208
  const rows = [];
304
209
 
305
- const extractCells = async (node) => {
306
- const cells = [];
307
- if (!node || !node.body) return cells;
308
- // Trim empty spaces while keeping line breaks
309
- const cellAst = node.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
310
- .filter(n => n.type !== TEXT || n.text);
311
-
312
- for (const child of cellAst) {
313
- if (child.type === BLOCK && (child.id.toLowerCase() === "cell" || child.id.toLowerCase() === "th" || child.id.toLowerCase() === "td")) {
314
- const cellContent = await renderNodeAst(child.body, this);
315
- cells.push(cellContent.trim());
316
- }
317
- }
318
- return cells;
319
- };
320
-
321
210
  const extractRows = async (sectionNode) => {
322
- if (!sectionNode || !sectionNode.body) return [];
323
211
  const sectionRows = [];
324
- // Trim empty spaces while keeping line breaks
325
- const rowAst = sectionNode.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
326
- .filter(n => n.type !== TEXT || n.text);
327
- for (const rowNode of rowAst) {
328
- if (rowNode.type === BLOCK && rowNode.id.toLowerCase() === "row") {
329
- const rowData = await extractCells(rowNode);
330
- if (rowData.length > 0) sectionRows.push(rowData);
331
- } else if (rowNode.type === STATIC_LOGIC) {
332
- try { await evaluator.execute(rowNode.code); } catch (e) { console.error(`Logic Error: ${e.message}`); }
212
+ for (const child of (sectionNode.body || [])) {
213
+ if (child.type === BLOCK && child.id?.toLowerCase() === "row") {
214
+ const rendered = await renderChild(child, { inTable: true });
215
+ const cells = rendered.split(ROW_SEP)[0]?.split(CELL_SEP).filter(c => c !== "") ?? [];
216
+ if (cells.length > 0) sectionRows.push(cells);
217
+ } else if (child.type === FOR_EACH) {
218
+ const rendered = await renderChild(child, { inTable: true });
219
+ for (const row of rendered.split(ROW_SEP)) {
220
+ const cells = row.split(CELL_SEP).filter(c => c !== "");
221
+ if (cells.length > 0) sectionRows.push(cells);
222
+ }
333
223
  }
334
224
  }
335
225
  return sectionRows;
336
226
  };
337
227
 
338
- const processTable = async () => {
339
- // Remove empty text blocks
340
- const tableNodes = ast.body.filter(n => n.type !== TEXT || n.text.trim());
341
- for (const node of tableNodes) {
342
- if (node.type === STATIC_LOGIC) {
343
- try { await evaluator.execute(node.code); } catch (e) { console.error(`Logic Error: ${e.message}`); }
344
- continue;
345
- }
346
- if (node.type !== BLOCK) continue;
347
-
348
- const id = node.id.toLowerCase();
349
- if (id === "header") {
350
- const headerRows = await extractRows(node);
351
- if (headerRows.length > 0) {
352
- headers.push(...headerRows[0]);
353
- }
354
- } else if (id === "body") {
355
- const bodyRows = await extractRows(node);
356
- rows.push(...bodyRows);
357
- }
228
+ for (const node of ast.body) {
229
+ if (node.type !== BLOCK) continue;
230
+ const id = node.id.toLowerCase();
231
+ if (id === "header") {
232
+ const headerRows = await extractRows(node);
233
+ if (headerRows.length > 0) headers.push(...headerRows[0]);
234
+ } else if (id === "body") {
235
+ rows.push(...(await extractRows(node)));
358
236
  }
359
- return md.table(headers, rows);
360
- };
237
+ }
361
238
 
362
- return processTable();
239
+ return md.table(headers, rows);
363
240
  },
364
- {
365
- escape: true,
366
- type: "Block",
367
- handleAst: true,
368
- trimAndWrapBlocks: false
369
- }
241
+ { escape: true, handleAst: true, trimAndWrapBlocks: false }
370
242
  );
371
243
 
372
244
  /**
373
245
  * Table Helpers - Internal tags for table structural organization.
374
246
  */
375
- MARKDOWN.register(["header", "body", "row", "cell"], ({ content }) => content);
247
+ MARKDOWN.register(["header", "body"], ({ content }) => content);
248
+
249
+ MARKDOWN.register("row", async function ({ ast, renderChild, inTable }) {
250
+ if (!inTable) {
251
+ let result = "";
252
+ for (const child of ast.body) {
253
+ if (child.type === TEXT) result += this.text(child.text);
254
+ else if (child.type === BLOCK) result += await renderChild(child);
255
+ }
256
+ return result;
257
+ }
258
+ let cells = "";
259
+ for (const child of ast.body) {
260
+ if (child.type !== BLOCK) continue;
261
+ const id = child.id?.toLowerCase();
262
+ if (id === "cell" || id === "th" || id === "td") {
263
+ cells += await renderChild(child, { inTable: true });
264
+ }
265
+ }
266
+ return cells + ROW_SEP;
267
+ }, { handleAst: true });
268
+
269
+ MARKDOWN.register(["cell", "th", "td"], ({ content, inTable }) => {
270
+ return inTable ? content.trim() + CELL_SEP : content;
271
+ });
376
272
 
377
273
  /**
378
274
  * Lists - Authoritative Native AST List resolution.
379
275
  * Supports Ordered (Number) and Unordered (Dotlex) lists with deep nesting.
380
276
  */
381
- MARKDOWN.register(["list", "List"], async function ({ ast, args }) {
382
- const items = [];
383
-
384
- // Determine list type (dot/unordered vs number/ordered)
385
- const indicator = safeArg({ args, index: 0, fallBack: "dot" });
277
+ MARKDOWN.register(["list", "List"], async function ({ ast, props, renderChild }) {
278
+ const indicator = safeArg({ props, index: 0, fallBack: "dot" });
386
279
  const isOrdered = indicator === "number" || indicator === "ol";
387
- let marker = "-";
388
-
389
- if (!isOrdered) {
390
- if (indicator === "dot") marker = "-";
391
- else marker = indicator; // Custom symbol like "*" or "+"
392
- }
393
-
394
- // Remove empty spaces from the start and end of strings
395
- const itemNodes = ast.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
396
- .filter(n => n.type !== TEXT || n.text);
280
+ const marker = isOrdered ? "" : (indicator === "dot" ? "-" : indicator);
281
+ const items = [];
397
282
 
398
- for (const node of itemNodes) {
283
+ for (const node of ast.body) {
284
+ if (node.type !== BLOCK) continue;
399
285
  const id = node.id?.toLowerCase();
400
- if (node.type === BLOCK && (id === "item")) {
401
- // Trim spaces inside the list item
402
- const itemBody = node.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
403
- .filter(n => n.type !== TEXT || n.text);
404
- const itemContent = await renderNodeAst(itemBody, this);
405
- items.push(itemContent.trim());
406
- } else if (node.type === BLOCK && (id === "list")) {
407
- // Add nested lists to the latest item
408
- if (items.length > 0) {
409
- const listContent = await renderNodeAst([node], this);
410
- items[items.length - 1] += "\n" + listContent;
411
- }
286
+ if (id === "item") {
287
+ items.push((await renderChild(node)).trim());
412
288
  }
413
289
  }
414
290
 
415
- const result = isOrdered
416
- ? md.orderedList(items, 0)
417
- : md.unorderedList(items, 0, marker);
418
-
419
- return result;
420
- }, { type: "Block", handleAst: true, trimAndWrapBlocks: false });
291
+ return isOrdered ? md.orderedList(items, 0) : md.unorderedList(items, 0, marker);
292
+ }, { handleAst: true, trimAndWrapBlocks: false });
421
293
 
422
294
  /**
423
295
  * List Helpers - Internal tags for list structural organization.
424
296
  */
425
- MARKDOWN.register(["item", "Item"], async function ({ ast }) {
426
- // Trim whitespace but keep line breaks
427
- const bodyAst = ast.body.map(n => n.type === TEXT ? { ...n, text: n.text.replace(/^[ ]+|[ ]+$/gm, "") } : n)
428
- .filter(n => n.type !== TEXT || n.text);
429
- return await renderNodeAst(bodyAst, this);
430
- }, { type: "Block", handleAst: true, trimAndWrapBlocks: false });
297
+ MARKDOWN.register(["item", "Item"], async function ({ ast, renderChild }) {
298
+ let result = "";
299
+ for (const child of ast.body) {
300
+ if (child.type === TEXT) result += this.text(child.text);
301
+ else if (child.type === BLOCK) result += await renderChild(child);
302
+ }
303
+ return result.trim();
304
+ }, { handleAst: true, trimAndWrapBlocks: false });
431
305
 
432
306
  /**
433
307
  * Todo - Renders task list items with status markers.
308
+ *
309
+ * Supported forms:
310
+ * [todo = task: "Add feature", status: "x" !] named self-closing
311
+ * [todo = "Add feature", "x" !] positional self-closing (task, status)
312
+ * [todo = "x"]Add feature[end] status in prop, task in body
434
313
  */
435
- MARKDOWN.register("todo", ({ args, content }) => {
436
- const statusMarkers = ["done", "x", "-", ""].map(s => s.toLowerCase());
437
-
438
- const isInlineStatus = statusMarkers.includes(content.toLowerCase());
439
- const status = isInlineStatus ? content : (args[0] || "");
440
- const task = isInlineStatus ? (args[0] || "") : content;
314
+ MARKDOWN.register("todo", ({ props, content, isSelfClosing }) => {
315
+ let status, task;
316
+
317
+ if (isSelfClosing) {
318
+ task = safeArg({ props, index: 0, key: "task", fallBack: "" });
319
+ status = safeArg({ props, index: 1, key: "status", fallBack: "" });
320
+ } else {
321
+ status = safeArg({ props, index: 0, fallBack: "" });
322
+ task = content;
323
+ }
441
324
 
442
325
  return md.todo(status, task);
443
- }, { type: "Block", trimAndWrapBlocks: false });
326
+ }, { trimAndWrapBlocks: false });
444
327
  export default MARKDOWN;