quikdown 1.1.1 → 1.2.3

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 (72) hide show
  1. package/README.md +39 -7
  2. package/dist/quikdown.cjs +23 -10
  3. package/dist/quikdown.dark.css +1 -1
  4. package/dist/quikdown.esm.js +23 -10
  5. package/dist/quikdown.esm.min.js +2 -2
  6. package/dist/quikdown.esm.min.js.gz +0 -0
  7. package/dist/quikdown.esm.min.js.map +1 -1
  8. package/dist/quikdown.light.css +1 -1
  9. package/dist/quikdown.umd.js +23 -10
  10. package/dist/quikdown.umd.min.js +2 -2
  11. package/dist/quikdown.umd.min.js.gz +0 -0
  12. package/dist/quikdown.umd.min.js.map +1 -1
  13. package/dist/quikdown_ast.cjs +513 -0
  14. package/dist/quikdown_ast.d.ts +227 -0
  15. package/dist/quikdown_ast.esm.js +511 -0
  16. package/dist/quikdown_ast.esm.min.js +8 -0
  17. package/dist/quikdown_ast.esm.min.js.gz +0 -0
  18. package/dist/quikdown_ast.esm.min.js.map +1 -0
  19. package/dist/quikdown_ast.umd.js +519 -0
  20. package/dist/quikdown_ast.umd.min.js +8 -0
  21. package/dist/quikdown_ast.umd.min.js.gz +0 -0
  22. package/dist/quikdown_ast.umd.min.js.map +1 -0
  23. package/dist/quikdown_ast_html.cjs +1058 -0
  24. package/dist/quikdown_ast_html.d.ts +68 -0
  25. package/dist/quikdown_ast_html.esm.js +1056 -0
  26. package/dist/quikdown_ast_html.esm.min.js +8 -0
  27. package/dist/quikdown_ast_html.esm.min.js.gz +0 -0
  28. package/dist/quikdown_ast_html.esm.min.js.map +1 -0
  29. package/dist/quikdown_ast_html.umd.js +1064 -0
  30. package/dist/quikdown_ast_html.umd.min.js +8 -0
  31. package/dist/quikdown_ast_html.umd.min.js.gz +0 -0
  32. package/dist/quikdown_ast_html.umd.min.js.map +1 -0
  33. package/dist/quikdown_bd.cjs +38 -19
  34. package/dist/quikdown_bd.esm.js +38 -19
  35. package/dist/quikdown_bd.esm.min.js +2 -2
  36. package/dist/quikdown_bd.esm.min.js.gz +0 -0
  37. package/dist/quikdown_bd.esm.min.js.map +1 -1
  38. package/dist/quikdown_bd.umd.js +38 -19
  39. package/dist/quikdown_bd.umd.min.js +2 -2
  40. package/dist/quikdown_bd.umd.min.js.gz +0 -0
  41. package/dist/quikdown_bd.umd.min.js.map +1 -1
  42. package/dist/quikdown_edit.cjs +836 -117
  43. package/dist/quikdown_edit.d.ts +123 -131
  44. package/dist/quikdown_edit.esm.js +836 -117
  45. package/dist/quikdown_edit.esm.min.js +3 -3
  46. package/dist/quikdown_edit.esm.min.js.gz +0 -0
  47. package/dist/quikdown_edit.esm.min.js.map +1 -1
  48. package/dist/quikdown_edit.umd.js +836 -117
  49. package/dist/quikdown_edit.umd.min.js +3 -3
  50. package/dist/quikdown_edit.umd.min.js.gz +0 -0
  51. package/dist/quikdown_edit.umd.min.js.map +1 -1
  52. package/dist/quikdown_json.cjs +556 -0
  53. package/dist/quikdown_json.d.ts +48 -0
  54. package/dist/quikdown_json.esm.js +554 -0
  55. package/dist/quikdown_json.esm.min.js +8 -0
  56. package/dist/quikdown_json.esm.min.js.gz +0 -0
  57. package/dist/quikdown_json.esm.min.js.map +1 -0
  58. package/dist/quikdown_json.umd.js +562 -0
  59. package/dist/quikdown_json.umd.min.js +8 -0
  60. package/dist/quikdown_json.umd.min.js.gz +0 -0
  61. package/dist/quikdown_json.umd.min.js.map +1 -0
  62. package/dist/quikdown_yaml.cjs +717 -0
  63. package/dist/quikdown_yaml.d.ts +51 -0
  64. package/dist/quikdown_yaml.esm.js +715 -0
  65. package/dist/quikdown_yaml.esm.min.js +8 -0
  66. package/dist/quikdown_yaml.esm.min.js.gz +0 -0
  67. package/dist/quikdown_yaml.esm.min.js.map +1 -0
  68. package/dist/quikdown_yaml.umd.js +723 -0
  69. package/dist/quikdown_yaml.umd.min.js +8 -0
  70. package/dist/quikdown_yaml.umd.min.js.gz +0 -0
  71. package/dist/quikdown_yaml.umd.min.js.map +1 -0
  72. package/package.json +100 -45
