quikdown 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/quikdown.cjs CHANGED
@@ -1,16 +1,11 @@
1
1
  /**
2
2
  * quikdown - Lightweight Markdown Parser
3
- * @version 1.0.1
3
+ * @version 1.0.3
4
4
  * @license BSD-2-Clause
5
5
  * @copyright DeftIO 2025
6
6
  */
7
7
  'use strict';
8
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
9
  /**
15
10
  * quikdown - A minimal markdown parser optimized for chat/LLM output
16
11
  * Supports tables, code blocks, lists, and common formatting
@@ -22,68 +17,71 @@ const quikdownVersion = "1.0.1";
22
17
  * @returns {string} - The rendered HTML
23
18
  */
24
19
 
20
+ // Version will be injected at build time
21
+ const quikdownVersion = '1.0.3';
25
22
 
26
- function quikdown(markdown, options = {}) {
27
- if (!markdown || typeof markdown !== 'string') {
28
- return '';
29
- }
30
-
31
- const { fence_plugin, inline_styles = false } = options;
23
+ // Constants for reuse
24
+ const CLASS_PREFIX = 'quikdown-';
25
+ const PLACEHOLDER_CB = '§CB';
26
+ const PLACEHOLDER_IC = '§IC';
32
27
 
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
- };
28
+ // Escape map at module level
29
+ const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
65
30
 
