quikdown 1.0.2 → 1.0.4

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.
@@ -0,0 +1,675 @@
1
+ /**
2
+ * quikdown_bd - Bidirectional Markdown Parser
3
+ * @version 1.0.4
4
+ * @license BSD-2-Clause
5
+ * @copyright DeftIO 2025
6
+ */
7
+ /**
8
+ * quikdown_bd - Bidirectional markdown/HTML converter
9
+ * Standalone version with round-trip conversion support
10
+ *
11
+ * Uses data-qd attributes to preserve original markdown syntax
12
+ * Enables HTML→Markdown conversion for quikdown-generated HTML
13
+ */
14
+
15
+ // Version - uses same version as core quikdown
16
+ const VERSION = '1.0.4';
17
+
18
+ // Helper to escape HTML (same as core)
19
+ const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
20
+ function escapeHtml(text) {
21
+ return text.replace(/[&<>"']/g, m => ESC_MAP[m]);
22
+ }
23
+
24
+ // Modified getAttr that adds data-qd attributes
25
+ function createGetAttrBD(inline_styles, styles) {
26
+ return function(tag, additionalStyle = '', sourceMarker = '') {
27
+ let attrs = '';
28
+
29
+ // Add data-qd attribute if source marker provided
30
+ if (sourceMarker) {
31
+ attrs += ` data-qd="${escapeHtml(sourceMarker)}"`;
32
+ }
33
+
34
+ // Add style or class
35
+ if (inline_styles) {
36
+ const style = styles[tag];
37
+ if (style || additionalStyle) {
38
+ const fullStyle = additionalStyle ? (style ? `${style};${additionalStyle}` : additionalStyle) : style;
39
+ attrs += ` style="${fullStyle}"`;
40
+ }
41
+ } else {
42
+ attrs += ` class="quikdown-${tag}"`;
43
+ }
44
+
45
+ return attrs;
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Enhanced markdown parser with bidirectional support
51
+ * Wraps the core parser and adds data-qd attributes
52
+ */
53
+ function quikdown_bd(markdown, options = {}) {
54
+ if (!markdown || typeof markdown !== 'string') {
55
+ return '';
56
+ }
57
+
58
+ const { fence_plugin, inline_styles = false, bidirectional = true } = options;
59
+
60
+ // If not bidirectional mode, process without data-qd attributes
61
+ if (!bidirectional) {
62
+ // Process without bidirectional tracking
63
+ options.bidirectional = false;
64
+ }
65
+
66
+ // For bidirectional, we need to manually process with source tracking
67
+ // This is a custom implementation that adds data-qd attributes
68
+
69
+ const QUIKDOWN_STYLES = {
70
+ h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',
71
+ h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',
72
+ h3: 'font-size:1.25em;font-weight:600;margin:1em 0',
73
+ h4: 'font-size:1em;font-weight:600;margin:1.33em 0',
74
+ h5: 'font-size:.875em;font-weight:600;margin:1.67em 0',
75
+ h6: 'font-size:.85em;font-weight:600;margin:2em 0',
76
+ pre: 'background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0',
77
+ code: 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace',
78
+ blockquote: 'border-left:4px solid #ddd;margin-left:0;padding-left:1em',
79
+ table: 'border-collapse:collapse;width:100%;margin:1em 0',
80
+ th: 'border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left',
81
+ td: 'border:1px solid #ddd;padding:8px;text-align:left',
82
+ hr: 'border:none;border-top:1px solid #ddd;margin:1em 0',
83
+ img: 'max-width:100%;height:auto',
84
+ a: 'color:#06c;text-decoration:underline',
85
+ strong: 'font-weight:bold',
86
+ em: 'font-style:italic',
87
+ del: 'text-decoration:line-through',
88
+ ul: 'margin:.5em 0;padding-left:2em',
89
+ ol: 'margin:.5em 0;padding-left:2em',
90
+ li: 'margin:.25em 0',
91
+ 'task-item': 'list-style:none',
92
+ 'task-checkbox': 'margin-right:.5em'
93
+ };
94
+
95
+ const getAttr = createGetAttrBD(inline_styles, QUIKDOWN_STYLES);
96
+
97
+ // Process markdown with source tracking
98
+ let html = markdown;
99
+
100
+ // Phase 1: Extract and protect code blocks
101
+ const codeBlocks = [];
102
+ const inlineCodes = [];
103
+
104
+ // Extract fenced code blocks
105
+ html = html.replace(/^(```|~~~)([^\n]*)\n([\s\S]*?)^\1$/gm, (match, fence, lang, code) => {
106
+ const placeholder = `§CB${codeBlocks.length}§`;
107
+ codeBlocks.push({
108
+ fence,
109
+ lang: lang.trim(),
110
+ code: escapeHtml(code.trimEnd()),
111
+ original: match
112
+ });
113
+ return placeholder;
114
+ });
115
+
116
+ // Extract inline code
117
+ html = html.replace(/`([^`]+)`/g, (match, code) => {
118
+ const placeholder = `§IC${inlineCodes.length}§`;
119
+ inlineCodes.push({
120
+ code: escapeHtml(code),
121
+ original: match
122
+ });
123
+ return placeholder;
124
+ });
125
+
126
+ // Escape HTML
127
+ html = escapeHtml(html);
128
+
129
+ // Process headings with source tracking
130
+ html = html.replace(/^(#{1,6})\s+(.+?)\s*#*$/gm, (match, hashes, content) => {
131
+ const level = hashes.length;
132
+ const sourceMarker = hashes;
133
+ return `<h${level}${getAttr('h' + level, '', sourceMarker)}>${content}</h${level}>`;
134
+ });
135
+
136
+ // Process bold/italic/strikethrough with source tracking
137
+ html = html.replace(/\*\*(.+?)\*\*/g, `<strong${getAttr('strong', '', '**')}>$1</strong>`);
138
+ html = html.replace(/__(.+?)__/g, `<strong${getAttr('strong', '', '__')}>$1</strong>`);
139
+ html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, `<em${getAttr('em', '', '*')}>$1</em>`);
140
+ html = html.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, `<em${getAttr('em', '', '_')}>$1</em>`);
141
+ html = html.replace(/~~(.+?)~~/g, `<del${getAttr('del', '', '~~')}>$1</del>`);
142
+
143
+ // Process blockquotes
144
+ html = html.replace(/^&gt;\s+(.+)$/gm, `<blockquote${getAttr('blockquote', '', '>')}>$1</blockquote>`);
145
+ html = html.replace(/<\/blockquote>\n<blockquote[^>]*>/g, '\n');
146
+
147
+ // Process horizontal rules
148
+ html = html.replace(/^---+$/gm, `<hr${getAttr('hr', '', '---')}>`);
149
+
150
+ // Process lists (simplified for now)
151
+ html = processListsBD(html, getAttr);
152
+
153
+ // Process links and images
154
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
155
+ return `<img${getAttr('img', '', '!')} src="${src}" alt="${alt}" data-qd-alt="${escapeHtml(alt)}" data-qd-src="${escapeHtml(src)}">`;
156
+ });
157
+
158
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
159
+ return `<a${getAttr('a', '', '[')} href="${href}" data-qd-text="${escapeHtml(text)}">${text}</a>`;
160
+ });
161
+
162
+ // Process tables
163
+ html = processTablesBD(html, getAttr);
164
+
165
+ // Line breaks
166
+ html = html.replace(/ $/gm, '<br data-qd=" ">');
167
+
168
+ // Paragraphs
169
+ html = html.replace(/\n\n+/g, '</p><p>');
170
+ html = '<p>' + html + '</p>';
171
+
172
+ // Clean up empty paragraphs and unwrap block elements
173
+ html = html.replace(/<p><\/p>/g, '');
174
+ html = html.replace(/<p>(<h[1-6][^>]*>)/g, '$1');
175
+ html = html.replace(/(<\/h[1-6]>)<\/p>/g, '$1');
176
+ html = html.replace(/<p>(<blockquote[^>]*>)/g, '$1');
177
+ html = html.replace(/(<\/blockquote>)<\/p>/g, '$1');
178
+ html = html.replace(/<p>(<ul[^>]*>|<ol[^>]*>)/g, '$1');
179
+ html = html.replace(/(<\/ul>|<\/ol>)<\/p>/g, '$1');
180
+ html = html.replace(/<p>(<hr[^>]*>)<\/p>/g, '$1');
181
+ html = html.replace(/<p>(<table[^>]*>)/g, '$1');
182
+ html = html.replace(/(<\/table>)<\/p>/g, '$1');
183
+
184
+ // Restore code blocks
185
+ codeBlocks.forEach((block, i) => {
186
+ const placeholder = `§CB${i}§`;
187
+ let replacement;
188
+
189
+ if (fence_plugin && typeof fence_plugin === 'function') {
190
+ replacement = fence_plugin(block.code, block.lang);
191
+ if (replacement === undefined) {
192
+ replacement = `<pre${getAttr('pre', '', block.fence)}><code data-qd-lang="${block.lang}">${block.code}</code></pre>`;
193
+ }
194
+ } else {
195
+ replacement = `<pre${getAttr('pre', '', block.fence)} data-qd-fence="${block.fence}" data-qd-lang="${block.lang}"><code>${block.code}</code></pre>`;
196
+ }
197
+
198
+ html = html.replace(placeholder, replacement);
199
+ });
200
+
201
+ // Restore inline codes
202
+ inlineCodes.forEach((item, i) => {
203
+ const placeholder = `§IC${i}§`;
204
+ html = html.replace(placeholder, `<code${getAttr('code', '', '`')}>${item.code}</code>`);
205
+ });
206
+
207
+ return html.trim();
208
+ }
209
+
210
+ // Process lists with source tracking
211
+ function processListsBD(text, getAttr, inline_styles) {
212
+ const lines = text.split('\n');
213
+ const result = [];
214
+ let listStack = [];
215
+
216
+ for (let i = 0; i < lines.length; i++) {
217
+ const line = lines[i];
218
+ const match = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.+)$/);
219
+
220
+ if (match) {
221
+ const [, indent, marker, content] = match;
222
+ const level = Math.floor(indent.length / 2);
223
+ const isOrdered = /^\d+\./.test(marker);
224
+ const listType = isOrdered ? 'ol' : 'ul';
225
+ const sourceMarker = isOrdered ? '1.' : marker;
226
+
227
+ // Handle task lists
228
+ let listItemContent = content;
229
+ let taskAttrs = '';
230
+ const taskMatch = content.match(/^\[([x ])\]\s+(.*)$/i);
231
+ if (taskMatch && !isOrdered) {
232
+ const [, checked, taskContent] = taskMatch;
233
+ const isChecked = checked.toLowerCase() === 'x';
234
+ listItemContent = `<input type="checkbox"${getAttr('task-checkbox', '', '[')}${isChecked ? ' checked' : ''}> ${taskContent}`;
235
+ taskAttrs = getAttr('task-item', '', '- [ ]');
236
+ }
237
+
238
+ // Close deeper levels
239
+ while (listStack.length > level + 1) {
240
+ const list = listStack.pop();
241
+ result.push(`</${list.type}>`);
242
+ }
243
+
244
+ // Open new level if needed
245
+ if (listStack.length === level) {
246
+ listStack.push({ type: listType, level, marker: sourceMarker });
247
+ result.push(`<${listType}${getAttr(listType, '', sourceMarker)}>`);
248
+ }
249
+
250
+ const liAttr = taskAttrs || getAttr('li', '', sourceMarker);
251
+ result.push(`<li${liAttr}>${listItemContent}</li>`);
252
+ } else {
253
+ // Close all lists
254
+ while (listStack.length > 0) {
255
+ const list = listStack.pop();
256
+ result.push(`</${list.type}>`);
257
+ }
258
+ result.push(line);
259
+ }
260
+ }
261
+
262
+ // Close remaining lists
263
+ while (listStack.length > 0) {
264
+ const list = listStack.pop();
265
+ result.push(`</${list.type}>`);
266
+ }
267
+
268
+ return result.join('\n');
269
+ }
270
+
271
+ // Process tables with source tracking
272
+ function processTablesBD(text, getAttr) {
273
+ const lines = text.split('\n');
274
+ const result = [];
275
+ let inTable = false;
276
+ let tableLines = [];
277
+
278
+ for (let i = 0; i < lines.length; i++) {
279
+ const line = lines[i].trim();
280
+
281
+ if (line.includes('|')) {
282
+ if (!inTable) {
283
+ inTable = true;
284
+ tableLines = [];
285
+ }
286
+ tableLines.push(line);
287
+ } else {
288
+ if (inTable) {
289
+ const tableHtml = buildTableBD(tableLines, getAttr);
290
+ if (tableHtml) {
291
+ result.push(tableHtml);
292
+ } else {
293
+ result.push(...tableLines);
294
+ }
295
+ inTable = false;
296
+ tableLines = [];
297
+ }
298
+ result.push(lines[i]);
299
+ }
300
+ }
301
+
302
+ if (inTable && tableLines.length > 0) {
303
+ const tableHtml = buildTableBD(tableLines, getAttr);
304
+ if (tableHtml) {
305
+ result.push(tableHtml);
306
+ } else {
307
+ result.push(...tableLines);
308
+ }
309
+ }
310
+
311
+ return result.join('\n');
312
+ }
313
+
314
+ // Build table with source tracking
315
+ function buildTableBD(lines, getAttr) {
316
+ if (lines.length < 2) return null;
317
+
318
+ // Find separator
319
+ let separatorIndex = -1;
320
+ let alignments = [];
321
+
322
+ for (let i = 1; i < lines.length; i++) {
323
+ if (/^\|?[\s\-:|]+\|?$/.test(lines[i]) && lines[i].includes('-')) {
324
+ separatorIndex = i;
325
+ const cells = lines[i].replace(/^\|/, '').replace(/\|$/, '').split('|');
326
+ alignments = cells.map(cell => {
327
+ const trimmed = cell.trim();
328
+ if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
329
+ if (trimmed.endsWith(':')) return 'right';
330
+ return 'left';
331
+ });
332
+ break;
333
+ }
334
+ }
335
+
336
+ if (separatorIndex === -1) return null;
337
+
338
+ let html = `<table${getAttr('table', '', '|')} data-qd-align="${alignments.join(',')}">\n`;
339
+
340
+ // Headers
341
+ if (separatorIndex > 0) {
342
+ html += `<thead${getAttr('thead', '', '|')}>\n<tr${getAttr('tr', '', '|')}>\n`;
343
+ const cells = lines[0].replace(/^\|/, '').replace(/\|$/, '').split('|');
344
+ cells.forEach((cell, i) => {
345
+ const align = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
346
+ html += `<th${getAttr('th', align, '|')} data-qd-align="${alignments[i] || 'left'}">${escapeHtml(cell.trim())}</th>\n`;
347
+ });
348
+ html += '</tr>\n</thead>\n';
349
+ }
350
+
351
+ // Body
352
+ const bodyLines = lines.slice(separatorIndex + 1);
353
+ if (bodyLines.length > 0) {
354
+ html += `<tbody${getAttr('tbody', '', '|')}>\n`;
355
+ bodyLines.forEach(line => {
356
+ html += `<tr${getAttr('tr', '', '|')}>\n`;
357
+ const cells = line.replace(/^\|/, '').replace(/\|$/, '').split('|');
358
+ cells.forEach((cell, i) => {
359
+ const align = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
360
+ html += `<td${getAttr('td', align, '|')} data-qd-align="${alignments[i] || 'left'}">${escapeHtml(cell.trim())}</td>\n`;
361
+ });
362
+ html += '</tr>\n';
363
+ });
364
+ html += '</tbody>\n';
365
+ }
366
+
367
+ html += '</table>';
368
+ return html;
369
+ }
370
+
371
+ /**
372
+ * Convert HTML back to Markdown by walking the DOM tree
373
+ * Uses data-qd attributes when available, falls back to canonical forms
374
+ * Assumes browser environment with DOM API available
375
+ */
376
+ quikdown_bd.toMarkdown = function(htmlOrElement) {
377
+ // Accept either HTML string or DOM element
378
+ let container;
379
+ if (typeof htmlOrElement === 'string') {
380
+ container = document.createElement('div');
381
+ container.innerHTML = htmlOrElement;
382
+ } else if (htmlOrElement instanceof Element) {
383
+ container = htmlOrElement;
384
+ } else {
385
+ return '';
386
+ }
387
+
388
+ // Walk the DOM tree and reconstruct markdown
389
+ function walkNode(node, parentContext = {}) {
390
+ if (node.nodeType === Node.TEXT_NODE) {
391
+ // Return text content, preserving whitespace where needed
392
+ return node.textContent;
393
+ }
394
+
395
+ if (node.nodeType !== Node.ELEMENT_NODE) {
396
+ return '';
397
+ }
398
+
399
+ const tag = node.tagName.toLowerCase();
400
+ const dataQd = node.getAttribute('data-qd');
401
+ const styles = window.getComputedStyle ? window.getComputedStyle(node) : {};
402
+
403
+ // Process children with context
404
+ let childContent = '';
405
+ for (let child of node.childNodes) {
406
+ childContent += walkNode(child, { parentTag: tag, ...parentContext });
407
+ }
408
+
409
+ // Determine markdown based on element and attributes
410
+ switch (tag) {
411
+ case 'h1':
412
+ case 'h2':
413
+ case 'h3':
414
+ case 'h4':
415
+ case 'h5':
416
+ case 'h6':
417
+ const level = parseInt(tag[1]);
418
+ const prefix = dataQd || '#'.repeat(level);
419
+ return `${prefix} ${childContent.trim()}\n\n`;
420
+
421
+ case 'strong':
422
+ case 'b':
423
+ // Check if it's bold through style too
424
+ if (styles.fontWeight === 'bold' || styles.fontWeight >= 700 || tag === 'strong' || tag === 'b') {
425
+ const boldMarker = dataQd || '**';
426
+ return `${boldMarker}${childContent}${boldMarker}`;
427
+ }
428
+ return childContent;
429
+
430
+ case 'em':
431
+ case 'i':
432
+ // Check for italic through style
433
+ if (styles.fontStyle === 'italic' || tag === 'em' || tag === 'i') {
434
+ const emMarker = dataQd || '*';
435
+ return `${emMarker}${childContent}${emMarker}`;
436
+ }
437
+ return childContent;
438
+
439
+ case 'del':
440
+ case 's':
441
+ case 'strike':
442
+ const delMarker = dataQd || '~~';
443
+ return `${delMarker}${childContent}${delMarker}`;
444
+
445
+ case 'code':
446
+ // Skip if inside pre (handled by pre)
447
+ if (parentContext.parentTag === 'pre') {
448
+ return childContent;
449
+ }
450
+ const codeMarker = dataQd || '`';
451
+ return `${codeMarker}${childContent}${codeMarker}`;
452
+
453
+ case 'pre':
454
+ const fence = node.getAttribute('data-qd-fence') || dataQd || '```';
455
+ const lang = node.getAttribute('data-qd-lang') || '';
456
+ // Look for code element child
457
+ const codeEl = node.querySelector('code');
458
+ const codeContent = codeEl ? codeEl.textContent : childContent;
459
+ return `${fence}${lang}\n${codeContent.trimEnd()}\n${fence}\n\n`;
460
+
461
+ case 'blockquote':
462
+ const quoteMarker = dataQd || '>';
463
+ const lines = childContent.trim().split('\n');
464
+ return lines.map(line => `${quoteMarker} ${line}`).join('\n') + '\n\n';
465
+
466
+ case 'hr':
467
+ const hrMarker = dataQd || '---';
468
+ return `${hrMarker}\n\n`;
469
+
470
+ case 'br':
471
+ const brMarker = dataQd || ' ';
472
+ return `${brMarker}\n`;
473
+
474
+ case 'a':
475
+ const linkText = node.getAttribute('data-qd-text') || childContent.trim();
476
+ const href = node.getAttribute('href') || '';
477
+ // Check for autolinks
478
+ if (linkText === href && !dataQd) {
479
+ return `<${href}>`;
480
+ }
481
+ return `[${linkText}](${href})`;
482
+
483
+ case 'img':
484
+ const alt = node.getAttribute('data-qd-alt') || node.getAttribute('alt') || '';
485
+ const src = node.getAttribute('data-qd-src') || node.getAttribute('src') || '';
486
+ const imgMarker = dataQd || '!';
487
+ return `${imgMarker}[${alt}](${src})`;
488
+
489
+ case 'ul':
490
+ case 'ol':
491
+ return walkList(node, tag === 'ol') + '\n';
492
+
493
+ case 'li':
494
+ // Handled by list processor
495
+ return childContent;
496
+
497
+ case 'table':
498
+ return walkTable(node) + '\n\n';
499
+
500
+ case 'p':
501
+ // Check if it's actually a paragraph or just a wrapper
502
+ if (childContent.trim()) {
503
+ return childContent.trim() + '\n\n';
504
+ }
505
+ return '';
506
+
507
+ case 'div':
508
+ // Check if it's a mermaid container
509
+ if (node.classList && node.classList.contains('mermaid-container')) {
510
+ const fence = node.getAttribute('data-qd-fence') || '```';
511
+ const lang = node.getAttribute('data-qd-lang') || 'mermaid';
512
+ // Look for the source element
513
+ const sourceElement = node.querySelector('.mermaid-source');
514
+ if (sourceElement) {
515
+ // Decode HTML entities
516
+ const temp = document.createElement('div');
517
+ temp.innerHTML = sourceElement.innerHTML;
518
+ const code = temp.textContent;
519
+ return `${fence}${lang}\n${code}\n${fence}\n\n`;
520
+ }
521
+ // Fallback: try to extract from the mermaid element
522
+ const mermaidElement = node.querySelector('.mermaid');
523
+ if (mermaidElement && mermaidElement.textContent.includes('graph')) {
524
+ return `${fence}${lang}\n${mermaidElement.textContent.trim()}\n${fence}\n\n`;
525
+ }
526
+ }
527
+ // Check if it's a standalone mermaid diagram (legacy)
528
+ if (node.classList && node.classList.contains('mermaid')) {
529
+ const fence = node.getAttribute('data-qd-fence') || '```';
530
+ const lang = node.getAttribute('data-qd-lang') || 'mermaid';
531
+ const code = node.textContent.trim();
532
+ return `${fence}${lang}\n${code}\n${fence}\n\n`;
533
+ }
534
+ // Pass through other divs
535
+ return childContent;
536
+
537
+ case 'span':
538
+ // Pass through container elements
539
+ return childContent;
540
+
541
+ default:
542
+ return childContent;
543
+ }
544
+ }
545
+
546
+ // Walk list elements
547
+ function walkList(listNode, isOrdered, depth = 0) {
548
+ let result = '';
549
+ let index = 1;
550
+ const indent = ' '.repeat(depth);
551
+
552
+ for (let child of listNode.children) {
553
+ if (child.tagName !== 'LI') continue;
554
+
555
+ const dataQd = child.getAttribute('data-qd');
556
+ let marker = dataQd || (isOrdered ? `${index}.` : '-');
557
+
558
+ // Check for task list checkbox
559
+ const checkbox = child.querySelector('input[type="checkbox"]');
560
+ if (checkbox) {
561
+ const checked = checkbox.checked ? 'x' : ' ';
562
+ marker = '-';
563
+ // Get text without the checkbox
564
+ let text = '';
565
+ for (let node of child.childNodes) {
566
+ if (node.nodeType === Node.TEXT_NODE) {
567
+ text += node.textContent;
568
+ } else if (node.tagName && node.tagName !== 'INPUT') {
569
+ text += walkNode(node);
570
+ }
571
+ }
572
+ result += `${indent}${marker} [${checked}] ${text.trim()}\n`;
573
+ } else {
574
+ let itemContent = '';
575
+
576
+ for (let node of child.childNodes) {
577
+ if (node.tagName === 'UL' || node.tagName === 'OL') {
578
+ itemContent += walkList(node, node.tagName === 'OL', depth + 1);
579
+ } else {
580
+ itemContent += walkNode(node);
581
+ }
582
+ }
583
+
584
+ result += `${indent}${marker} ${itemContent.trim()}\n`;
585
+ }
586
+
587
+ index++;
588
+ }
589
+
590
+ return result;
591
+ }
592
+
593
+ // Walk table elements
594
+ function walkTable(table) {
595
+ let result = '';
596
+ const alignData = table.getAttribute('data-qd-align');
597
+ const alignments = alignData ? alignData.split(',') : [];
598
+
599
+ // Process header
600
+ const thead = table.querySelector('thead');
601
+ if (thead) {
602
+ const headerRow = thead.querySelector('tr');
603
+ if (headerRow) {
604
+ const headers = [];
605
+ for (let th of headerRow.querySelectorAll('th')) {
606
+ headers.push(th.textContent.trim());
607
+ }
608
+ result += '| ' + headers.join(' | ') + ' |\n';
609
+
610
+ // Add separator with alignment
611
+ const separators = headers.map((_, i) => {
612
+ const align = alignments[i] || th.getAttribute('data-qd-align') || 'left';
613
+ if (align === 'center') return ':---:';
614
+ if (align === 'right') return '---:';
615
+ return '---';
616
+ });
617
+ result += '| ' + separators.join(' | ') + ' |\n';
618
+ }
619
+ }
620
+
621
+ // Process body
622
+ const tbody = table.querySelector('tbody');
623
+ if (tbody) {
624
+ for (let row of tbody.querySelectorAll('tr')) {
625
+ const cells = [];
626
+ for (let td of row.querySelectorAll('td')) {
627
+ cells.push(td.textContent.trim());
628
+ }
629
+ if (cells.length > 0) {
630
+ result += '| ' + cells.join(' | ') + ' |\n';
631
+ }
632
+ }
633
+ }
634
+
635
+ return result.trim();
636
+ }
637
+
638
+ // Process the DOM tree
639
+ let markdown = walkNode(container);
640
+
641
+ // Clean up
642
+ markdown = markdown.replace(/\n{3,}/g, '\n\n'); // Remove excessive newlines
643
+ markdown = markdown.trim();
644
+
645
+ return markdown;
646
+ };
647
+
648
+ // Add emitStyles method (same as core)
649
+ quikdown_bd.emitStyles = function(prefix = 'quikdown-', theme = 'light') {
650
+ // This would generate CSS based on the styles
651
+ // For now, returning empty string as placeholder
652
+ // In production, this would generate the full CSS
653
+ return '';
654
+ };
655
+
656
+ // Configure method
657
+ quikdown_bd.configure = function(options) {
658
+ return function(markdown) {
659
+ return quikdown_bd(markdown, options);
660
+ };
661
+ };
662
+
663
+ // Version property
664
+ quikdown_bd.version = VERSION;
665
+
666
+ // Export for both module and browser
667
+ if (typeof module !== 'undefined' && module.exports) {
668
+ module.exports = quikdown_bd;
669
+ }
670
+
671
+ if (typeof window !== 'undefined') {
672
+ window.quikdown_bd = quikdown_bd;
673
+ }
674
+
675
+ export { quikdown_bd as default };