@@ -0,0 +1,715 @@
1
+ /**
2
+ * quikdown_yaml - YAML Markdown Parser
3
+ * @version 1.2.3
4
+ * @license BSD-2-Clause
5
+ * @copyright DeftIO 2025
6
+ */
7
+ /**
8
+ * quikdown_ast - Forgiving markdown to AST parser
9
+ * Converts markdown to a structured Abstract Syntax Tree
10
+ * @param {string} markdown - The markdown source text
11
+ * @param {Object} options - Optional configuration object
12
+ * @returns {Object} - The AST object
13
+ */
14
+
15
+ // Version will be injected at build time
16
+ const quikdownVersion$1 = '1.2.3';
17
+
18
+ // Safety limit to prevent infinite loops in list parsing
19
+ const MAX_LOOP_ITERATIONS = 1000;
20
+
21
+ /**
22
+ * Parse markdown into an AST
23
+ * @param {string} markdown - The markdown source text
24
+ * @param {Object} options - Optional configuration object
25
+ * @returns {Object} - The AST object
26
+ */
27
+ function quikdown_ast(markdown, options = {}) {
28
+ if (!markdown || typeof markdown !== 'string') {
29
+ return { type: 'document', children: [] };
30
+ }
31
+
32
+ // Normalize line endings (handle CRLF, CR, LF uniformly)
33
+ const text = markdown.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
34
+
35
+ const children = parseBlocks(text);
36
+
37
+ return {
38
+ type: 'document',
39
+ children
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Parse block-level elements
45
+ */
46
+ function parseBlocks(text, options) {
47
+ const blocks = [];
48
+ const lines = text.split('\n');
49
+ let i = 0;
50
+
51
+ while (i < lines.length) {
52
+ const line = lines[i];
53
+
54
+ // Empty line - skip
55
+ if (line.trim() === '') {
56
+ i++;
57
+ continue;
58
+ }
59
+
60
+ // Fenced code block (``` or ~~~)
61
+ const fenceMatch = line.match(/^(```|~~~)(.*)$/);
62
+ if (fenceMatch) {
63
+ const [, openFence, langPart] = fenceMatch;
64
+ const lang = langPart.trim();
65
+ const codeLines = [];
66
+ i++;
67
+
68
+ // Find closing fence (forgiving: accept mismatched fences or EOF)
69
+ while (i < lines.length) {
70
+ const closingMatch = lines[i].match(/^(```|~~~)\s*$/);
71
+ if (closingMatch) {
72
+ i++;
73
+ break;
74
+ }
75
+ codeLines.push(lines[i]);
76
+ i++;
77
+ }
78
+
79
+ blocks.push({
80
+ type: 'code_block',
81
+ lang: lang || null,
82
+ content: codeLines.join('\n'),
83
+ fence: openFence
84
+ });
85
+ continue;
86
+ }
87
+
88
+ // Horizontal rule
89
+ if (/^---+\s*$/.test(line) || /^\*\*\*+\s*$/.test(line) || /^___+\s*$/.test(line)) {
90
+ blocks.push({ type: 'hr' });
91
+ i++;
92
+ continue;
93
+ }
94
+
95
+ // Heading (forgiving: accept #heading without space)
96
+ const headingMatch = line.match(/^(#{1,6})\s*(.+?)\s*#*$/);
97
+ if (headingMatch) {
98
+ const [, hashes, content] = headingMatch;
99
+ blocks.push({
100
+ type: 'heading',
101
+ level: hashes.length,
102
+ children: parseInline(content)
103
+ });
104
+ i++;
105
+ continue;
106
+ }
107
+
108
+ // Table (look for separator line)
109
+ if (line.includes('|')) {
110
+ const tableResult = tryParseTable(lines, i);
111
+ if (tableResult) {
112
+ blocks.push(tableResult.node);
113
+ i = tableResult.nextIndex;
114
+ continue;
115
+ }
116
+ }
117
+
118
+ // Blockquote
119
+ if (line.match(/^>\s*/)) {
120
+ const quoteLines = [];
121
+ while (i < lines.length && lines[i].match(/^>\s*/)) {
122
+ quoteLines.push(lines[i].replace(/^>\s*/, ''));
123
+ i++;
124
+ }
125
+ blocks.push({
126
+ type: 'blockquote',
127
+ children: parseBlocks(quoteLines.join('\n'))
128
+ });
129
+ continue;
130
+ }
131
+
132
+ // List (ordered or unordered)
133
+ const listMatch = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.*)$/);
134
+ if (listMatch) {
135
+ const listResult = parseList(lines, i);
136
+ blocks.push(listResult.node);
137
+ i = listResult.nextIndex;
138
+ continue;
139
+ }
140
+
141
+ // Paragraph - collect lines until empty line or block element
142
+ const paragraphLines = [];
143
+ while (i < lines.length) {
144
+ const pLine = lines[i];
145
+
146
+ // Stop on empty line
147
+ if (pLine.trim() === '') break;
148
+
149
+ // Stop on block elements
150
+ if (/^(```|~~~)/.test(pLine)) break;
151
+ if (/^#{1,6}\s/.test(pLine)) break;
152
+ if (/^---+\s*$/.test(pLine) || /^\*\*\*+\s*$/.test(pLine) || /^___+\s*$/.test(pLine)) break;
153
+ if (/^>\s*/.test(pLine)) break;
154
+ if (/^(\s*)([*\-+]|\d+\.)\s+/.test(pLine)) break;
155
+ if (pLine.includes('|') && i + 1 < lines.length && /^\|?[\s\-:|]+\|?$/.test(lines[i + 1])) break;
156
+
157
+ paragraphLines.push(pLine);
158
+ i++;
159
+ }
160
+
161
+ if (paragraphLines.length > 0) {
162
+ blocks.push({
163
+ type: 'paragraph',
164
+ children: parseInline(paragraphLines.join('\n'))
165
+ });
166
+ }
167
+ }
168
+
169
+ return blocks;
170
+ }
171
+
172
+ /**
173
+ * Try to parse a table starting at the given line
174
+ */
175
+ function tryParseTable(lines, startIndex, options) {
176
+ // Need at least 2 lines (header + separator)
177
+ if (startIndex + 1 >= lines.length) return null;
178
+
179
+ const headerLine = lines[startIndex];
180
+ const separatorLine = lines[startIndex + 1];
181
+
182
+ // Check if separator line is valid
183
+ if (!/^\|?[\s\-:|]+\|?$/.test(separatorLine) || !separatorLine.includes('-')) {
184
+ return null;
185
+ }
186
+
187
+ // Parse header
188
+ const headerCells = parseTableRow(headerLine);
189
+ if (headerCells.length === 0) return null;
190
+
191
+ // Parse alignments from separator
192
+ const separatorCells = parseTableRow(separatorLine);
193
+ const alignments = separatorCells.map(cell => {
194
+ const trimmed = cell.trim();
195
+ if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
196
+ if (trimmed.endsWith(':')) return 'right';
197
+ return 'left';
198
+ });
199
+
200
+ // Parse headers with inline formatting
201
+ const headers = headerCells.map(cell => parseInline(cell.trim()));
202
+
203
+ // Parse body rows
204
+ const rows = [];
205
+ let i = startIndex + 2;
206
+ while (i < lines.length) {
207
+ const rowLine = lines[i];
208
+ if (!rowLine.includes('|') || rowLine.trim() === '') break;
209
+
210
+ const cells = parseTableRow(rowLine);
211
+ rows.push(cells.map(cell => parseInline(cell.trim())));
212
+ i++;
213
+ }
214
+
215
+ return {
216
+ node: {
217
+ type: 'table',
218
+ headers,
219
+ rows,
220
+ alignments
221
+ },
222
+ nextIndex: i
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Parse a table row into cells
228
+ */
229
+ function parseTableRow(line) {
230
+ // Handle pipes at start/end or not
231
+ let trimmed = line.trim();
232
+ if (trimmed.startsWith('|')) trimmed = trimmed.slice(1);
233
+ if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1);
234
+ return trimmed.split('|');
235
+ }
236
+
237
+ /**
238
+ * Parse a list starting at the given line
239
+ */
240
+ function parseList(lines, startIndex, options) {
241
+ const items = [];
242
+ let i = startIndex;
243
+ let loopCount = 0;
244
+
245
+ // Determine initial list type
246
+ const firstMatch = lines[i].match(/^(\s*)([*\-+]|\d+\.)\s+(.*)$/);
247
+ const isOrdered = /^\d+\./.test(firstMatch[2]);
248
+ const baseIndent = firstMatch[1].length;
249
+
250
+ while (i < lines.length && loopCount < MAX_LOOP_ITERATIONS) {
251
+ loopCount++;
252
+ const line = lines[i];
253
+ const match = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.*)$/);
254
+
255
+ if (!match) break;
256
+
257
+ const [, indent, marker, content] = match;
258
+ const indentLevel = indent.length;
259
+
260
+ // If less indented than base, stop
261
+ if (indentLevel < baseIndent) break;
262
+
263
+ // If same indentation but different list type, stop
264
+ const itemIsOrdered = /^\d+\./.test(marker);
265
+ if (indentLevel === baseIndent && itemIsOrdered !== isOrdered) break;
266
+
267
+ // If more indented, it's a nested list - handle by collecting sub-lines
268
+ if (indentLevel > baseIndent) {
269
+ // This is a nested list item, collect and parse as sublist
270
+ const subLines = [];
271
+ let subLoopCount = 0;
272
+ while (i < lines.length && subLoopCount < MAX_LOOP_ITERATIONS) {
273
+ subLoopCount++;
274
+ const subLine = lines[i];
275
+ const subMatch = subLine.match(/^(\s*)([*\-+]|\d+\.)\s+/);
276
+ if (!subMatch) break;
277
+ if (subMatch[1].length < baseIndent) break;
278
+ if (subMatch[1].length === baseIndent) break;
279
+ subLines.push(subLine);
280
+ i++;
281
+ }
282
+
283
+ if (subLines.length > 0 && items.length > 0) {
284
+ // Add nested list to last item
285
+ const nestedResult = parseList(subLines, 0);
286
+ const lastItem = items[items.length - 1];
287
+ if (!lastItem.children) {
288
+ lastItem.children = [];
289
+ } else if (!Array.isArray(lastItem.children)) {
290
+ lastItem.children = [{ type: 'paragraph', children: lastItem.children }];
291
+ }
292
+ lastItem.children.push(nestedResult.node);
293
+ }
294
+ continue;
295
+ }
296
+
297
+ // Parse list item
298
+ const itemNode = {
299
+ type: 'list_item',
300
+ checked: null,
301
+ children: null
302
+ };
303
+
304
+ // Check for task list syntax
305
+ const taskMatch = content.match(/^\[([x ])\]\s*(.*)$/i);
306
+ if (taskMatch && !isOrdered) {
307
+ itemNode.checked = taskMatch[1].toLowerCase() === 'x';
308
+ itemNode.children = parseInline(taskMatch[2]);
309
+ } else {
310
+ itemNode.children = parseInline(content);
311
+ }
312
+
313
+ items.push(itemNode);
314
+ i++;
315
+ }
316
+
317
+ return {
318
+ node: {
319
+ type: 'list',
320
+ ordered: isOrdered,
321
+ items
322
+ },
323
+ nextIndex: i
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Parse inline elements
329
+ */
330
+ function parseInline(text, options) {
331
+ if (!text) return [];
332
+
333
+ const nodes = [];
334
+ let remaining = text;
335
+
336
+ while (remaining.length > 0) {
337
+ let matched = false;
338
+
339
+ // Line break (1+ trailing spaces or explicit \n after processing)
340
+ // Handle inline line breaks (two spaces at end of line or backslash before newline)
341
+ const brMatch = remaining.match(/^(.+?)(?: {2}|\\\n|\n)/);
342
+ if (brMatch && remaining.includes('\n')) {
343
+ const beforeBr = remaining.indexOf('\n');
344
+ const beforeText = remaining.slice(0, beforeBr);
345
+ const afterText = remaining.slice(beforeBr + 1);
346
+
347
+ // Check if line break is significant (2+ trailing spaces or backslash)
348
+ if (beforeText.endsWith(' ') || beforeText.endsWith('\\')) {
349
+ const cleanText = beforeText.replace(/\\$/, '').replace(/ +$/, '');
350
+ if (cleanText) {
351
+ nodes.push(...parseInlineContent(cleanText));
352
+ }
353
+ nodes.push({ type: 'br' });
354
+ remaining = afterText;
355
+ matched = true;
356
+ continue;
357
+ }
358
+ }
359
+
360
+ // Images: ![alt](url)
361
+ const imgMatch = remaining.match(/^!\[([^\]]*)\]\(\s*([^)\s]+)\s*\)/);
362
+ if (imgMatch) {
363
+ nodes.push({
364
+ type: 'image',
365
+ alt: imgMatch[1],
366
+ url: imgMatch[2].trim() // Forgiving: trim whitespace in URL
367
+ });
368
+ remaining = remaining.slice(imgMatch[0].length);
369
+ matched = true;
370
+ continue;
371
+ }
372
+
373
+ // Links: [text](url)
374
+ const linkMatch = remaining.match(/^\[([^\]]+)\]\(\s*([^)\s]+)\s*\)/);
375
+ if (linkMatch) {
376
+ nodes.push({
377
+ type: 'link',
378
+ url: linkMatch[2].trim(), // Forgiving: trim whitespace in URL
379
+ children: parseInlineContent(linkMatch[1])
380
+ });
381
+ remaining = remaining.slice(linkMatch[0].length);
382
+ matched = true;
383
+ continue;
384
+ }
385
+
386
+ // Inline code: `code`
387
+ const codeMatch = remaining.match(/^`([^`]+)`/);
388
+ if (codeMatch) {
389
+ nodes.push({
390
+ type: 'code',
391
+ value: codeMatch[1]
392
+ });
393
+ remaining = remaining.slice(codeMatch[0].length);
394
+ matched = true;
395
+ continue;
396
+ }
397
+
398
+ // Bold: **text** or __text__
399
+ const boldMatch = remaining.match(/^(\*\*|__)(.+?)\1/);
400
+ if (boldMatch) {
401
+ nodes.push({
402
+ type: 'strong',
403
+ children: parseInlineContent(boldMatch[2])
404
+ });
405
+ remaining = remaining.slice(boldMatch[0].length);
406
+ matched = true;
407
+ continue;
408
+ }
409
+
410
+ // Strikethrough: ~~text~~
411
+ const strikeMatch = remaining.match(/^~~(.+?)~~/);
412
+ if (strikeMatch) {
413
+ nodes.push({
414
+ type: 'del',
415
+ children: parseInlineContent(strikeMatch[1])
416
+ });
417
+ remaining = remaining.slice(strikeMatch[0].length);
418
+ matched = true;
419
+ continue;
420
+ }
421
+
422
+ // Italic: *text* or _text_ (not at word boundary for underscores)
423
+ const emMatch = remaining.match(/^(\*|_)(?!\1)(.+?)(?<!\1)\1(?!\1)/);
424
+ if (emMatch) {
425
+ nodes.push({
426
+ type: 'em',
427
+ children: parseInlineContent(emMatch[2])
428
+ });
429
+ remaining = remaining.slice(emMatch[0].length);
430
+ matched = true;
431
+ continue;
432
+ }
433
+
434
+ // Autolinks: URLs starting with http:// or https://
435
+ const urlMatch = remaining.match(/^(https?:\/\/[^\s<>[\]]+)/);
436
+ if (urlMatch) {
437
+ nodes.push({
438
+ type: 'link',
439
+ url: urlMatch[1],
440
+ children: [{ type: 'text', value: urlMatch[1] }]
441
+ });
442
+ remaining = remaining.slice(urlMatch[0].length);
443
+ matched = true;
444
+ continue;
445
+ }
446
+
447
+ // Plain text - consume until next potential inline element or end
448
+ if (!matched) {
449
+ // Find next potential inline marker
450
+ const nextMarker = remaining.search(/[`*_~![\\n]|https?:\/\//);
451
+ if (nextMarker === -1) {
452
+ // No more markers, consume rest as text
453
+ nodes.push({ type: 'text', value: remaining });
454
+ break;
455
+ } else if (nextMarker === 0) {
456
+ // Current char is a marker but didn't match - consume it as text
457
+ nodes.push({ type: 'text', value: remaining[0] });
458
+ remaining = remaining.slice(1);
459
+ } else {
460
+ // Consume text up to next marker
461
+ nodes.push({ type: 'text', value: remaining.slice(0, nextMarker) });
462
+ remaining = remaining.slice(nextMarker);
463
+ }
464
+ }
465
+ }
466
+
467
+ // Merge adjacent text nodes
468
+ return mergeTextNodes(nodes);
469
+ }
470
+
471
+ /**
472
+ * Parse inline content (recursive helper for nested inline elements)
473
+ */
474
+ function parseInlineContent(text, options) {
475
+ // For simple nested content, use parseInline
476
+ // But handle newlines as spaces for inline content
477
+ const normalized = text.replace(/\n/g, ' ');
478
+ return parseInline(normalized);
479
+ }
480
+
481
+ /**
482
+ * Merge adjacent text nodes
483
+ */
484
+ function mergeTextNodes(nodes) {
485
+ const merged = [];
486
+ for (const node of nodes) {
487
+ if (node.type === 'text' && merged.length > 0 && merged[merged.length - 1].type === 'text') {
488
+ merged[merged.length - 1].value += node.value;
489
+ } else {
490
+ merged.push(node);
491
+ }
492
+ }
493
+ return merged;
494
+ }
495
+
496
+ // Attach version
497
+ quikdown_ast.version = quikdownVersion$1;
498
+
499
+ // Export for both CommonJS and ES6
500
+ /* istanbul ignore next */
501
+ if (typeof module !== 'undefined' && module.exports) {
502
+ module.exports = quikdown_ast;
503
+ }
504
+
505
+ // For browser global
506
+ /* istanbul ignore next */
507
+ if (typeof window !== 'undefined') {
508
+ window.quikdown_ast = quikdown_ast;
509
+ }
510
+
511
+ /**
512
+ * quikdown_yaml - Markdown to YAML converter
513
+ * Converts markdown to YAML via AST
514
+ * @param {string} markdown - The markdown source text
515
+ * @param {Object} options - Optional configuration object
516
+ * @returns {string} - YAML string representation of the AST
517
+ */
518
+
519
+
520
+ // Version will be injected at build time
521
+ const quikdownVersion = '1.2.3';
522
+
523
+ /**
524
+ * Convert markdown to YAML
525
+ * @param {string} markdown - The markdown source text
526
+ * @param {Object} options - Optional configuration object
527
+ * @returns {string} - YAML string
528
+ */
529
+ function quikdown_yaml(markdown, options = {}) {
530
+ const ast = quikdown_ast(markdown, options);
531
+ return astToYaml(ast, 0);
532
+ }
533
+
534
+ /**
535
+ * Convert an AST node to YAML string
536
+ * Minimal YAML serializer - no external dependencies
537
+ * @param {Object|Array|string|number|boolean|null} node - The value to serialize
538
+ * @param {number} indent - Current indentation level
539
+ * @returns {string} - YAML string
540
+ */
541
+ function astToYaml(node, indent) {
542
+ const spaces = ' '.repeat(indent);
543
+
544
+ if (node === null || node === undefined) {
545
+ return 'null';
546
+ }
547
+
548
+ if (typeof node === 'boolean') {
549
+ return node ? 'true' : 'false';
550
+ }
551
+
552
+ if (typeof node === 'number') {
553
+ return String(node);
554
+ }
555
+
556
+ if (typeof node === 'string') {
557
+ return formatYamlString(node);
558
+ }
559
+
560
+ if (Array.isArray(node)) {
561
+ if (node.length === 0) {
562
+ return '[]';
563
+ }
564
+
565
+ const lines = [];
566
+ for (const item of node) {
567
+ if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
568
+ // Object item - inline the first property on the same line as dash
569
+ const props = Object.entries(item);
570
+ if (props.length > 0) {
571
+ const [firstKey, firstValue] = props[0];
572
+ const firstValueStr = formatValue(firstValue, indent + 1);
573
+ lines.push(`${spaces}- ${firstKey}: ${firstValueStr}`);
574
+
575
+ // Remaining properties
576
+ for (let i = 1; i < props.length; i++) {
577
+ const [key, value] = props[i];
578
+ const valueStr = formatValue(value, indent + 1);
579
+ lines.push(`${spaces} ${key}: ${valueStr}`);
580
+ }
581
+ } else {
582
+ lines.push(`${spaces}- {}`);
583
+ }
584
+ } else {
585
+ // Simple value
586
+ const valueStr = astToYaml(item, indent + 1);
587
+ lines.push(`${spaces}- ${valueStr}`);
588
+ }
589
+ }
590
+ return '\n' + lines.join('\n');
591
+ }
592
+
593
+ if (typeof node === 'object') {
594
+ const entries = Object.entries(node);
595
+ if (entries.length === 0) {
596
+ return '{}';
597
+ }
598
+
599
+ const lines = [];
600
+ for (const [key, value] of entries) {
601
+ const valueStr = formatValue(value, indent);
602
+ lines.push(`${spaces}${key}: ${valueStr}`);
603
+ }
604
+ return lines.join('\n');
605
+ }
606
+
607
+ return String(node);
608
+ }
609
+
610
+ /**
611
+ * Format a value for YAML (handles nested objects/arrays)
612
+ */
613
+ function formatValue(value, indent) {
614
+ if (value === null || value === undefined) {
615
+ return 'null';
616
+ }
617
+
618
+ if (typeof value === 'boolean') {
619
+ return value ? 'true' : 'false';
620
+ }
621
+
622
+ if (typeof value === 'number') {
623
+ return String(value);
624
+ }
625
+
626
+ if (typeof value === 'string') {
627
+ return formatYamlString(value);
628
+ }
629
+
630
+ if (Array.isArray(value)) {
631
+ if (value.length === 0) {
632
+ return '[]';
633
+ }
634
+ return astToYaml(value, indent + 1);
635
+ }
636
+
637
+ if (typeof value === 'object') {
638
+ // Nested object - format on new lines with increased indent
639
+ const entries = Object.entries(value);
640
+ if (entries.length === 0) {
641
+ return '{}';
642
+ }
643
+ return '\n' + astToYaml(value, indent + 1);
644
+ }
645
+
646
+ return String(value);
647
+ }
648
+
649
+ /**
650
+ * Format a string for YAML (handle quoting and escaping)
651
+ */
652
+ function formatYamlString(str) {
653
+ // Check if string needs quoting
654
+ const needsQuoting = (
655
+ str === '' ||
656
+ str.includes('\n') ||
657
+ str.includes(':') ||
658
+ str.includes('#') ||
659
+ str.includes("'") ||
660
+ str.includes('"') ||
661
+ str.startsWith(' ') ||
662
+ str.endsWith(' ') ||
663
+ str.startsWith('-') ||
664
+ str.startsWith('[') ||
665
+ str.startsWith('{') ||
666
+ str === 'true' ||
667
+ str === 'false' ||
668
+ str === 'null' ||
669
+ str === 'yes' ||
670
+ str === 'no' ||
671
+ str === 'on' ||
672
+ str === 'off' ||
673
+ /^\d+$/.test(str) ||
674
+ /^\d+\.\d+$/.test(str)
675
+ );
676
+
677
+ if (!needsQuoting) {
678
+ return str;
679
+ }
680
+
681
+ // Use double quotes and escape special characters
682
+ if (str.includes('\n')) {
683
+ // For multiline strings, use literal block style
684
+ const escaped = str.replace(/\n/g, '\\n');
685
+ return `"${escaped.replace(/"/g, '\\"')}"`;
686
+ }
687
+
688
+ // Simple string quoting
689
+ return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
690
+ }
691
+
692
+ // Expose the AST parser for direct access
693
+ quikdown_yaml.parse = quikdown_ast;
694
+
695
+ // Expose the YAML serializer for direct AST-to-YAML conversion
696
+ quikdown_yaml.stringify = function(ast) {
697
+ return astToYaml(ast, 0);
698
+ };
699
+
700
+ // Attach version
701
+ quikdown_yaml.version = quikdownVersion;
702
+
703
+ // Export for both CommonJS and ES6
704
+ /* istanbul ignore next */
705
+ if (typeof module !== 'undefined' && module.exports) {
706
+ module.exports = quikdown_yaml;
707
+ }
708
+
709
+ // For browser global
710
+ /* istanbul ignore next */
711
+ if (typeof window !== 'undefined') {
712
+ window.quikdown_yaml = quikdown_yaml;
713
+ }
714
+
715
+ export { quikdown_yaml as default };