66
- // Helper to get class or style attribute
67
- function getAttr(tag, additionalStyle = '') {
31
+ // Single source of truth for all style definitions - optimized
32
+ const QUIKDOWN_STYLES = {
33
+ h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',
34
+ h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',
35
+ h3: 'font-size:1.25em;font-weight:600;margin:1em 0',
36
+ h4: 'font-size:1em;font-weight:600;margin:1.33em 0',
37
+ h5: 'font-size:.875em;font-weight:600;margin:1.67em 0',
38
+ h6: 'font-size:.85em;font-weight:600;margin:2em 0',
39
+ pre: 'background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0',
40
+ code: 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace',
41
+ blockquote: 'border-left:4px solid #ddd;margin-left:0;padding-left:1em',
42
+ table: 'border-collapse:collapse;width:100%;margin:1em 0',
43
+ th: 'border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left',
44
+ td: 'border:1px solid #ddd;padding:8px;text-align:left',
45
+ hr: 'border:none;border-top:1px solid #ddd;margin:1em 0',
46
+ img: 'max-width:100%;height:auto',
47
+ a: 'color:#06c;text-decoration:underline',
48
+ strong: 'font-weight:bold',
49
+ em: 'font-style:italic',
50
+ del: 'text-decoration:line-through',
51
+ ul: 'margin:.5em 0;padding-left:2em',
52
+ ol: 'margin:.5em 0;padding-left:2em',
53
+ li: 'margin:.25em 0',
54
+ // Task list specific styles
55
+ 'task-item': 'list-style:none',
56
+ 'task-checkbox': 'margin-right:.5em'
57
+ };
58
+
59
+ // Factory function to create getAttr for a given context
60
+ function createGetAttr(inline_styles, styles) {
61
+ return function(tag, additionalStyle = '') {
68
62
  if (inline_styles) {
69
- const style = styles[tag] || '';
70
- const fullStyle = additionalStyle ? `${style}; ${additionalStyle}` : style;
71
- return fullStyle ? ` style="${fullStyle}"` : '';
63
+ const style = styles[tag];
64
+ if (!style && !additionalStyle) return '';
65
+ const fullStyle = additionalStyle ? (style ? `${style};${additionalStyle}` : additionalStyle) : style;
66
+ return ` style="${fullStyle}"`;
72
67
  } else {
73
- return ` class="quikdown-${tag}"`;
68
+ return ` class="${CLASS_PREFIX}${tag}"`;
74
69
  }
70
+ };
71
+ }
72
+
73
+ function quikdown(markdown, options = {}) {
74
+ if (!markdown || typeof markdown !== 'string') {
75
+ return '';
75
76
  }
77
+
78
+ const { fence_plugin, inline_styles = false } = options;
79
+ const styles = QUIKDOWN_STYLES; // Use module-level styles
80
+ const getAttr = createGetAttr(inline_styles, styles); // Create getAttr once
76
81
 
77
82
  // Escape HTML entities to prevent XSS
78
83
  function escapeHtml(text) {
79
- const map = {
80
- '&': '&amp;',
81
- '<': '&lt;',
82
- '>': '&gt;',
83
- '"': '&quot;',
84
- "'": '&#39;'
85
- };
86
- return text.replace(/[&<>"']/g, m => map[m]);
84
+ return text.replace(/[&<>"']/g, m => ESC_MAP[m]);
87
85
  }
88
86
 
89
87
  // Sanitize URLs to prevent XSS attacks
@@ -93,7 +91,6 @@ function quikdown(markdown, options = {}) {
93
91
  // If unsafe URLs are explicitly allowed, return as-is
94
92
  if (allowUnsafe) return url;
95
93
 
96
- // Trim and lowercase for checking
97
94
  const trimmedUrl = url.trim();
98
95
  const lowerUrl = trimmedUrl.toLowerCase();
99
96
 
@@ -122,19 +119,24 @@ function quikdown(markdown, options = {}) {
122
119
  const inlineCodes = [];
123
120
 
124
121
  // 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}%%%`;
122
+ // Match paired fences - ``` with ``` and ~~~ with ~~~
123
+ // Fence must be at start of line
124
+ html = html.replace(/^(```|~~~)([^\n]*)\n([\s\S]*?)^\1$/gm, (match, fence, lang, code) => {
125
+ const placeholder = `${PLACEHOLDER_CB}${codeBlocks.length}§`;
126
+
127
+ // Trim the language specification
128
+ const langTrimmed = lang ? lang.trim() : '';
127
129
 
128
130
  // If custom fence plugin is provided, use it
129
131
  if (fence_plugin && typeof fence_plugin === 'function') {
130
132
  codeBlocks.push({
131
- lang: lang || '',
133
+ lang: langTrimmed,
132
134
  code: code.trimEnd(),
133
135
  custom: true
134
136
  });
135
137
  } else {
136
138
  codeBlocks.push({
137
- lang: lang || '',
139
+ lang: langTrimmed,
138
140
  code: escapeHtml(code.trimEnd()),
139
141
  custom: false
140
142
  });
@@ -144,7 +146,7 @@ function quikdown(markdown, options = {}) {
144
146
 
145
147
  // Extract inline code
146
148
  html = html.replace(/`([^`]+)`/g, (match, code) => {
147
- const placeholder = `%%%INLINECODE${inlineCodes.length}%%%`;
149
+ const placeholder = `${PLACEHOLDER_IC}${inlineCodes.length}§`;
148
150
  inlineCodes.push(escapeHtml(code));
149
151
  return placeholder;
150
152
  });
@@ -155,7 +157,7 @@ function quikdown(markdown, options = {}) {
155
157
  // Phase 2: Process block elements
156
158
 
157
159
  // Process tables
158
- html = processTable(html, inline_styles, styles);
160
+ html = processTable(html, getAttr);
159
161
 
160
162
  // Process headings (supports optional trailing #'s)
161
163
  html = html.replace(/^(#{1,6})\s+(.+?)\s*#*$/gm, (match, hashes, content) => {
@@ -172,7 +174,7 @@ function quikdown(markdown, options = {}) {
172
174
  html = html.replace(/^---+$/gm, `<hr${getAttr('hr')}>`);
173
175
 
174
176
  // Process lists
175
- html = processLists(html, inline_styles, styles);
177
+ html = processLists(html, getAttr, inline_styles);
176
178
 
177
179
  // Phase 3: Process inline elements
178
180
 
@@ -197,16 +199,18 @@ function quikdown(markdown, options = {}) {
197
199
  return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
198
200
  });
199
201
 
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>`);
202
+ // Process inline formatting (bold, italic, strikethrough)
203
+ const inlinePatterns = [
204
+ [/\*\*(.+?)\*\*/g, 'strong'],
205
+ [/__(.+?)__/g, 'strong'],
206
+ [/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, 'em'],
207
+ [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em'],
208
+ [/~~(.+?)~~/g, 'del']
209
+ ];
210
+
211
+ inlinePatterns.forEach(([pattern, tag]) => {
212
+ html = html.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);
213
+ });
210
214
 
211
215
  // Line breaks (two spaces at end of line)
212
216
  html = html.replace(/ $/gm, `<br${getAttr('br')}>`);
@@ -215,21 +219,26 @@ function quikdown(markdown, options = {}) {
215
219
  html = html.replace(/\n\n+/g, '</p><p>');
216
220
  html = '<p>' + html + '</p>';
217
221
 
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');
222
+ // Clean up empty paragraphs and unwrap block elements
223
+ const cleanupPatterns = [
224
+ [/<p><\/p>/g, ''],
225
+ [/<p>(<h[1-6][^>]*>)/g, '$1'],
226
+ [/(<\/h[1-6]>)<\/p>/g, '$1'],
227
+ [/<p>(<blockquote[^>]*>)/g, '$1'],
228
+ [/(<\/blockquote>)<\/p>/g, '$1'],
229
+ [/<p>(<ul[^>]*>|<ol[^>]*>)/g, '$1'],
230
+ [/(<\/ul>|<\/ol>)<\/p>/g, '$1'],
231
+ [/<p>(<hr[^>]*>)<\/p>/g, '$1'],
232
+ [/<p>(<table[^>]*>)/g, '$1'],
233
+ [/(<\/table>)<\/p>/g, '$1'],
234
+ [/<p>(<pre[^>]*>)/g, '$1'],
235
+ [/(<\/pre>)<\/p>/g, '$1'],
236
+ [new RegExp(`<p>(${PLACEHOLDER_CB}\\d)<\/p>`, 'g'), '$1']
237
+ ];
238
+
239
+ cleanupPatterns.forEach(([pattern, replacement]) => {
240
+ html = html.replace(pattern, replacement);
241
+ });
233
242
 
234
243
  // Phase 4: Restore code blocks and inline code
235
244
 
@@ -253,13 +262,13 @@ function quikdown(markdown, options = {}) {
253
262
  replacement = `<pre${getAttr('pre')}><code${codeAttr}>${block.code}</code></pre>`;
254
263
  }
255
264
 
256
- const placeholder = `%%%CODEBLOCK${i}%%%`;
265
+ const placeholder = `${PLACEHOLDER_CB}${i}§`;
257
266
  html = html.replace(placeholder, replacement);
258
267
  });
259
268
 
260
269
  // Restore inline code
261
270
  inlineCodes.forEach((code, i) => {
262
- const placeholder = `%%%INLINECODE${i}%%%`;
271
+ const placeholder = `${PLACEHOLDER_IC}${i}§`;
263
272
  html = html.replace(placeholder, `<code${getAttr('code')}>${code}</code>`);
264
273
  });
265
274
 
@@ -269,31 +278,21 @@ function quikdown(markdown, options = {}) {
269
278
  /**
270
279
  * Process inline markdown formatting
271
280
  */
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>`);
281
+ function processInlineMarkdown(text, getAttr) {
282
+
283
+ // Process inline formatting patterns
284
+ const patterns = [
285
+ [/\*\*(.+?)\*\*/g, 'strong'],
286
+ [/__(.+?)__/g, 'strong'],
287
+ [/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, 'em'],
288
+ [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em'],
289
+ [/~~(.+?)~~/g, 'del'],
290
+ [/`([^`]+)`/g, 'code']
291
+ ];
292
+
293
+ patterns.forEach(([pattern, tag]) => {
294
+ text = text.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);
295
+ });
297
296
 
298
297
  return text;
299
298
  }
@@ -301,7 +300,7 @@ function processInlineMarkdown(text, inline_styles, styles) {
301
300
  /**
302
301
  * Process markdown tables
303
302
  */
304
- function processTable(text, inline_styles, styles) {
303
+ function processTable(text, getAttr) {
305
304
  const lines = text.split('\n');
306
305
  const result = [];
307
306
  let inTable = false;
@@ -321,7 +320,7 @@ function processTable(text, inline_styles, styles) {
321
320
  // Not a table line
322
321
  if (inTable) {
323
322
  // Process the accumulated table
324
- const tableHtml = buildTable(tableLines, inline_styles, styles);
323
+ const tableHtml = buildTable(tableLines, getAttr);
325
324
  if (tableHtml) {
326
325
  result.push(tableHtml);
327
326
  } else {
@@ -337,7 +336,7 @@ function processTable(text, inline_styles, styles) {
337
336
 
338
337
  // Handle table at end of text
339
338
  if (inTable && tableLines.length > 0) {
340
- const tableHtml = buildTable(tableLines, inline_styles, styles);
339
+ const tableHtml = buildTable(tableLines, getAttr);
341
340
  if (tableHtml) {
342
341
  result.push(tableHtml);
343
342
  } else {
@@ -351,17 +350,7 @@ function processTable(text, inline_styles, styles) {
351
350
  /**
352
351
  * Build an HTML table from markdown table lines
353
352
  */
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
- }
353
+ function buildTable(lines, getAttr) {
365
354
 
366
355
  if (lines.length < 2) return null;
367
356
 
@@ -401,8 +390,8 @@ function buildTable(lines, inline_styles, styles) {
401
390
  // Handle pipes at start/end or not
402
391
  const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
403
392
  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);
393
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
394
+ const processedCell = processInlineMarkdown(cell.trim(), getAttr);
406
395
  html += `<th${getAttr('th', alignStyle)}>${processedCell}</th>\n`;
407
396
  });
408
397
  html += '</tr>\n';
@@ -418,8 +407,8 @@ function buildTable(lines, inline_styles, styles) {
418
407
  // Handle pipes at start/end or not
419
408
  const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
420
409
  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);
410
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
411
+ const processedCell = processInlineMarkdown(cell.trim(), getAttr);
423
412
  html += `<td${getAttr('td', alignStyle)}>${processedCell}</td>\n`;
424
413
  });
425
414
  html += '</tr>\n';
@@ -434,17 +423,7 @@ function buildTable(lines, inline_styles, styles) {
434
423
  /**
435
424
  * Process markdown lists (ordered and unordered)
436
425
  */
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
- }
426
+ function processLists(text, getAttr, inline_styles) {
448
427
 
449
428
  const lines = text.split('\n');
450
429
  const result = [];
@@ -468,10 +447,10 @@ function processLists(text, inline_styles, styles) {
468
447
  const [, checked, taskContent] = taskMatch;
469
448
  const isChecked = checked.toLowerCase() === 'x';
470
449
  const checkboxAttr = inline_styles
471
- ? ' style="margin-right: 0.5em"'
472
- : ' class="quikdown-task-checkbox"';
450
+ ? ' style="margin-right:.5em"'
451
+ : ` class="${CLASS_PREFIX}task-checkbox"`;
473
452
  listItemContent = `<input type="checkbox"${checkboxAttr}${isChecked ? ' checked' : ''} disabled> ${taskContent}`;
474
- taskListClass = inline_styles ? ' style="list-style: none"' : ' class="quikdown-task-item"';
453
+ taskListClass = inline_styles ? ' style="list-style:none"' : ` class="${CLASS_PREFIX}task-item"`;
475
454
  }
476
455
 
477
456
  // Close deeper levels
@@ -519,39 +498,56 @@ function processLists(text, inline_styles, styles) {
519
498
 
520
499
  /**
521
500
  * Emit CSS styles for quikdown elements
501
+ * @param {string} prefix - Optional class prefix (default: 'quikdown-')
502
+ * @param {string} theme - Optional theme: 'light' (default) or 'dark'
522
503
  * @returns {string} CSS string with quikdown styles
523
504
  */
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'
505
+ quikdown.emitStyles = function(prefix = 'quikdown-', theme = 'light') {
506
+ const styles = QUIKDOWN_STYLES;
507
+
508
+ // Define theme color overrides
509
+ const themeOverrides = {
510
+ dark: {
511
+ '#f4f4f4': '#2a2a2a', // pre background
512
+ '#f0f0f0': '#2a2a2a', // code background
513
+ '#f2f2f2': '#2a2a2a', // th background
514
+ '#ddd': '#3a3a3a', // borders
515
+ '#06c': '#6db3f2', // links
516
+ _textColor: '#e0e0e0'
517
+ },
518
+ light: {
519
+ _textColor: '#333' // Explicit text color for light theme
520
+ }
549
521
  };
550
522
 
551
523
  let css = '';
552
524
  for (const [tag, style] of Object.entries(styles)) {
553
525
  if (style) {
554
- css += `.quikdown-${tag} { ${style} }\n`;
526
+ let themedStyle = style;
527
+
528
+ // Apply theme overrides if dark theme
529
+ if (theme === 'dark' && themeOverrides.dark) {
530
+ // Replace colors
531
+ for (const [oldColor, newColor] of Object.entries(themeOverrides.dark)) {
532
+ if (!oldColor.startsWith('_')) {
533
+ themedStyle = themedStyle.replace(new RegExp(oldColor, 'g'), newColor);
534
+ }
535
+ }
536
+
537
+ // Add text color for certain elements in dark theme
538
+ const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
539
+ if (needsTextColor.includes(tag)) {
540
+ themedStyle += `;color:${themeOverrides.dark._textColor}`;
541
+ }
542
+ } else if (theme === 'light' && themeOverrides.light) {
543
+ // Add explicit text color for light theme elements too
544
+ const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
545
+ if (needsTextColor.includes(tag)) {
546
+ themedStyle += `;color:${themeOverrides.light._textColor}`;
547
+ }
548
+ }
549
+
550
+ css += `.${prefix}${tag} { ${themedStyle} }\n`;
555
551
  }
556
552
  }
557
553
 
@@ -0,0 +1,70 @@
1
+ /**
2
+ * quikdown - Lightweight Markdown Parser
3
+ * TypeScript definitions
4
+ */
5
+
6
+ declare module 'quikdown' {
7
+ /**
8
+ * Options for configuring the quikdown parser
9
+ */
10
+ export interface QuikdownOptions {
11
+ /**
12
+ * Custom renderer for fenced code blocks.
13
+ * Return undefined to use default rendering.
14
+ * @param content - The code block content (unescaped)
15
+ * @param language - The language identifier (or empty string)
16
+ * @returns HTML string or undefined for default rendering
17
+ */
18
+ fence_plugin?: (content: string, language: string) => string | undefined;
19
+
20
+ /**
21
+ * If true, uses inline styles instead of CSS classes.
22
+ * Useful for emails or environments without CSS support.
23
+ * @default false
24
+ */
25
+ inline_styles?: boolean;
26
+
27
+ /**
28
+ * If true, allows potentially unsafe URLs (javascript:, data:, etc).
29
+ * Only use with trusted content.
30
+ * @default false
31
+ */
32
+ allow_unsafe_urls?: boolean;
33
+ }
34
+
35
+ /**
36
+ * Parse markdown to HTML
37
+ * @param markdown - The markdown source text
38
+ * @param options - Optional configuration
39
+ * @returns The rendered HTML string
40
+ */
41
+ function quikdown(markdown: string, options?: QuikdownOptions): string;
42
+
43
+ namespace quikdown {
44
+ /**
45
+ * Generate CSS styles for quikdown classes with theme support
46
+ * @param prefix - CSS class prefix (default: 'quikdown-')
47
+ * @param theme - Theme name: 'light' (default) or 'dark'
48
+ * @returns CSS string with themed .quikdown-* styles
49
+ */
50
+ export function emitStyles(prefix?: string, theme?: 'light' | 'dark'): string;
51
+
52
+ /**
53
+ * Create a configured parser function with preset options
54
+ * @param options - Configuration to apply to all parsing
55
+ * @returns A parser function with the options pre-applied
56
+ */
57
+ export function configure(options: QuikdownOptions): (markdown: string) => string;
58
+
59
+ /**
60
+ * The version of quikdown
61
+ */
62
+ export const version: string;
63
+ }
64
+
65
+ export = quikdown;
66
+ }
67
+
68
+ // For ES6 module imports
69
+ export default quikdown;
70
+ export { QuikdownOptions };