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