quikdown 1.1.0 → 1.2.2

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 (58) hide show
  1. package/README.md +46 -6
  2. package/dist/quikdown.cjs +5 -5
  3. package/dist/quikdown.dark.css +1 -1
  4. package/dist/quikdown.esm.js +5 -5
  5. package/dist/quikdown.esm.min.js +2 -2
  6. package/dist/quikdown.esm.min.js.map +1 -1
  7. package/dist/quikdown.light.css +1 -1
  8. package/dist/quikdown.umd.js +5 -5
  9. package/dist/quikdown.umd.min.js +2 -2
  10. package/dist/quikdown.umd.min.js.map +1 -1
  11. package/dist/quikdown_ast.cjs +513 -0
  12. package/dist/quikdown_ast.d.ts +227 -0
  13. package/dist/quikdown_ast.esm.js +511 -0
  14. package/dist/quikdown_ast.esm.min.js +8 -0
  15. package/dist/quikdown_ast.esm.min.js.map +1 -0
  16. package/dist/quikdown_ast.umd.js +519 -0
  17. package/dist/quikdown_ast.umd.min.js +8 -0
  18. package/dist/quikdown_ast.umd.min.js.map +1 -0
  19. package/dist/quikdown_ast_html.cjs +1058 -0
  20. package/dist/quikdown_ast_html.d.ts +68 -0
  21. package/dist/quikdown_ast_html.esm.js +1056 -0
  22. package/dist/quikdown_ast_html.esm.min.js +8 -0
  23. package/dist/quikdown_ast_html.esm.min.js.map +1 -0
  24. package/dist/quikdown_ast_html.umd.js +1064 -0
  25. package/dist/quikdown_ast_html.umd.min.js +8 -0
  26. package/dist/quikdown_ast_html.umd.min.js.map +1 -0
  27. package/dist/quikdown_bd.cjs +12 -12
  28. package/dist/quikdown_bd.esm.js +12 -12
  29. package/dist/quikdown_bd.esm.min.js +2 -2
  30. package/dist/quikdown_bd.esm.min.js.map +1 -1
  31. package/dist/quikdown_bd.umd.js +12 -12
  32. package/dist/quikdown_bd.umd.min.js +2 -2
  33. package/dist/quikdown_bd.umd.min.js.map +1 -1
  34. package/dist/quikdown_edit.cjs +2297 -136
  35. package/dist/quikdown_edit.d.ts +110 -132
  36. package/dist/quikdown_edit.esm.js +2297 -136
  37. package/dist/quikdown_edit.esm.min.js +3 -4
  38. package/dist/quikdown_edit.esm.min.js.map +1 -1
  39. package/dist/quikdown_edit.umd.js +2298 -137
  40. package/dist/quikdown_edit.umd.min.js +3 -4
  41. package/dist/quikdown_edit.umd.min.js.map +1 -1
  42. package/dist/quikdown_json.cjs +556 -0
  43. package/dist/quikdown_json.d.ts +48 -0
  44. package/dist/quikdown_json.esm.js +554 -0
  45. package/dist/quikdown_json.esm.min.js +8 -0
  46. package/dist/quikdown_json.esm.min.js.map +1 -0
  47. package/dist/quikdown_json.umd.js +562 -0
  48. package/dist/quikdown_json.umd.min.js +8 -0
  49. package/dist/quikdown_json.umd.min.js.map +1 -0
  50. package/dist/quikdown_yaml.cjs +717 -0
  51. package/dist/quikdown_yaml.d.ts +51 -0
  52. package/dist/quikdown_yaml.esm.js +715 -0
  53. package/dist/quikdown_yaml.esm.min.js +8 -0
  54. package/dist/quikdown_yaml.esm.min.js.map +1 -0
  55. package/dist/quikdown_yaml.umd.js +723 -0
  56. package/dist/quikdown_yaml.umd.min.js +8 -0
  57. package/dist/quikdown_yaml.umd.min.js.map +1 -0
  58. package/package.json +92 -39
