quikdown 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,587 @@
1
+ /**
2
+ * quikdown - Lightweight Markdown Parser
3
+ * @version 1.0.1
4
+ * @license BSD-2-Clause
5
+ * @copyright DeftIO 2025
6
+ */
7
+ 'use strict';
8
+
9
+ // Auto-generated version file - DO NOT EDIT MANUALLY
10
+ // This file is automatically updated by tools/updateVersion.js
11
+
12
+ const quikdownVersion = "1.0.1";
13
+
14
+ /**
15
+ * quikdown - A minimal markdown parser optimized for chat/LLM output
16
+ * Supports tables, code blocks, lists, and common formatting
17
+ * @param {string} markdown - The markdown source text
18
+ * @param {Object} options - Optional configuration object
19
+ * @param {Function} options.fence_plugin - Custom renderer for fenced code blocks
20
+ * (content, fence_string) => html string
21
+ * @param {boolean} options.inline_styles - If true, uses inline styles instead of classes
22
+ * @returns {string} - The rendered HTML
23
+ */
24
+
25
+
26
+ function quikdown(markdown, options = {}) {
27
+ if (!markdown || typeof markdown !== 'string') {
28
+ return '';
29
+ }
30
+
31
+ const { fence_plugin, inline_styles = false } = options;
32
+
33
+ // Style definitions - deduplicated to save space
34
+ const headingStyle = 'margin-top: 0.5em; margin-bottom: 0.3em';
35
+ const listStyle = 'margin: 0.5em 0; padding-left: 2em';
36
+ const cellBorder = 'border: 1px solid #ddd; padding: 8px';
37
+
38
+ const styles = {
39
+ h1: headingStyle,
40
+ h2: headingStyle,
41
+ h3: headingStyle,
42
+ h4: headingStyle,
43
+ h5: headingStyle,
44
+ h6: headingStyle,
45
+ pre: 'background: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto',
46
+ code: 'background: #f0f0f0; padding: 2px 4px; border-radius: 3px',
47
+ blockquote: 'border-left: 4px solid #ddd; margin-left: 0; padding-left: 1em; color: #666',
48
+ table: 'border-collapse: collapse; width: 100%; margin: 1em 0',
49
+ thead: '',
50
+ tbody: '',
51
+ tr: '',
52
+ th: cellBorder + '; background-color: #f2f2f2; font-weight: bold',
53
+ td: cellBorder + '; text-align: left',
54
+ hr: 'border: none; border-top: 1px solid #ddd; margin: 1em 0',
55
+ img: 'max-width: 100%; height: auto',
56
+ a: 'color: #0066cc; text-decoration: underline',
57
+ strong: 'font-weight: bold',
58
+ em: 'font-style: italic',
59
+ del: 'text-decoration: line-through',
60
+ ul: listStyle,
61
+ ol: listStyle,
62
+ li: 'margin: 0.25em 0',
63
+ br: ''
64
+ };
65
+
66
+ // Helper to get class or style attribute
67
+ function getAttr(tag, additionalStyle = '') {
68
+ if (inline_styles) {
69
+ const style = styles[tag] || '';
70
+ const fullStyle = additionalStyle ? `${style}; ${additionalStyle}` : style;
71
+ return fullStyle ? ` style="${fullStyle}"` : '';
72
+ } else {
73
+ return ` class="quikdown-${tag}"`;
74
+ }
75
+ }
76
+
77
+ // Escape HTML entities to prevent XSS
78
+ function escapeHtml(text) {
79
+ const map = {
80
+ '&': '&',
81
+ '<': '&lt;',
82
+ '>': '&gt;',
83
+ '"': '&quot;',
84
+ "'": '&#39;'
85
+ };
86
+ return text.replace(/[&<>"']/g, m => map[m]);
87
+ }
88
+
89
+ // Sanitize URLs to prevent XSS attacks
90
+ function sanitizeUrl(url, allowUnsafe = false) {
91
+ if (!url) return '';
92
+
93
+ // If unsafe URLs are explicitly allowed, return as-is
94
+ if (allowUnsafe) return url;
95
+
96
+ // Trim and lowercase for checking
97
+ const trimmedUrl = url.trim();
98
+ const lowerUrl = trimmedUrl.toLowerCase();
99
+
100
+ // Block dangerous protocols
101
+ const dangerousProtocols = ['javascript:', 'vbscript:', 'data:'];
102
+
103
+ for (const protocol of dangerousProtocols) {
104
+ if (lowerUrl.startsWith(protocol)) {
105
+ // Exception: Allow data:image/* for images
106
+ if (protocol === 'data:' && lowerUrl.startsWith('data:image/')) {
107
+ return trimmedUrl;
108
+ }
109
+ // Return safe empty link for dangerous protocols
110
+ return '#';
111
+ }
112
+ }
113
+
114
+ return trimmedUrl;
115
+ }
116
+
117
+ // Process the markdown in phases
118
+ let html = markdown;
119
+
120
+ // Phase 1: Extract and protect code blocks and inline code
121
+ const codeBlocks = [];
122
+ const inlineCodes = [];
123
+
124
+ // Extract fenced code blocks first (supports both ``` and ~~~)
125
+ html = html.replace(/(?:```|~~~)([^\n]*)\n([\s\S]*?)(?:```|~~~)/g, (match, lang, code) => {
126
+ const placeholder = `%%%CODEBLOCK${codeBlocks.length}%%%`;
127
+
128
+ // If custom fence plugin is provided, use it
129
+ if (fence_plugin && typeof fence_plugin === 'function') {
130
+ codeBlocks.push({
131
+ lang: lang || '',
132
+ code: code.trimEnd(),
133
+ custom: true
134
+ });
135
+ } else {
136
+ codeBlocks.push({
137
+ lang: lang || '',
138
+ code: escapeHtml(code.trimEnd()),
139
+ custom: false
140
+ });
141
+ }
142
+ return placeholder;
143
+ });
144
+
145
+ // Extract inline code
146
+ html = html.replace(/`([^`]+)`/g, (match, code) => {
147
+ const placeholder = `%%%INLINECODE${inlineCodes.length}%%%`;
148
+ inlineCodes.push(escapeHtml(code));
149
+ return placeholder;
150
+ });
151
+
152
+ // Now escape HTML in the rest of the content
153
+ html = escapeHtml(html);
154
+
155
+ // Phase 2: Process block elements
156
+
157
+ // Process tables
158
+ html = processTable(html, inline_styles, styles);
159
+
160
+ // Process headings (supports optional trailing #'s)
161
+ html = html.replace(/^(#{1,6})\s+(.+?)\s*#*$/gm, (match, hashes, content) => {
162
+ const level = hashes.length;
163
+ return `<h${level}${getAttr('h' + level)}>${content}</h${level}>`;
164
+ });
165
+
166
+ // Process blockquotes (must handle escaped > since we already escaped HTML)
167
+ html = html.replace(/^&gt;\s+(.+)$/gm, `<blockquote${getAttr('blockquote')}>$1</blockquote>`);
168
+ // Merge consecutive blockquotes
169
+ html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n');
170
+
171
+ // Process horizontal rules
172
+ html = html.replace(/^---+$/gm, `<hr${getAttr('hr')}>`);
173
+
174
+ // Process lists
175
+ html = processLists(html, inline_styles, styles);
176
+
177
+ // Phase 3: Process inline elements
178
+
179
+ // Images (must come before links, with URL sanitization)
180
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
181
+ const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
182
+ return `<img${getAttr('img')} src="${sanitizedSrc}" alt="${alt}">`;
183
+ });
184
+
185
+ // Links (with URL sanitization)
186
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
187
+ // Sanitize URL to prevent XSS
188
+ const sanitizedHref = sanitizeUrl(href, options.allow_unsafe_urls);
189
+ const isExternal = /^https?:\/\//i.test(sanitizedHref);
190
+ const rel = isExternal ? ' rel="noopener noreferrer"' : '';
191
+ return `<a${getAttr('a')} href="${sanitizedHref}"${rel}>${text}</a>`;
192
+ });
193
+
194
+ // Autolinks - convert bare URLs to clickable links
195
+ html = html.replace(/(^|\s)(https?:\/\/[^\s<]+)/g, (match, prefix, url) => {
196
+ const sanitizedUrl = sanitizeUrl(url, options.allow_unsafe_urls);
197
+ return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
198
+ });
199
+
200
+ // Bold (must use non-greedy matching)
201
+ html = html.replace(/\*\*(.+?)\*\*/g, `<strong${getAttr('strong')}>$1</strong>`);
202
+ html = html.replace(/__(.+?)__/g, `<strong${getAttr('strong')}>$1</strong>`);
203
+
204
+ // Italic (must not match bold markers)
205
+ html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, `<em${getAttr('em')}>$1</em>`);
206
+ html = html.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, `<em${getAttr('em')}>$1</em>`);
207
+
208
+ // Strikethrough
209
+ html = html.replace(/~~(.+?)~~/g, `<del${getAttr('del')}>$1</del>`);
210
+
211
+ // Line breaks (two spaces at end of line)
212
+ html = html.replace(/ $/gm, `<br${getAttr('br')}>`);
213
+
214
+ // Paragraphs (double newlines)
215
+ html = html.replace(/\n\n+/g, '</p><p>');
216
+ html = '<p>' + html + '</p>';
217
+
218
+ // Clean up empty paragraphs and unwrap block elements (account for attributes)
219
+ html = html.replace(/<p><\/p>/g, '');
220
+ html = html.replace(/<p>(<h[1-6][^>]*>)/g, '$1');
221
+ html = html.replace(/(<\/h[1-6]>)<\/p>/g, '$1');
222
+ html = html.replace(/<p>(<blockquote[^>]*>)/g, '$1');
223
+ html = html.replace(/(<\/blockquote>)<\/p>/g, '$1');
224
+ html = html.replace(/<p>(<ul[^>]*>|<ol[^>]*>)/g, '$1');
225
+ html = html.replace(/(<\/ul>|<\/ol>)<\/p>/g, '$1');
226
+ html = html.replace(/<p>(<hr[^>]*>)<\/p>/g, '$1');
227
+ html = html.replace(/<p>(<table[^>]*>)/g, '$1');
228
+ html = html.replace(/(<\/table>)<\/p>/g, '$1');
229
+ html = html.replace(/<p>(<pre[^>]*>)/g, '$1');
230
+ html = html.replace(/(<\/pre>)<\/p>/g, '$1');
231
+ // Also unwrap code block placeholders
232
+ html = html.replace(/<p>(%%%CODEBLOCK\d+%%%)<\/p>/g, '$1');
233
+
234
+ // Phase 4: Restore code blocks and inline code
235
+
236
+ // Restore code blocks
237
+ codeBlocks.forEach((block, i) => {
238
+ let replacement;
239
+
240
+ if (block.custom && fence_plugin) {
241
+ // Use custom fence plugin
242
+ replacement = fence_plugin(block.code, block.lang);
243
+ // If plugin returns undefined, fall back to default rendering
244
+ if (replacement === undefined) {
245
+ const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
246
+ const codeAttr = inline_styles ? getAttr('code') : langClass;
247
+ replacement = `<pre${getAttr('pre')}><code${codeAttr}>${escapeHtml(block.code)}</code></pre>`;
248
+ }
249
+ } else {
250
+ // Default rendering
251
+ const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
252
+ const codeAttr = inline_styles ? getAttr('code') : langClass;
253
+ replacement = `<pre${getAttr('pre')}><code${codeAttr}>${block.code}</code></pre>`;
254
+ }
255
+
256
+ const placeholder = `%%%CODEBLOCK${i}%%%`;
257
+ html = html.replace(placeholder, replacement);
258
+ });
259
+
260
+ // Restore inline code
261
+ inlineCodes.forEach((code, i) => {
262
+ const placeholder = `%%%INLINECODE${i}%%%`;
263
+ html = html.replace(placeholder, `<code${getAttr('code')}>${code}</code>`);
264
+ });
265
+
266
+ return html.trim();
267
+ }
268
+
269
+ /**
270
+ * Process inline markdown formatting
271
+ */
272
+ function processInlineMarkdown(text, inline_styles, styles) {
273
+ // Helper to get attributes
274
+ function getAttr(tag, additionalStyle = '') {
275
+ if (inline_styles) {
276
+ const style = styles[tag] || '';
277
+ const fullStyle = additionalStyle ? `${style}; ${additionalStyle}` : style;
278
+ return fullStyle ? ` style="${fullStyle}"` : '';
279
+ } else {
280
+ return ` class="quikdown-${tag}"`;
281
+ }
282
+ }
283
+
284
+ // Process bold
285
+ text = text.replace(/\*\*(.+?)\*\*/g, `<strong${getAttr('strong')}>$1</strong>`);
286
+ text = text.replace(/__(.+?)__/g, `<strong${getAttr('strong')}>$1</strong>`);
287
+
288
+ // Process italic (must not match bold markers)
289
+ text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, `<em${getAttr('em')}>$1</em>`);
290
+ text = text.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, `<em${getAttr('em')}>$1</em>`);
291
+
292
+ // Process strikethrough
293
+ text = text.replace(/~~(.+?)~~/g, `<del${getAttr('del')}>$1</del>`);
294
+
295
+ // Process inline code
296
+ text = text.replace(/`([^`]+)`/g, `<code${getAttr('code')}>$1</code>`);
297
+
298
+ return text;
299
+ }
300
+
301
+ /**
302
+ * Process markdown tables
303
+ */
304
+ function processTable(text, inline_styles, styles) {
305
+ const lines = text.split('\n');
306
+ const result = [];
307
+ let inTable = false;
308
+ let tableLines = [];
309
+
310
+ for (let i = 0; i < lines.length; i++) {
311
+ const line = lines[i].trim();
312
+
313
+ // Check if this line looks like a table row (with or without trailing |)
314
+ if (line.includes('|') && (line.startsWith('|') || /[^\\|]/.test(line))) {
315
+ if (!inTable) {
316
+ inTable = true;
317
+ tableLines = [];
318
+ }
319
+ tableLines.push(line);
320
+ } else {
321
+ // Not a table line
322
+ if (inTable) {
323
+ // Process the accumulated table
324
+ const tableHtml = buildTable(tableLines, inline_styles, styles);
325
+ if (tableHtml) {
326
+ result.push(tableHtml);
327
+ } else {
328
+ // Not a valid table, restore original lines
329
+ result.push(...tableLines);
330
+ }
331
+ inTable = false;
332
+ tableLines = [];
333
+ }
334
+ result.push(lines[i]);
335
+ }
336
+ }
337
+
338
+ // Handle table at end of text
339
+ if (inTable && tableLines.length > 0) {
340
+ const tableHtml = buildTable(tableLines, inline_styles, styles);
341
+ if (tableHtml) {
342
+ result.push(tableHtml);
343
+ } else {
344
+ result.push(...tableLines);
345
+ }
346
+ }
347
+
348
+ return result.join('\n');
349
+ }
350
+
351
+ /**
352
+ * Build an HTML table from markdown table lines
353
+ */
354
+ function buildTable(lines, inline_styles, styles) {
355
+ // Helper to get attributes
356
+ function getAttr(tag, additionalStyle = '') {
357
+ if (inline_styles) {
358
+ const style = styles[tag] || '';
359
+ const fullStyle = additionalStyle ? `${style}; ${additionalStyle}` : style;
360
+ return fullStyle ? ` style="${fullStyle}"` : '';
361
+ } else {
362
+ return ` class="quikdown-${tag}"`;
363
+ }
364
+ }
365
+
366
+ if (lines.length < 2) return null;
367
+
368
+ // Check for separator line (second line should be the separator)
369
+ let separatorIndex = -1;
370
+ for (let i = 1; i < lines.length; i++) {
371
+ // Support separator with or without leading/trailing pipes
372
+ if (/^\|?[\s\-:|]+\|?$/.test(lines[i]) && lines[i].includes('-')) {
373
+ separatorIndex = i;
374
+ break;
375
+ }
376
+ }
377
+
378
+ if (separatorIndex === -1) return null;
379
+
380
+ const headerLines = lines.slice(0, separatorIndex);
381
+ const bodyLines = lines.slice(separatorIndex + 1);
382
+
383
+ // Parse alignment from separator
384
+ const separator = lines[separatorIndex];
385
+ // Handle pipes at start/end or not
386
+ const separatorCells = separator.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
387
+ const alignments = separatorCells.map(cell => {
388
+ const trimmed = cell.trim();
389
+ if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
390
+ if (trimmed.endsWith(':')) return 'right';
391
+ return 'left';
392
+ });
393
+
394
+ let html = `<table${getAttr('table')}>\n`;
395
+
396
+ // Build header
397
+ if (headerLines.length > 0) {
398
+ html += `<thead${getAttr('thead')}>\n`;
399
+ headerLines.forEach(line => {
400
+ html += `<tr${getAttr('tr')}>\n`;
401
+ // Handle pipes at start/end or not
402
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
403
+ cells.forEach((cell, i) => {
404
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align: ${alignments[i]}` : '';
405
+ const processedCell = processInlineMarkdown(cell.trim(), inline_styles, styles);
406
+ html += `<th${getAttr('th', alignStyle)}>${processedCell}</th>\n`;
407
+ });
408
+ html += '</tr>\n';
409
+ });
410
+ html += '</thead>\n';
411
+ }
412
+
413
+ // Build body
414
+ if (bodyLines.length > 0) {
415
+ html += `<tbody${getAttr('tbody')}>\n`;
416
+ bodyLines.forEach(line => {
417
+ html += `<tr${getAttr('tr')}>\n`;
418
+ // Handle pipes at start/end or not
419
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
420
+ cells.forEach((cell, i) => {
421
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align: ${alignments[i]}` : '';
422
+ const processedCell = processInlineMarkdown(cell.trim(), inline_styles, styles);
423
+ html += `<td${getAttr('td', alignStyle)}>${processedCell}</td>\n`;
424
+ });
425
+ html += '</tr>\n';
426
+ });
427
+ html += '</tbody>\n';
428
+ }
429
+
430
+ html += '</table>';
431
+ return html;
432
+ }
433
+
434
+ /**
435
+ * Process markdown lists (ordered and unordered)
436
+ */
437
+ function processLists(text, inline_styles, styles) {
438
+ // Helper to get attributes
439
+ function getAttr(tag, additionalStyle = '') {
440
+ if (inline_styles) {
441
+ const style = styles[tag] || '';
442
+ const fullStyle = additionalStyle ? `${style}; ${additionalStyle}` : style;
443
+ return fullStyle ? ` style="${fullStyle}"` : '';
444
+ } else {
445
+ return ` class="quikdown-${tag}"`;
446
+ }
447
+ }
448
+
449
+ const lines = text.split('\n');
450
+ const result = [];
451
+ let listStack = []; // Track nested lists
452
+
453
+ for (let i = 0; i < lines.length; i++) {
454
+ const line = lines[i];
455
+ const match = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.+)$/);
456
+
457
+ if (match) {
458
+ const [, indent, marker, content] = match;
459
+ const level = Math.floor(indent.length / 2);
460
+ const isOrdered = /^\d+\./.test(marker);
461
+ const listType = isOrdered ? 'ol' : 'ul';
462
+
463
+ // Check for task list items
464
+ let listItemContent = content;
465
+ let taskListClass = '';
466
+ const taskMatch = content.match(/^\[([x ])\]\s+(.*)$/i);
467
+ if (taskMatch && !isOrdered) {
468
+ const [, checked, taskContent] = taskMatch;
469
+ const isChecked = checked.toLowerCase() === 'x';
470
+ const checkboxAttr = inline_styles
471
+ ? ' style="margin-right: 0.5em"'
472
+ : ' class="quikdown-task-checkbox"';
473
+ listItemContent = `<input type="checkbox"${checkboxAttr}${isChecked ? ' checked' : ''} disabled> ${taskContent}`;
474
+ taskListClass = inline_styles ? ' style="list-style: none"' : ' class="quikdown-task-item"';
475
+ }
476
+
477
+ // Close deeper levels
478
+ while (listStack.length > level + 1) {
479
+ const list = listStack.pop();
480
+ result.push(`</${list.type}>`);
481
+ }
482
+
483
+ // Open new level if needed
484
+ if (listStack.length === level) {
485
+ // Need to open a new list
486
+ listStack.push({ type: listType, level });
487
+ result.push(`<${listType}${getAttr(listType)}>`);
488
+ } else if (listStack.length === level + 1) {
489
+ // Check if we need to switch list type
490
+ const currentList = listStack[listStack.length - 1];
491
+ if (currentList.type !== listType) {
492
+ result.push(`</${currentList.type}>`);
493
+ listStack.pop();
494
+ listStack.push({ type: listType, level });
495
+ result.push(`<${listType}${getAttr(listType)}>`);
496
+ }
497
+ }
498
+
499
+ const liAttr = taskListClass || getAttr('li');
500
+ result.push(`<li${liAttr}>${listItemContent}</li>`);
501
+ } else {
502
+ // Not a list item, close all lists
503
+ while (listStack.length > 0) {
504
+ const list = listStack.pop();
505
+ result.push(`</${list.type}>`);
506
+ }
507
+ result.push(line);
508
+ }
509
+ }
510
+
511
+ // Close any remaining lists
512
+ while (listStack.length > 0) {
513
+ const list = listStack.pop();
514
+ result.push(`</${list.type}>`);
515
+ }
516
+
517
+ return result.join('\n');
518
+ }
519
+
520
+ /**
521
+ * Emit CSS styles for quikdown elements
522
+ * @returns {string} CSS string with quikdown styles
523
+ */
524
+ quikdown.emitStyles = function() {
525
+ const styles = {
526
+ h1: 'margin-top: 0.5em; margin-bottom: 0.3em',
527
+ h2: 'margin-top: 0.5em; margin-bottom: 0.3em',
528
+ h3: 'margin-top: 0.5em; margin-bottom: 0.3em',
529
+ h4: 'margin-top: 0.5em; margin-bottom: 0.3em',
530
+ h5: 'margin-top: 0.5em; margin-bottom: 0.3em',
531
+ h6: 'margin-top: 0.5em; margin-bottom: 0.3em',
532
+ pre: 'background: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto',
533
+ code: 'background: #f0f0f0; padding: 2px 4px; border-radius: 3px',
534
+ blockquote: 'border-left: 4px solid #ddd; margin-left: 0; padding-left: 1em; color: #666',
535
+ table: 'border-collapse: collapse; width: 100%; margin: 1em 0',
536
+ th: 'border: 1px solid #ddd; padding: 8px; background-color: #f2f2f2; font-weight: bold',
537
+ td: 'border: 1px solid #ddd; padding: 8px; text-align: left',
538
+ hr: 'border: none; border-top: 1px solid #ddd; margin: 1em 0',
539
+ img: 'max-width: 100%; height: auto',
540
+ a: 'color: #0066cc; text-decoration: underline',
541
+ strong: 'font-weight: bold',
542
+ em: 'font-style: italic',
543
+ del: 'text-decoration: line-through',
544
+ ul: 'margin: 0.5em 0; padding-left: 2em',
545
+ ol: 'margin: 0.5em 0; padding-left: 2em',
546
+ li: 'margin: 0.25em 0',
547
+ 'task-item': 'list-style: none',
548
+ 'task-checkbox': 'margin-right: 0.5em'
549
+ };
550
+
551
+ let css = '';
552
+ for (const [tag, style] of Object.entries(styles)) {
553
+ if (style) {
554
+ css += `.quikdown-${tag} { ${style} }\n`;
555
+ }
556
+ }
557
+
558
+ return css;
559
+ };
560
+
561
+ /**
562
+ * Configure quikdown with options and return a function
563
+ * @param {Object} options - Configuration options
564
+ * @returns {Function} Configured quikdown function
565
+ */
566
+ quikdown.configure = function(options) {
567
+ return function(markdown) {
568
+ return quikdown(markdown, options);
569
+ };
570
+ };
571
+
572
+ /**
573
+ * Version information
574
+ */
575
+ quikdown.version = quikdownVersion;
576
+
577
+ // Export for both CommonJS and ES6
578
+ if (typeof module !== 'undefined' && module.exports) {
579
+ module.exports = quikdown;
580
+ }
581
+
582
+ // For browser global
583
+ if (typeof window !== 'undefined') {
584
+ window.quikdown = quikdown;
585
+ }
586
+
587
+ module.exports = quikdown;