@@ -0,0 +1,1058 @@
1
+ /**
2
+ * quikdown_ast_html - AST to HTML Markdown Parser
3
+ * @version 1.2.2
4
+ * @license BSD-2-Clause
5
+ * @copyright DeftIO 2025
6
+ */
7
+ 'use strict';
8
+
9
+ /**
10
+ * quikdown_ast - Forgiving markdown to AST parser
11
+ * Converts markdown to a structured Abstract Syntax Tree
12
+ * @param {string} markdown - The markdown source text
13
+ * @param {Object} options - Optional configuration object
14
+ * @returns {Object} - The AST object
15
+ */
16
+
17
+ // Version will be injected at build time
18
+ const quikdownVersion$1 = '1.2.2';
19
+
20
+ // Safety limit to prevent infinite loops in list parsing
21
+ const MAX_LOOP_ITERATIONS = 1000;
22
+
23
+ /**
24
+ * Parse markdown into an AST
25
+ * @param {string} markdown - The markdown source text
26
+ * @param {Object} options - Optional configuration object
27
+ * @returns {Object} - The AST object
28
+ */
29
+ function quikdown_ast(markdown, options = {}) {
30
+ if (!markdown || typeof markdown !== 'string') {
31
+ return { type: 'document', children: [] };
32
+ }
33
+
34
+ // Normalize line endings (handle CRLF, CR, LF uniformly)
35
+ const text = markdown.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
36
+
37
+ const children = parseBlocks(text);
38
+
39
+ return {
40
+ type: 'document',
41
+ children
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Parse block-level elements
47
+ */
48
+ function parseBlocks(text, options) {
49
+ const blocks = [];
50
+ const lines = text.split('\n');
51
+ let i = 0;
52
+
53
+ while (i < lines.length) {
54
+ const line = lines[i];
55
+
56
+ // Empty line - skip
57
+ if (line.trim() === '') {
58
+ i++;
59
+ continue;
60
+ }
61
+
62
+ // Fenced code block (``` or ~~~)
63
+ const fenceMatch = line.match(/^(```|~~~)(.*)$/);
64
+ if (fenceMatch) {
65
+ const [, openFence, langPart] = fenceMatch;
66
+ const lang = langPart.trim();
67
+ const codeLines = [];
68
+ i++;
69
+
70
+ // Find closing fence (forgiving: accept mismatched fences or EOF)
71
+ while (i < lines.length) {
72
+ const closingMatch = lines[i].match(/^(```|~~~)\s*$/);
73
+ if (closingMatch) {
74
+ i++;
75
+ break;
76
+ }
77
+ codeLines.push(lines[i]);
78
+ i++;
79
+ }
80
+
81
+ blocks.push({
82
+ type: 'code_block',
83
+ lang: lang || null,
84
+ content: codeLines.join('\n'),
85
+ fence: openFence
86
+ });
87
+ continue;
88
+ }
89
+
90
+ // Horizontal rule
91
+ if (/^---+\s*$/.test(line) || /^\*\*\*+\s*$/.test(line) || /^___+\s*$/.test(line)) {
92
+ blocks.push({ type: 'hr' });
93
+ i++;
94
+ continue;
95
+ }
96
+
97
+ // Heading (forgiving: accept #heading without space)
98
+ const headingMatch = line.match(/^(#{1,6})\s*(.+?)\s*#*$/);
99
+ if (headingMatch) {
100
+ const [, hashes, content] = headingMatch;
101
+ blocks.push({
102
+ type: 'heading',
103
+ level: hashes.length,
104
+ children: parseInline(content)
105
+ });
106
+ i++;
107
+ continue;
108
+ }
109
+
110
+ // Table (look for separator line)
111
+ if (line.includes('|')) {
112
+ const tableResult = tryParseTable(lines, i);
113
+ if (tableResult) {
114
+ blocks.push(tableResult.node);
115
+ i = tableResult.nextIndex;
116
+ continue;
117
+ }
118
+ }
119
+
120
+ // Blockquote
121
+ if (line.match(/^>\s*/)) {
122
+ const quoteLines = [];
123
+ while (i < lines.length && lines[i].match(/^>\s*/)) {
124
+ quoteLines.push(lines[i].replace(/^>\s*/, ''));
125
+ i++;
126
+ }
127
+ blocks.push({
128
+ type: 'blockquote',
129
+ children: parseBlocks(quoteLines.join('\n'))
130
+ });
131
+ continue;
132
+ }
133
+
134
+ // List (ordered or unordered)
135
+ const listMatch = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.*)$/);
136
+ if (listMatch) {
137
+ const listResult = parseList(lines, i);
138
+ blocks.push(listResult.node);
139
+ i = listResult.nextIndex;
140
+ continue;
141
+ }
142
+
143
+ // Paragraph - collect lines until empty line or block element
144
+ const paragraphLines = [];
145
+ while (i < lines.length) {
146
+ const pLine = lines[i];
147
+
148
+ // Stop on empty line
149
+ if (pLine.trim() === '') break;
150
+
151
+ // Stop on block elements
152
+ if (/^(```|~~~)/.test(pLine)) break;
153
+ if (/^#{1,6}\s/.test(pLine)) break;
154
+ if (/^---+\s*$/.test(pLine) || /^\*\*\*+\s*$/.test(pLine) || /^___+\s*$/.test(pLine)) break;
155
+ if (/^>\s*/.test(pLine)) break;
156
+ if (/^(\s*)([*\-+]|\d+\.)\s+/.test(pLine)) break;
157
+ if (pLine.includes('|') && i + 1 < lines.length && /^\|?[\s\-:|]+\|?$/.test(lines[i + 1])) break;
158
+
159
+ paragraphLines.push(pLine);
160
+ i++;
161
+ }
162
+
163
+ if (paragraphLines.length > 0) {
164
+ blocks.push({
165
+ type: 'paragraph',
166
+ children: parseInline(paragraphLines.join('\n'))
167
+ });
168
+ }
169
+ }
170
+
171
+ return blocks;
172
+ }
173
+
174
+ /**
175
+ * Try to parse a table starting at the given line
176
+ */
177
+ function tryParseTable(lines, startIndex, options) {
178
+ // Need at least 2 lines (header + separator)
179
+ if (startIndex + 1 >= lines.length) return null;
180
+
181
+ const headerLine = lines[startIndex];
182
+ const separatorLine = lines[startIndex + 1];
183
+
184
+ // Check if separator line is valid
185
+ if (!/^\|?[\s\-:|]+\|?$/.test(separatorLine) || !separatorLine.includes('-')) {
186
+ return null;
187
+ }
188
+
189
+ // Parse header
190
+ const headerCells = parseTableRow(headerLine);
191
+ if (headerCells.length === 0) return null;
192
+
193
+ // Parse alignments from separator
194
+ const separatorCells = parseTableRow(separatorLine);
195
+ const alignments = separatorCells.map(cell => {
196
+ const trimmed = cell.trim();
197
+ if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
198
+ if (trimmed.endsWith(':')) return 'right';
199
+ return 'left';
200
+ });
201
+
202
+ // Parse headers with inline formatting
203
+ const headers = headerCells.map(cell => parseInline(cell.trim()));
204
+
205
+ // Parse body rows
206
+ const rows = [];
207
+ let i = startIndex + 2;
208
+ while (i < lines.length) {
209
+ const rowLine = lines[i];
210
+ if (!rowLine.includes('|') || rowLine.trim() === '') break;
211
+
212
+ const cells = parseTableRow(rowLine);
213
+ rows.push(cells.map(cell => parseInline(cell.trim())));
214
+ i++;
215
+ }
216
+
217
+ return {
218
+ node: {
219
+ type: 'table',
220
+ headers,
221
+ rows,
222
+ alignments
223
+ },
224
+ nextIndex: i
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Parse a table row into cells
230
+ */
231
+ function parseTableRow(line) {
232
+ // Handle pipes at start/end or not
233
+ let trimmed = line.trim();
234
+ if (trimmed.startsWith('|')) trimmed = trimmed.slice(1);
235
+ if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1);
236
+ return trimmed.split('|');
237
+ }
238
+
239
+ /**
240
+ * Parse a list starting at the given line
241
+ */
242
+ function parseList(lines, startIndex, options) {
243
+ const items = [];
244
+ let i = startIndex;
245
+ let loopCount = 0;
246
+
247
+ // Determine initial list type
248
+ const firstMatch = lines[i].match(/^(\s*)([*\-+]|\d+\.)\s+(.*)$/);
249
+ const isOrdered = /^\d+\./.test(firstMatch[2]);
250
+ const baseIndent = firstMatch[1].length;
251
+
252
+ while (i < lines.length && loopCount < MAX_LOOP_ITERATIONS) {
253
+ loopCount++;
254
+ const line = lines[i];
255
+ const match = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.*)$/);
256
+
257
+ if (!match) break;
258
+
259
+ const [, indent, marker, content] = match;
260
+ const indentLevel = indent.length;
261
+
262
+ // If less indented than base, stop
263
+ if (indentLevel < baseIndent) break;
264
+
265
+ // If same indentation but different list type, stop
266
+ const itemIsOrdered = /^\d+\./.test(marker);
267
+ if (indentLevel === baseIndent && itemIsOrdered !== isOrdered) break;
268
+
269
+ // If more indented, it's a nested list - handle by collecting sub-lines
270
+ if (indentLevel > baseIndent) {
271
+ // This is a nested list item, collect and parse as sublist
272
+ const subLines = [];
273
+ let subLoopCount = 0;
274
+ while (i < lines.length && subLoopCount < MAX_LOOP_ITERATIONS) {
275
+ subLoopCount++;
276
+ const subLine = lines[i];
277
+ const subMatch = subLine.match(/^(\s*)([*\-+]|\d+\.)\s+/);
278
+ if (!subMatch) break;
279
+ if (subMatch[1].length < baseIndent) break;
280
+ if (subMatch[1].length === baseIndent) break;
281
+ subLines.push(subLine);
282
+ i++;
283
+ }
284
+
285
+ if (subLines.length > 0 && items.length > 0) {
286
+ // Add nested list to last item
287
+ const nestedResult = parseList(subLines, 0);
288
+ const lastItem = items[items.length - 1];
289
+ if (!lastItem.children) {
290
+ lastItem.children = [];
291
+ } else if (!Array.isArray(lastItem.children)) {
292
+ lastItem.children = [{ type: 'paragraph', children: lastItem.children }];
293
+ }
294
+ lastItem.children.push(nestedResult.node);
295
+ }
296
+ continue;
297
+ }
298
+
299
+ // Parse list item
300
+ const itemNode = {
301
+ type: 'list_item',
302
+ checked: null,
303
+ children: null
304
+ };
305
+
306
+ // Check for task list syntax
307
+ const taskMatch = content.match(/^\[([x ])\]\s*(.*)$/i);
308
+ if (taskMatch && !isOrdered) {
309
+ itemNode.checked = taskMatch[1].toLowerCase() === 'x';
310
+ itemNode.children = parseInline(taskMatch[2]);
311
+ } else {
312
+ itemNode.children = parseInline(content);
313
+ }
314
+
315
+ items.push(itemNode);
316
+ i++;
317
+ }
318
+
319
+ return {
320
+ node: {
321
+ type: 'list',
322
+ ordered: isOrdered,
323
+ items
324
+ },
325
+ nextIndex: i
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Parse inline elements
331
+ */
332
+ function parseInline(text, options) {
333
+ if (!text) return [];
334
+
335
+ const nodes = [];
336
+ let remaining = text;
337
+
338
+ while (remaining.length > 0) {
339
+ let matched = false;
340
+
341
+ // Line break (1+ trailing spaces or explicit \n after processing)
342
+ // Handle inline line breaks (two spaces at end of line or backslash before newline)
343
+ const brMatch = remaining.match(/^(.+?)(?: {2}|\\\n|\n)/);
344
+ if (brMatch && remaining.includes('\n')) {
345
+ const beforeBr = remaining.indexOf('\n');
346
+ const beforeText = remaining.slice(0, beforeBr);
347
+ const afterText = remaining.slice(beforeBr + 1);
348
+
349
+ // Check if line break is significant (2+ trailing spaces or backslash)
350
+ if (beforeText.endsWith(' ') || beforeText.endsWith('\\')) {
351
+ const cleanText = beforeText.replace(/\\$/, '').replace(/ +$/, '');
352
+ if (cleanText) {
353
+ nodes.push(...parseInlineContent(cleanText));
354
+ }
355
+ nodes.push({ type: 'br' });
356
+ remaining = afterText;
357
+ matched = true;
358
+ continue;
359
+ }
360
+ }
361
+
362
+ // Images: ![alt](url)
363
+ const imgMatch = remaining.match(/^!\[([^\]]*)\]\(\s*([^)\s]+)\s*\)/);
364
+ if (imgMatch) {
365
+ nodes.push({
366
+ type: 'image',
367
+ alt: imgMatch[1],
368
+ url: imgMatch[2].trim() // Forgiving: trim whitespace in URL
369
+ });
370
+ remaining = remaining.slice(imgMatch[0].length);
371
+ matched = true;
372
+ continue;
373
+ }
374
+
375
+ // Links: [text](url)
376
+ const linkMatch = remaining.match(/^\[([^\]]+)\]\(\s*([^)\s]+)\s*\)/);
377
+ if (linkMatch) {
378
+ nodes.push({
379
+ type: 'link',
380
+ url: linkMatch[2].trim(), // Forgiving: trim whitespace in URL
381
+ children: parseInlineContent(linkMatch[1])
382
+ });
383
+ remaining = remaining.slice(linkMatch[0].length);
384
+ matched = true;
385
+ continue;
386
+ }
387
+
388
+ // Inline code: `code`
389
+ const codeMatch = remaining.match(/^`([^`]+)`/);
390
+ if (codeMatch) {
391
+ nodes.push({
392
+ type: 'code',
393
+ value: codeMatch[1]
394
+ });
395
+ remaining = remaining.slice(codeMatch[0].length);
396
+ matched = true;
397
+ continue;
398
+ }
399
+
400
+ // Bold: **text** or __text__
401
+ const boldMatch = remaining.match(/^(\*\*|__)(.+?)\1/);
402
+ if (boldMatch) {
403
+ nodes.push({
404
+ type: 'strong',
405
+ children: parseInlineContent(boldMatch[2])
406
+ });
407
+ remaining = remaining.slice(boldMatch[0].length);
408
+ matched = true;
409
+ continue;
410
+ }
411
+
412
+ // Strikethrough: ~~text~~
413
+ const strikeMatch = remaining.match(/^~~(.+?)~~/);
414
+ if (strikeMatch) {
415
+ nodes.push({
416
+ type: 'del',
417
+ children: parseInlineContent(strikeMatch[1])
418
+ });
419
+ remaining = remaining.slice(strikeMatch[0].length);
420
+ matched = true;
421
+ continue;
422
+ }
423
+
424
+ // Italic: *text* or _text_ (not at word boundary for underscores)
425
+ const emMatch = remaining.match(/^(\*|_)(?!\1)(.+?)(?<!\1)\1(?!\1)/);
426
+ if (emMatch) {
427
+ nodes.push({
428
+ type: 'em',
429
+ children: parseInlineContent(emMatch[2])
430
+ });
431
+ remaining = remaining.slice(emMatch[0].length);
432
+ matched = true;
433
+ continue;
434
+ }
435
+
436
+ // Autolinks: URLs starting with http:// or https://
437
+ const urlMatch = remaining.match(/^(https?:\/\/[^\s<>[\]]+)/);
438
+ if (urlMatch) {
439
+ nodes.push({
440
+ type: 'link',
441
+ url: urlMatch[1],
442
+ children: [{ type: 'text', value: urlMatch[1] }]
443
+ });
444
+ remaining = remaining.slice(urlMatch[0].length);
445
+ matched = true;
446
+ continue;
447
+ }
448
+
449
+ // Plain text - consume until next potential inline element or end
450
+ if (!matched) {
451
+ // Find next potential inline marker
452
+ const nextMarker = remaining.search(/[`*_~![\\n]|https?:\/\//);
453
+ if (nextMarker === -1) {
454
+ // No more markers, consume rest as text
455
+ nodes.push({ type: 'text', value: remaining });
456
+ break;
457
+ } else if (nextMarker === 0) {
458
+ // Current char is a marker but didn't match - consume it as text
459
+ nodes.push({ type: 'text', value: remaining[0] });
460
+ remaining = remaining.slice(1);
461
+ } else {
462
+ // Consume text up to next marker
463
+ nodes.push({ type: 'text', value: remaining.slice(0, nextMarker) });
464
+ remaining = remaining.slice(nextMarker);
465
+ }
466
+ }
467
+ }
468
+
469
+ // Merge adjacent text nodes
470
+ return mergeTextNodes(nodes);
471
+ }
472
+
473
+ /**
474
+ * Parse inline content (recursive helper for nested inline elements)
475
+ */
476
+ function parseInlineContent(text, options) {
477
+ // For simple nested content, use parseInline
478
+ // But handle newlines as spaces for inline content
479
+ const normalized = text.replace(/\n/g, ' ');
480
+ return parseInline(normalized);
481
+ }
482
+
483
+ /**
484
+ * Merge adjacent text nodes
485
+ */
486
+ function mergeTextNodes(nodes) {
487
+ const merged = [];
488
+ for (const node of nodes) {
489
+ if (node.type === 'text' && merged.length > 0 && merged[merged.length - 1].type === 'text') {
490
+ merged[merged.length - 1].value += node.value;
491
+ } else {
492
+ merged.push(node);
493
+ }
494
+ }
495
+ return merged;
496
+ }
497
+
498
+ // Attach version
499
+ quikdown_ast.version = quikdownVersion$1;
500
+
501
+ // Export for both CommonJS and ES6
502
+ /* istanbul ignore next */
503
+ if (typeof module !== 'undefined' && module.exports) {
504
+ module.exports = quikdown_ast;
505
+ }
506
+
507
+ // For browser global
508
+ /* istanbul ignore next */
509
+ if (typeof window !== 'undefined') {
510
+ window.quikdown_ast = quikdown_ast;
511
+ }
512
+
513
+ /**
514
+ * quikdown_ast_html - AST to HTML converter
515
+ * Converts AST (or markdown/JSON/YAML) to HTML
516
+ * @param {string|Object} input - Markdown string, AST object, JSON string, or YAML string
517
+ * @param {Object} options - Optional configuration object
518
+ * @returns {string} - HTML string
519
+ */
520
+
521
+
522
+ // Version will be injected at build time
523
+ const quikdownVersion = '1.2.2';
524
+
525
+ // Constants
526
+ const CLASS_PREFIX = 'quikdown-';
527
+
528
+ // Escape map for HTML
529
+ const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
530
+
531
+ // Style definitions (matching quikdown.js)
532
+ const QUIKDOWN_STYLES = {
533
+ h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',
534
+ h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',
535
+ h3: 'font-size:1.25em;font-weight:600;margin:1em 0',
536
+ h4: 'font-size:1em;font-weight:600;margin:1.33em 0',
537
+ h5: 'font-size:.875em;font-weight:600;margin:1.67em 0',
538
+ h6: 'font-size:.85em;font-weight:600;margin:2em 0',
539
+ pre: 'background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0',
540
+ code: 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace',
541
+ blockquote: 'border-left:4px solid #ddd;margin-left:0;padding-left:1em',
542
+ table: 'border-collapse:collapse;width:100%;margin:1em 0',
543
+ th: 'border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left',
544
+ td: 'border:1px solid #ddd;padding:8px;text-align:left',
545
+ hr: 'border:none;border-top:1px solid #ddd;margin:1em 0',
546
+ img: 'max-width:100%;height:auto',
547
+ a: 'color:#06c;text-decoration:underline',
548
+ strong: 'font-weight:bold',
549
+ em: 'font-style:italic',
550
+ del: 'text-decoration:line-through',
551
+ ul: 'margin:.5em 0;padding-left:2em',
552
+ ol: 'margin:.5em 0;padding-left:2em',
553
+ li: 'margin:.25em 0',
554
+ 'task-item': 'list-style:none',
555
+ 'task-checkbox': 'margin-right:.5em'
556
+ };
557
+
558
+ /**
559
+ * Escape HTML entities
560
+ */
561
+ function escapeHtml(text) {
562
+ if (!text) return '';
563
+ return String(text).replace(/[&<>"']/g, m => ESC_MAP[m]);
564
+ }
565
+
566
+ /**
567
+ * Create attribute string generator
568
+ */
569
+ function createGetAttr(inline_styles) {
570
+ return function(tag, additionalStyle = '') {
571
+ if (inline_styles) {
572
+ let style = QUIKDOWN_STYLES[tag];
573
+ if (!style && !additionalStyle) return '';
574
+
575
+ if (additionalStyle && additionalStyle.includes('text-align') && style && style.includes('text-align')) {
576
+ style = style.replace(/text-align:[^;]+;?/, '').trim();
577
+ if (style && !style.endsWith(';')) style += ';';
578
+ }
579
+
580
+ const fullStyle = additionalStyle ? (style ? `${style}${additionalStyle}` : additionalStyle) : style;
581
+ return ` style="${fullStyle}"`;
582
+ } else {
583
+ const classAttr = ` class="${CLASS_PREFIX}${tag}"`;
584
+ if (additionalStyle) {
585
+ return `${classAttr} style="${additionalStyle}"`;
586
+ }
587
+ return classAttr;
588
+ }
589
+ };
590
+ }
591
+
592
+ /**
593
+ * Sanitize URLs
594
+ */
595
+ function sanitizeUrl(url) {
596
+ if (!url) return '';
597
+ const trimmedUrl = url.trim();
598
+ const lowerUrl = trimmedUrl.toLowerCase();
599
+
600
+ const dangerousProtocols = ['javascript:', 'vbscript:', 'data:'];
601
+ for (const protocol of dangerousProtocols) {
602
+ if (lowerUrl.startsWith(protocol)) {
603
+ if (protocol === 'data:' && lowerUrl.startsWith('data:image/')) {
604
+ return trimmedUrl;
605
+ }
606
+ return '#';
607
+ }
608
+ }
609
+
610
+ return trimmedUrl;
611
+ }
612
+
613
+ /**
614
+ * Convert input to AST
615
+ * Accepts markdown string, AST object, JSON string, or YAML string
616
+ */
617
+ function toAst(input, options = {}) {
618
+ if (!input) {
619
+ return { type: 'document', children: [] };
620
+ }
621
+
622
+ // Already an AST object
623
+ if (typeof input === 'object' && input.type) {
624
+ return input;
625
+ }
626
+
627
+ if (typeof input === 'string') {
628
+ const trimmed = input.trim();
629
+
630
+ // Try JSON first (starts with { or [)
631
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
632
+ try {
633
+ const parsed = JSON.parse(trimmed);
634
+ if (parsed.type === 'document') {
635
+ return parsed;
636
+ }
637
+ // If it's an array, wrap it as document children
638
+ if (Array.isArray(parsed)) {
639
+ return { type: 'document', children: parsed };
640
+ }
641
+ return parsed;
642
+ } catch (_e) {
643
+ // Not valid JSON, fall through to markdown
644
+ }
645
+ }
646
+
647
+ // Try YAML detection (has type: and children: patterns typical of AST)
648
+ if (trimmed.includes('type:') && (trimmed.includes('children:') || trimmed.includes('value:'))) {
649
+ try {
650
+ const parsed = parseYaml(trimmed);
651
+ if (parsed && parsed.type) {
652
+ return parsed;
653
+ }
654
+ } catch (_e) {
655
+ // Not valid YAML AST, fall through to markdown
656
+ }
657
+ }
658
+
659
+ // Treat as markdown
660
+ return quikdown_ast(input, options);
661
+ }
662
+
663
+ return { type: 'document', children: [] };
664
+ }
665
+
666
+ /**
667
+ * Simple YAML parser for AST format
668
+ * Only handles the subset needed for quikdown AST
669
+ */
670
+ function parseYaml(yaml) {
671
+ const lines = yaml.split('\n');
672
+ return parseYamlNode(lines, 0, 0).value;
673
+ }
674
+
675
+ /**
676
+ * Parse a YAML node starting at given line and indent
677
+ */
678
+ function parseYamlNode(lines, startLine, minIndent) {
679
+ if (startLine >= lines.length) {
680
+ return { value: null, nextLine: startLine };
681
+ }
682
+
683
+ const line = lines[startLine];
684
+ const trimmed = line.trim();
685
+
686
+ // Skip empty lines
687
+ if (trimmed === '') {
688
+ return parseYamlNode(lines, startLine + 1, minIndent);
689
+ }
690
+
691
+ // Get current indent
692
+ const indent = line.search(/\S/);
693
+ if (indent < minIndent && indent >= 0) {
694
+ return { value: null, nextLine: startLine };
695
+ }
696
+
697
+ // Array item
698
+ if (trimmed.startsWith('- ')) {
699
+ return parseYamlArray(lines, startLine, indent);
700
+ }
701
+
702
+ // Empty array
703
+ if (trimmed === '[]') {
704
+ return { value: [], nextLine: startLine + 1 };
705
+ }
706
+
707
+ // Empty object
708
+ if (trimmed === '{}') {
709
+ return { value: {}, nextLine: startLine + 1 };
710
+ }
711
+
712
+ // Key-value pair
713
+ const colonIndex = trimmed.indexOf(':');
714
+ if (colonIndex > 0) {
715
+ return parseYamlObject(lines, startLine, indent);
716
+ }
717
+
718
+ // Scalar value
719
+ return { value: parseYamlScalar(trimmed), nextLine: startLine + 1 };
720
+ }
721
+
722
+ /**
723
+ * Parse YAML array
724
+ */
725
+ function parseYamlArray(lines, startLine, baseIndent) {
726
+ const items = [];
727
+ let i = startLine;
728
+
729
+ while (i < lines.length) {
730
+ const line = lines[i];
731
+ const trimmed = line.trim();
732
+
733
+ if (trimmed === '') {
734
+ i++;
735
+ continue;
736
+ }
737
+
738
+ const indent = line.search(/\S/);
739
+ if (indent < baseIndent && indent >= 0) break;
740
+ if (indent > baseIndent && items.length > 0) {
741
+ // Continuation of previous item
742
+ i++;
743
+ continue;
744
+ }
745
+
746
+ if (!trimmed.startsWith('- ')) break;
747
+
748
+ // Parse the item after "- "
749
+ const itemContent = trimmed.slice(2);
750
+
751
+ if (itemContent.includes(':')) {
752
+ // Object item - parse inline and following properties
753
+ const obj = {};
754
+ const colonIdx = itemContent.indexOf(':');
755
+ const key = itemContent.slice(0, colonIdx).trim();
756
+ const value = itemContent.slice(colonIdx + 1).trim();
757
+
758
+ if (value === '' || value.startsWith('\n')) {
759
+ // Value on next lines
760
+ const result = parseYamlNode(lines, i + 1, indent + 2);
761
+ obj[key] = result.value;
762
+ i = result.nextLine;
763
+ } else {
764
+ obj[key] = parseYamlScalar(value);
765
+ i++;
766
+ }
767
+
768
+ // Parse remaining properties at same indent
769
+ while (i < lines.length) {
770
+ const nextLine = lines[i];
771
+ const nextTrimmed = nextLine.trim();
772
+ if (nextTrimmed === '') {
773
+ i++;
774
+ continue;
775
+ }
776
+
777
+ const nextIndent = nextLine.search(/\S/);
778
+ if (nextIndent <= baseIndent) break;
779
+ if (nextTrimmed.startsWith('- ')) break;
780
+
781
+ const nextColonIdx = nextTrimmed.indexOf(':');
782
+ if (nextColonIdx > 0) {
783
+ const nextKey = nextTrimmed.slice(0, nextColonIdx).trim();
784
+ const nextValue = nextTrimmed.slice(nextColonIdx + 1).trim();
785
+
786
+ if (nextValue === '' || nextValue.startsWith('\n')) {
787
+ const result = parseYamlNode(lines, i + 1, nextIndent + 2);
788
+ obj[nextKey] = result.value;
789
+ i = result.nextLine;
790
+ } else {
791
+ obj[nextKey] = parseYamlScalar(nextValue);
792
+ i++;
793
+ }
794
+ } else {
795
+ i++;
796
+ }
797
+ }
798
+
799
+ items.push(obj);
800
+ } else {
801
+ items.push(parseYamlScalar(itemContent));
802
+ i++;
803
+ }
804
+ }
805
+
806
+ return { value: items, nextLine: i };
807
+ }
808
+
809
+ /**
810
+ * Parse YAML object
811
+ */
812
+ function parseYamlObject(lines, startLine, baseIndent) {
813
+ const obj = {};
814
+ let i = startLine;
815
+
816
+ while (i < lines.length) {
817
+ const line = lines[i];
818
+ const trimmed = line.trim();
819
+
820
+ if (trimmed === '') {
821
+ i++;
822
+ continue;
823
+ }
824
+
825
+ const indent = line.search(/\S/);
826
+ if (indent < baseIndent && indent >= 0) break;
827
+
828
+ const colonIdx = trimmed.indexOf(':');
829
+ if (colonIdx <= 0) {
830
+ i++;
831
+ continue;
832
+ }
833
+
834
+ const key = trimmed.slice(0, colonIdx).trim();
835
+ const value = trimmed.slice(colonIdx + 1).trim();
836
+
837
+ if (value === '' || value === '|' || value === '>') {
838
+ // Value on next lines
839
+ const result = parseYamlNode(lines, i + 1, indent + 2);
840
+ obj[key] = result.value;
841
+ i = result.nextLine;
842
+ } else {
843
+ obj[key] = parseYamlScalar(value);
844
+ i++;
845
+ }
846
+ }
847
+
848
+ return { value: obj, nextLine: i };
849
+ }
850
+
851
+ /**
852
+ * Parse YAML scalar value
853
+ */
854
+ function parseYamlScalar(str) {
855
+ if (!str) return null;
856
+
857
+ const trimmed = str.trim();
858
+
859
+ if (trimmed === 'null' || trimmed === '~') return null;
860
+ if (trimmed === 'true') return true;
861
+ if (trimmed === 'false') return false;
862
+
863
+ // Quoted string
864
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
865
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
866
+ return trimmed.slice(1, -1)
867
+ .replace(/\\n/g, '\n')
868
+ .replace(/\\"/g, '"')
869
+ .replace(/\\\\/g, '\\');
870
+ }
871
+
872
+ // Number
873
+ if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
874
+ if (/^-?\d+\.\d+$/.test(trimmed)) return parseFloat(trimmed);
875
+
876
+ return trimmed;
877
+ }
878
+
879
+ /**
880
+ * Convert AST (or any valid input) to HTML
881
+ * @param {string|Object} input - Markdown, AST, JSON, or YAML
882
+ * @param {Object} options - Configuration options
883
+ * @returns {string} - HTML string
884
+ */
885
+ function quikdown_ast_html(input, options = {}) {
886
+ const ast = toAst(input, options);
887
+ return renderAst(ast, options);
888
+ }
889
+
890
+ /**
891
+ * Render an AST node to HTML
892
+ */
893
+ function renderAst(node, options = {}) {
894
+ if (!node) return '';
895
+
896
+ const { inline_styles = false } = options;
897
+ const getAttr = createGetAttr(inline_styles);
898
+
899
+ return renderNode(node, getAttr, options);
900
+ }
901
+
902
+ /**
903
+ * Render a single node
904
+ */
905
+ function renderNode(node, getAttr, options) {
906
+ if (!node) return '';
907
+
908
+ switch (node.type) {
909
+ case 'document':
910
+ return renderChildren(node.children, getAttr, options);
911
+
912
+ case 'paragraph':
913
+ return `<p>${renderChildren(node.children, getAttr, options)}</p>`;
914
+
915
+ case 'heading':
916
+ const level = node.level || 1;
917
+ return `<h${level}${getAttr('h' + level)}>${renderChildren(node.children, getAttr, options)}</h${level}>`;
918
+
919
+ case 'code_block':
920
+ const langClass = !options.inline_styles && node.lang ? ` class="language-${node.lang}"` : '';
921
+ const codeAttr = options.inline_styles ? getAttr('code') : langClass;
922
+ return `<pre${getAttr('pre')}><code${codeAttr}>${escapeHtml(node.content)}</code></pre>`;
923
+
924
+ case 'blockquote':
925
+ return `<blockquote${getAttr('blockquote')}>${renderChildren(node.children, getAttr, options)}</blockquote>`;
926
+
927
+ case 'list':
928
+ const listTag = node.ordered ? 'ol' : 'ul';
929
+ const items = (node.items || []).map(item => renderNode(item, getAttr, options)).join('');
930
+ return `<${listTag}${getAttr(listTag)}>${items}</${listTag}>`;
931
+
932
+ case 'list_item':
933
+ // Handle task list items
934
+ if (node.checked !== null && node.checked !== undefined) {
935
+ const checkboxAttr = options.inline_styles
936
+ ? ' style="margin-right:.5em"'
937
+ : ` class="${CLASS_PREFIX}task-checkbox"`;
938
+ const checked = node.checked ? ' checked' : '';
939
+ const itemAttr = options.inline_styles
940
+ ? ' style="list-style:none"'
941
+ : ` class="${CLASS_PREFIX}task-item"`;
942
+ return `<li${itemAttr}><input type="checkbox"${checkboxAttr}${checked} disabled> ${renderChildren(node.children, getAttr, options)}</li>`;
943
+ }
944
+ return `<li${getAttr('li')}>${renderChildren(node.children, getAttr, options)}</li>`;
945
+
946
+ case 'table':
947
+ return renderTable(node, getAttr, options);
948
+
949
+ case 'hr':
950
+ return `<hr${getAttr('hr')}>`;
951
+
952
+ case 'text':
953
+ return escapeHtml(node.value || '');
954
+
955
+ case 'strong':
956
+ return `<strong${getAttr('strong')}>${renderChildren(node.children, getAttr, options)}</strong>`;
957
+
958
+ case 'em':
959
+ return `<em${getAttr('em')}>${renderChildren(node.children, getAttr, options)}</em>`;
960
+
961
+ case 'del':
962
+ return `<del${getAttr('del')}>${renderChildren(node.children, getAttr, options)}</del>`;
963
+
964
+ case 'code':
965
+ return `<code${getAttr('code')}>${escapeHtml(node.value || '')}</code>`;
966
+
967
+ case 'link':
968
+ const sanitizedHref = sanitizeUrl(node.url);
969
+ const isExternal = /^https?:\/\//i.test(sanitizedHref);
970
+ const rel = isExternal ? ' rel="noopener noreferrer"' : '';
971
+ return `<a${getAttr('a')} href="${sanitizedHref}"${rel}>${renderChildren(node.children, getAttr, options)}</a>`;
972
+
973
+ case 'image':
974
+ const sanitizedSrc = sanitizeUrl(node.url);
975
+ return `<img${getAttr('img')} src="${sanitizedSrc}" alt="${escapeHtml(node.alt || '')}">`;
976
+
977
+ case 'br':
978
+ return '<br>';
979
+
980
+ default:
981
+ // Unknown node type - try to render children if present
982
+ if (node.children) {
983
+ return renderChildren(node.children, getAttr, options);
984
+ }
985
+ if (node.value !== undefined) {
986
+ return escapeHtml(String(node.value));
987
+ }
988
+ return '';
989
+ }
990
+ }
991
+
992
+ /**
993
+ * Render children array
994
+ */
995
+ function renderChildren(children, getAttr, options) {
996
+ if (!children) return '';
997
+ if (!Array.isArray(children)) {
998
+ return renderNode(children, getAttr, options);
999
+ }
1000
+ return children.map(child => renderNode(child, getAttr, options)).join('');
1001
+ }
1002
+
1003
+ /**
1004
+ * Render a table node
1005
+ */
1006
+ function renderTable(node, getAttr, options) {
1007
+ const alignments = node.alignments || [];
1008
+
1009
+ let html = `<table${getAttr('table')}>\n`;
1010
+
1011
+ // Headers
1012
+ if (node.headers && node.headers.length > 0) {
1013
+ html += '<thead>\n<tr>\n';
1014
+ node.headers.forEach((header, i) => {
1015
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
1016
+ html += `<th${getAttr('th', alignStyle)}>${renderChildren(header, getAttr, options)}</th>\n`;
1017
+ });
1018
+ html += '</tr>\n</thead>\n';
1019
+ }
1020
+
1021
+ // Body
1022
+ if (node.rows && node.rows.length > 0) {
1023
+ html += '<tbody>\n';
1024
+ node.rows.forEach(row => {
1025
+ html += '<tr>\n';
1026
+ row.forEach((cell, i) => {
1027
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
1028
+ html += `<td${getAttr('td', alignStyle)}>${renderChildren(cell, getAttr, options)}</td>\n`;
1029
+ });
1030
+ html += '</tr>\n';
1031
+ });
1032
+ html += '</tbody>\n';
1033
+ }
1034
+
1035
+ html += '</table>';
1036
+ return html;
1037
+ }
1038
+
1039
+ // Expose helper functions
1040
+ quikdown_ast_html.toAst = toAst;
1041
+ quikdown_ast_html.renderAst = renderAst;
1042
+
1043
+ // Attach version
1044
+ quikdown_ast_html.version = quikdownVersion;
1045
+
1046
+ // Export for both CommonJS and ES6
1047
+ /* istanbul ignore next */
1048
+ if (typeof module !== 'undefined' && module.exports) {
1049
+ module.exports = quikdown_ast_html;
1050
+ }
1051
+
1052
+ // For browser global
1053
+ /* istanbul ignore next */
1054
+ if (typeof window !== 'undefined') {
1055
+ window.quikdown_ast_html = quikdown_ast_html;
1056
+ }
1057
+
1058
+ module.exports = quikdown_ast_html;