quikdown 1.2.9 → 1.2.11

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 (68) hide show
  1. package/README.md +5 -5
  2. package/dist/quikdown.cjs +104 -5
  3. package/dist/quikdown.d.ts +12 -0
  4. package/dist/quikdown.dark.css +1 -1
  5. package/dist/quikdown.esm.js +104 -5
  6. package/dist/quikdown.esm.min.js +2 -2
  7. package/dist/quikdown.esm.min.js.gz +0 -0
  8. package/dist/quikdown.esm.min.js.map +1 -1
  9. package/dist/quikdown.light.css +1 -1
  10. package/dist/quikdown.umd.js +104 -5
  11. package/dist/quikdown.umd.min.js +2 -2
  12. package/dist/quikdown.umd.min.js.gz +0 -0
  13. package/dist/quikdown.umd.min.js.map +1 -1
  14. package/dist/quikdown_ast.cjs +16 -28
  15. package/dist/quikdown_ast.esm.js +16 -28
  16. package/dist/quikdown_ast.esm.min.js +2 -2
  17. package/dist/quikdown_ast.esm.min.js.gz +0 -0
  18. package/dist/quikdown_ast.esm.min.js.map +1 -1
  19. package/dist/quikdown_ast.umd.js +16 -28
  20. package/dist/quikdown_ast.umd.min.js +2 -2
  21. package/dist/quikdown_ast.umd.min.js.gz +0 -0
  22. package/dist/quikdown_ast.umd.min.js.map +1 -1
  23. package/dist/quikdown_ast_html.cjs +17 -29
  24. package/dist/quikdown_ast_html.esm.js +17 -29
  25. package/dist/quikdown_ast_html.esm.min.js +2 -2
  26. package/dist/quikdown_ast_html.esm.min.js.gz +0 -0
  27. package/dist/quikdown_ast_html.esm.min.js.map +1 -1
  28. package/dist/quikdown_ast_html.umd.js +17 -29
  29. package/dist/quikdown_ast_html.umd.min.js +2 -2
  30. package/dist/quikdown_ast_html.umd.min.js.gz +0 -0
  31. package/dist/quikdown_ast_html.umd.min.js.map +1 -1
  32. package/dist/quikdown_bd.cjs +104 -5
  33. package/dist/quikdown_bd.esm.js +104 -5
  34. package/dist/quikdown_bd.esm.min.js +2 -2
  35. package/dist/quikdown_bd.esm.min.js.gz +0 -0
  36. package/dist/quikdown_bd.esm.min.js.map +1 -1
  37. package/dist/quikdown_bd.umd.js +104 -5
  38. package/dist/quikdown_bd.umd.min.js +2 -2
  39. package/dist/quikdown_bd.umd.min.js.gz +0 -0
  40. package/dist/quikdown_bd.umd.min.js.map +1 -1
  41. package/dist/quikdown_edit.cjs +244 -12
  42. package/dist/quikdown_edit.esm.js +244 -12
  43. package/dist/quikdown_edit.esm.min.js +3 -3
  44. package/dist/quikdown_edit.esm.min.js.gz +0 -0
  45. package/dist/quikdown_edit.esm.min.js.map +1 -1
  46. package/dist/quikdown_edit.umd.js +244 -12
  47. package/dist/quikdown_edit.umd.min.js +3 -3
  48. package/dist/quikdown_edit.umd.min.js.gz +0 -0
  49. package/dist/quikdown_edit.umd.min.js.map +1 -1
  50. package/dist/quikdown_json.cjs +17 -29
  51. package/dist/quikdown_json.esm.js +17 -29
  52. package/dist/quikdown_json.esm.min.js +2 -2
  53. package/dist/quikdown_json.esm.min.js.gz +0 -0
  54. package/dist/quikdown_json.esm.min.js.map +1 -1
  55. package/dist/quikdown_json.umd.js +17 -29
  56. package/dist/quikdown_json.umd.min.js +2 -2
  57. package/dist/quikdown_json.umd.min.js.gz +0 -0
  58. package/dist/quikdown_json.umd.min.js.map +1 -1
  59. package/dist/quikdown_yaml.cjs +17 -29
  60. package/dist/quikdown_yaml.esm.js +17 -29
  61. package/dist/quikdown_yaml.esm.min.js +2 -2
  62. package/dist/quikdown_yaml.esm.min.js.gz +0 -0
  63. package/dist/quikdown_yaml.esm.min.js.map +1 -1
  64. package/dist/quikdown_yaml.umd.js +17 -29
  65. package/dist/quikdown_yaml.umd.min.js +2 -2
  66. package/dist/quikdown_yaml.umd.min.js.gz +0 -0
  67. package/dist/quikdown_yaml.umd.min.js.map +1 -1
  68. package/package.json +10 -10
package/README.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  [![CI](https://github.com/deftio/quikdown/actions/workflows/ci.yml/badge.svg)](https://github.com/deftio/quikdown/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/quikdown.svg)](https://www.npmjs.com/package/quikdown)
5
- [![Coverage](https://img.shields.io/badge/coverage-95.4%25-brightgreen)](https://github.com/deftio/quikdown)
5
+ [![Coverage](https://img.shields.io/badge/coverage-96%25-brightgreen)](https://github.com/deftio/quikdown)
6
6
  [![License: BSD-2-Clause](https://img.shields.io/badge/License-BSD%202--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause)
7
- [![Bundle Size](https://img.shields.io/badge/minified-9.7KB-green.svg)](https://bundlephobia.com/package/quikdown)
7
+ [![Bundle Size](https://img.shields.io/badge/minified-10.8KB-green.svg)](https://bundlephobia.com/package/quikdown)
8
8
 
9
9
  A small, secure markdown parser and editor for browsers and Node.js. Three modules — use only what you need.
10
10
 
11
- - **quikdown.js** (9.3 KB) — Markdown to HTML parser. XSS-safe, fence plugin callbacks, inline styles or CSS classes.
12
- - **quikdown_bd.js** (14.1 KB) — Bidirectional: everything in core plus HTML to Markdown round-trip.
13
- - **quikdown_edit.js** (80 KB) — Drop-in split-view editor with live preview, undo/redo, bidirectional editing, and lazy-loaded fence plugins for code highlighting, Mermaid, MathJax, SVG, CSV, GeoJSON, and STL.
11
+ - **quikdown.js** (9.8 KB) — Markdown to HTML parser. XSS-safe, fence plugin callbacks, inline styles or CSS classes.
12
+ - **quikdown_bd.js** (14.6 KB) — Bidirectional: everything in core plus HTML to Markdown round-trip.
13
+ - **quikdown_edit.js** (84.3 KB) — Drop-in split-view editor with live preview, undo/redo, bidirectional editing, and lazy-loaded fence plugins for code highlighting, Mermaid, MathJax, SVG, CSV, GeoJSON, and STL.
14
14
  - **quikdown_edit_standalone.js** (3.8 MB) — Offline/air-gapped editor. Same as above but bundles highlight.js, Mermaid, DOMPurify, Leaflet, and Three.js — no CDN required. See [Standalone Docs](docs/standalone-editor.md).
15
15
  - **quikdown_ast.js** / **quikdown_json.js** / **quikdown_yaml.js** / **quikdown_ast_html.js** — AST companion libraries for structured output.
16
16
 
package/dist/quikdown.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * quikdown - Lightweight Markdown Parser
3
- * @version 1.2.9
3
+ * @version 1.2.11
4
4
  * @license BSD-2-Clause
5
5
  * @copyright DeftIO 2025
6
6
  */
@@ -114,7 +114,7 @@ function isDashHRLine(trimmed) {
114
114
  // ────────────────────────────────────────────────────────────────────
115
115
 
116
116
  /** Build-time version stamp (injected by tools/updateVersion) */
117
- const quikdownVersion = '1.2.9';
117
+ const quikdownVersion = '1.2.11';
118
118
 
119
119
  /** CSS class prefix used for all generated elements */
120
120
  const CLASS_PREFIX = 'quikdown-';
@@ -122,6 +122,10 @@ const CLASS_PREFIX = 'quikdown-';
122
122
  /** Placeholder sigils — chosen to be extremely unlikely in real text */
123
123
  const PLACEHOLDER_CB = '§CB'; // fenced code blocks
124
124
  const PLACEHOLDER_IC = '§IC'; // inline code spans
125
+ const PLACEHOLDER_HT = '§HT'; // safe HTML tags (limited mode)
126
+
127
+ /** Attributes whose values need URL sanitization */
128
+ const URL_ATTRIBUTES = { href:1, src:1, action:1, formaction:1 };
125
129
 
126
130
  /** HTML entity escape map */
127
131
  const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
@@ -256,6 +260,46 @@ function quikdown(markdown, options = {}) {
256
260
  return trimmedUrl;
257
261
  }
258
262
 
263
+ /**
264
+ * Sanitize attributes on an HTML tag string for limited mode.
265
+ * Strips on* event handlers (case-insensitive) and runs sanitizeUrl()
266
+ * on href/src/action/formaction values.
267
+ */
268
+ function sanitizeHtmlTagAttrs(tagStr) {
269
+ // Self-closing or void tag without attributes — pass through
270
+ if (!/\s/.test(tagStr.replace(/<\/?[a-zA-Z][a-zA-Z0-9]*/, '').replace(/\/?>$/, ''))) {
271
+ return tagStr;
272
+ }
273
+ // Parse: <tagname ...attrs... > or <tagname ...attrs... />
274
+ const m = tagStr.match(/^(<\/?[a-zA-Z][a-zA-Z0-9]*)([\s\S]*?)(\/?>)$/);
275
+ /* istanbul ignore next - defensive: Phase 1.5 regex guarantees valid tag shape */
276
+ if (!m) return tagStr;
277
+
278
+ const [, open, attrStr, close] = m;
279
+ // Match individual attributes: name="value", name='value', name=value, or bare name
280
+ // eslint-disable-next-line security/detect-unsafe-regex -- linear: no nested quantifiers
281
+ const attrRe = /([a-zA-Z_][\w\-.:]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
282
+ const attrs = [];
283
+ let am;
284
+ while ((am = attrRe.exec(attrStr)) !== null) {
285
+ const name = am[1];
286
+ const value = am[2] !== undefined ? am[2] : am[3] !== undefined ? am[3] : am[4];
287
+ // Strip event handlers (on*)
288
+ if (/^on/i.test(name)) continue;
289
+ if (value === undefined) {
290
+ // Boolean attribute (e.g. disabled, checked)
291
+ attrs.push(name);
292
+ } else {
293
+ let sanitized = value;
294
+ if (name.toLowerCase() in URL_ATTRIBUTES) {
295
+ sanitized = sanitizeUrl(value);
296
+ }
297
+ attrs.push(`${name}="${sanitized}"`);
298
+ }
299
+ }
300
+ return open + (attrs.length ? ' ' + attrs.join(' ') : '') + close;
301
+ }
302
+
259
303
  // ────────────────────────────────────────────────────────────────
260
304
  // Phase 1 — Code Extraction
261
305
  // ────────────────────────────────────────────────────────────────
@@ -307,17 +351,57 @@ function quikdown(markdown, options = {}) {
307
351
  return placeholder;
308
352
  });
309
353
 
354
+ // ────────────────────────────────────────────────────────────────
355
+ // Phase 1.5 — Safe HTML Extraction (whitelist mode)
356
+ // ────────────────────────────────────────────────────────────────
357
+ // When allow_unsafe_html is an object or array, extract whitelisted
358
+ // HTML tags, sanitize their attributes, and replace with placeholders.
359
+ // Non-whitelisted tags stay in text so Phase 2 will escape them.
360
+
361
+ const safeTags = [];
362
+ // Normalize: array → object for O(1) lookup; object used as-is
363
+ const htmlAllow = Array.isArray(allow_unsafe_html)
364
+ ? Object.fromEntries(allow_unsafe_html.map(t => [t, 1]))
365
+ : (allow_unsafe_html && typeof allow_unsafe_html === 'object') ? allow_unsafe_html : null;
366
+
367
+ if (htmlAllow) {
368
+ // Pass through HTML comments — browsers render them as nothing
369
+ html = html.replace(/<!--[\s\S]*?-->/g, (match) => {
370
+ const idx = safeTags.length;
371
+ safeTags.push(match);
372
+ return `${PLACEHOLDER_HT}${idx}§`;
373
+ });
374
+ html = html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g, (match, tagName) => {
375
+ if (tagName.toLowerCase() in htmlAllow) {
376
+ const sanitized = sanitizeHtmlTagAttrs(match);
377
+ const idx = safeTags.length;
378
+ safeTags.push(sanitized);
379
+ return `${PLACEHOLDER_HT}${idx}§`;
380
+ }
381
+ // Not whitelisted — leave in text for Phase 2 to escape
382
+ return match;
383
+ });
384
+ }
385
+
310
386
  // ────────────────────────────────────────────────────────────────
311
387
  // Phase 2 — HTML Escaping
312
388
  // ────────────────────────────────────────────────────────────────
313
389
  // All remaining text (everything except code placeholders) is escaped
314
390
  // to prevent XSS. The `allow_unsafe_html` option skips this for
315
391
  // trusted pipelines that intentionally embed raw HTML.
392
+ // For whitelist mode, escaping still runs (only `true` bypasses it).
316
393
 
317
- if (!allow_unsafe_html) {
394
+ if (allow_unsafe_html !== true) {
318
395
  html = escapeHtml(html);
319
396
  }
320
397
 
398
+ // Restore safe HTML tag placeholders after escaping
399
+ if (htmlAllow) {
400
+ safeTags.forEach((tag, i) => {
401
+ html = html.replace(`${PLACEHOLDER_HT}${i}§`, tag);
402
+ });
403
+ }
404
+
321
405
  // ────────────────────────────────────────────────────────────────
322
406
  // Phase 3 — Block Scanning + Inline Formatting + Paragraphs
323
407
  // ────────────────────────────────────────────────────────────────
@@ -359,7 +443,6 @@ function quikdown(markdown, options = {}) {
359
443
  // Images (must come before links — ![alt](src) vs [text](url))
360
444
  html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
361
445
  const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
362
- // Bidirectional attributes are only exercised via quikdown_bd bundle.
363
446
  /* istanbul ignore next - bd-only branch */
364
447
  const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
365
448
  /* istanbul ignore next - bd-only branch */
@@ -383,8 +466,12 @@ function quikdown(markdown, options = {}) {
383
466
  return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
384
467
  });
385
468
 
469
+ // Protect rendered tags so emphasis regexes don't see attribute
470
+ // values — fixes #3 (underscores in URLs interpreted as emphasis).
471
+ const savedTags = [];
472
+ html = html.replace(/<[^>]+>/g, m => { savedTags.push(m); return `%%T${savedTags.length - 1}%%`; });
473
+
386
474
  // Bold, italic, strikethrough
387
- // Order matters: ** before * (so ** isn't consumed as two *s)
388
475
  const inlinePatterns = [
389
476
  [/\*\*(.+?)\*\*/g, 'strong', '**'],
390
477
  [/__(.+?)__/g, 'strong', '__'],
@@ -396,6 +483,9 @@ function quikdown(markdown, options = {}) {
396
483
  html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
397
484
  });
398
485
 
486
+ // Restore protected tags
487
+ html = html.replace(/%%T(\d+)%%/g, (_, i) => savedTags[i]);
488
+
399
489
  // ── Step 5: Line breaks + paragraph wrapping ──
400
490
  if (lazy_linefeeds) {
401
491
  // Lazy linefeeds mode: every single \n becomes <br> EXCEPT:
@@ -553,6 +643,14 @@ function scanLineBlocks(text, getAttr, dataQd) {
553
643
  while (i < lines.length) {
554
644
  const line = lines[i];
555
645
 
646
+ // ── Markdown comment (reference-link hack) ──
647
+ // [//]: # (comment) or [//]: # "comment" or [//]: #
648
+ // These produce no output — standard markdown comment convention.
649
+ if (/^\[\/\/\]: #/.test(line)) {
650
+ i++;
651
+ continue;
652
+ }
653
+
556
654
  // ── Heading ──
557
655
  // Count leading '#' characters. Valid heading: 1-6 hashes then a space.
558
656
  // Example: "## Hello World ##" → <h2>Hello World</h2>
@@ -917,6 +1015,7 @@ quikdown.configure = function(options) {
917
1015
  /** Semantic version (injected at build time) */
918
1016
  quikdown.version = quikdownVersion;
919
1017
 
1018
+
920
1019
  // ════════════════════════════════════════════════════════════════════
921
1020
  // Exports
922
1021
  // ════════════════════════════════════════════════════════════════════
@@ -52,6 +52,17 @@ declare module 'quikdown' {
52
52
  */
53
53
  allow_unsafe_urls?: boolean;
54
54
 
55
+ /**
56
+ * Controls HTML passthrough in parsed output.
57
+ * - false (default): all HTML tags are escaped (secure)
58
+ * - true: all HTML passes through unchanged (unsafe, use with trusted content only)
59
+ * - Record<string, any>: object whose keys are lowercase tag names to allow
60
+ * (e.g. { img: 1, a: 1, div: 1 }). Use quikdown.SAFE_HTML_TAGS for a curated default.
61
+ * - string[]: array of tag names (converted to object internally)
62
+ * @default false
63
+ */
64
+ allow_unsafe_html?: boolean | Record<string, any> | string[];
65
+
55
66
  /**
56
67
  * If true, adds data-qd attributes for bidirectional conversion.
57
68
  * Enables HTML to Markdown conversion.
@@ -95,6 +106,7 @@ declare module 'quikdown' {
95
106
  * The version of quikdown
96
107
  */
97
108
  export const version: string;
109
+
98
110
  }
99
111
 
100
112
  export = quikdown;
@@ -5,7 +5,7 @@
5
5
  * Theme with container-based scoping.
6
6
  * Usage: <div class="quikdown-dark">...content...</div>
7
7
  *
8
- * @version 1.2.9
8
+ * @version 1.2.11
9
9
  * @source tools/generateThemeCSS.js
10
10
  */
11
11
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * quikdown - Lightweight Markdown Parser
3
- * @version 1.2.9
3
+ * @version 1.2.11
4
4
  * @license BSD-2-Clause
5
5
  * @copyright DeftIO 2025
6
6
  */
@@ -112,7 +112,7 @@ function isDashHRLine(trimmed) {
112
112
  // ────────────────────────────────────────────────────────────────────
113
113
 
114
114
  /** Build-time version stamp (injected by tools/updateVersion) */
115
- const quikdownVersion = '1.2.9';
115
+ const quikdownVersion = '1.2.11';
116
116
 
117
117
  /** CSS class prefix used for all generated elements */
118
118
  const CLASS_PREFIX = 'quikdown-';
@@ -120,6 +120,10 @@ const CLASS_PREFIX = 'quikdown-';
120
120
  /** Placeholder sigils — chosen to be extremely unlikely in real text */
121
121
  const PLACEHOLDER_CB = '§CB'; // fenced code blocks
122
122
  const PLACEHOLDER_IC = '§IC'; // inline code spans
123
+ const PLACEHOLDER_HT = '§HT'; // safe HTML tags (limited mode)
124
+
125
+ /** Attributes whose values need URL sanitization */
126
+ const URL_ATTRIBUTES = { href:1, src:1, action:1, formaction:1 };
123
127
 
124
128
  /** HTML entity escape map */
125
129
  const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
@@ -254,6 +258,46 @@ function quikdown(markdown, options = {}) {
254
258
  return trimmedUrl;
255
259
  }
256
260
 
261
+ /**
262
+ * Sanitize attributes on an HTML tag string for limited mode.
263
+ * Strips on* event handlers (case-insensitive) and runs sanitizeUrl()
264
+ * on href/src/action/formaction values.
265
+ */
266
+ function sanitizeHtmlTagAttrs(tagStr) {
267
+ // Self-closing or void tag without attributes — pass through
268
+ if (!/\s/.test(tagStr.replace(/<\/?[a-zA-Z][a-zA-Z0-9]*/, '').replace(/\/?>$/, ''))) {
269
+ return tagStr;
270
+ }
271
+ // Parse: <tagname ...attrs... > or <tagname ...attrs... />
272
+ const m = tagStr.match(/^(<\/?[a-zA-Z][a-zA-Z0-9]*)([\s\S]*?)(\/?>)$/);
273
+ /* istanbul ignore next - defensive: Phase 1.5 regex guarantees valid tag shape */
274
+ if (!m) return tagStr;
275
+
276
+ const [, open, attrStr, close] = m;
277
+ // Match individual attributes: name="value", name='value', name=value, or bare name
278
+ // eslint-disable-next-line security/detect-unsafe-regex -- linear: no nested quantifiers
279
+ const attrRe = /([a-zA-Z_][\w\-.:]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
280
+ const attrs = [];
281
+ let am;
282
+ while ((am = attrRe.exec(attrStr)) !== null) {
283
+ const name = am[1];
284
+ const value = am[2] !== undefined ? am[2] : am[3] !== undefined ? am[3] : am[4];
285
+ // Strip event handlers (on*)
286
+ if (/^on/i.test(name)) continue;
287
+ if (value === undefined) {
288
+ // Boolean attribute (e.g. disabled, checked)
289
+ attrs.push(name);
290
+ } else {
291
+ let sanitized = value;
292
+ if (name.toLowerCase() in URL_ATTRIBUTES) {
293
+ sanitized = sanitizeUrl(value);
294
+ }
295
+ attrs.push(`${name}="${sanitized}"`);
296
+ }
297
+ }
298
+ return open + (attrs.length ? ' ' + attrs.join(' ') : '') + close;
299
+ }
300
+
257
301
  // ────────────────────────────────────────────────────────────────
258
302
  // Phase 1 — Code Extraction
259
303
  // ────────────────────────────────────────────────────────────────
@@ -305,17 +349,57 @@ function quikdown(markdown, options = {}) {
305
349
  return placeholder;
306
350
  });
307
351
 
352
+ // ────────────────────────────────────────────────────────────────
353
+ // Phase 1.5 — Safe HTML Extraction (whitelist mode)
354
+ // ────────────────────────────────────────────────────────────────
355
+ // When allow_unsafe_html is an object or array, extract whitelisted
356
+ // HTML tags, sanitize their attributes, and replace with placeholders.
357
+ // Non-whitelisted tags stay in text so Phase 2 will escape them.
358
+
359
+ const safeTags = [];
360
+ // Normalize: array → object for O(1) lookup; object used as-is
361
+ const htmlAllow = Array.isArray(allow_unsafe_html)
362
+ ? Object.fromEntries(allow_unsafe_html.map(t => [t, 1]))
363
+ : (allow_unsafe_html && typeof allow_unsafe_html === 'object') ? allow_unsafe_html : null;
364
+
365
+ if (htmlAllow) {
366
+ // Pass through HTML comments — browsers render them as nothing
367
+ html = html.replace(/<!--[\s\S]*?-->/g, (match) => {
368
+ const idx = safeTags.length;
369
+ safeTags.push(match);
370
+ return `${PLACEHOLDER_HT}${idx}§`;
371
+ });
372
+ html = html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g, (match, tagName) => {
373
+ if (tagName.toLowerCase() in htmlAllow) {
374
+ const sanitized = sanitizeHtmlTagAttrs(match);
375
+ const idx = safeTags.length;
376
+ safeTags.push(sanitized);
377
+ return `${PLACEHOLDER_HT}${idx}§`;
378
+ }
379
+ // Not whitelisted — leave in text for Phase 2 to escape
380
+ return match;
381
+ });
382
+ }
383
+
308
384
  // ────────────────────────────────────────────────────────────────
309
385
  // Phase 2 — HTML Escaping
310
386
  // ────────────────────────────────────────────────────────────────
311
387
  // All remaining text (everything except code placeholders) is escaped
312
388
  // to prevent XSS. The `allow_unsafe_html` option skips this for
313
389
  // trusted pipelines that intentionally embed raw HTML.
390
+ // For whitelist mode, escaping still runs (only `true` bypasses it).
314
391
 
315
- if (!allow_unsafe_html) {
392
+ if (allow_unsafe_html !== true) {
316
393
  html = escapeHtml(html);
317
394
  }
318
395
 
396
+ // Restore safe HTML tag placeholders after escaping
397
+ if (htmlAllow) {
398
+ safeTags.forEach((tag, i) => {
399
+ html = html.replace(`${PLACEHOLDER_HT}${i}§`, tag);
400
+ });
401
+ }
402
+
319
403
  // ────────────────────────────────────────────────────────────────
320
404
  // Phase 3 — Block Scanning + Inline Formatting + Paragraphs
321
405
  // ────────────────────────────────────────────────────────────────
@@ -357,7 +441,6 @@ function quikdown(markdown, options = {}) {
357
441
  // Images (must come before links — ![alt](src) vs [text](url))
358
442
  html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
359
443
  const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
360
- // Bidirectional attributes are only exercised via quikdown_bd bundle.
361
444
  /* istanbul ignore next - bd-only branch */
362
445
  const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
363
446
  /* istanbul ignore next - bd-only branch */
@@ -381,8 +464,12 @@ function quikdown(markdown, options = {}) {
381
464
  return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
382
465
  });
383
466
 
467
+ // Protect rendered tags so emphasis regexes don't see attribute
468
+ // values — fixes #3 (underscores in URLs interpreted as emphasis).
469
+ const savedTags = [];
470
+ html = html.replace(/<[^>]+>/g, m => { savedTags.push(m); return `%%T${savedTags.length - 1}%%`; });
471
+
384
472
  // Bold, italic, strikethrough
385
- // Order matters: ** before * (so ** isn't consumed as two *s)
386
473
  const inlinePatterns = [
387
474
  [/\*\*(.+?)\*\*/g, 'strong', '**'],
388
475
  [/__(.+?)__/g, 'strong', '__'],
@@ -394,6 +481,9 @@ function quikdown(markdown, options = {}) {
394
481
  html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
395
482
  });
396
483
 
484
+ // Restore protected tags
485
+ html = html.replace(/%%T(\d+)%%/g, (_, i) => savedTags[i]);
486
+
397
487
  // ── Step 5: Line breaks + paragraph wrapping ──
398
488
  if (lazy_linefeeds) {
399
489
  // Lazy linefeeds mode: every single \n becomes <br> EXCEPT:
@@ -551,6 +641,14 @@ function scanLineBlocks(text, getAttr, dataQd) {
551
641
  while (i < lines.length) {
552
642
  const line = lines[i];
553
643
 
644
+ // ── Markdown comment (reference-link hack) ──
645
+ // [//]: # (comment) or [//]: # "comment" or [//]: #
646
+ // These produce no output — standard markdown comment convention.
647
+ if (/^\[\/\/\]: #/.test(line)) {
648
+ i++;
649
+ continue;
650
+ }
651
+
554
652
  // ── Heading ──
555
653
  // Count leading '#' characters. Valid heading: 1-6 hashes then a space.
556
654
  // Example: "## Hello World ##" → <h2>Hello World</h2>
@@ -915,6 +1013,7 @@ quikdown.configure = function(options) {
915
1013
  /** Semantic version (injected at build time) */
916
1014
  quikdown.version = quikdownVersion;
917
1015
 
1016
+
918
1017
  // ════════════════════════════════════════════════════════════════════
919
1018
  // Exports
920
1019
  // ════════════════════════════════════════════════════════════════════
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * quikdown - Lightweight Markdown Parser
3
- * @version 1.2.9
3
+ * @version 1.2.11
4
4
  * @license BSD-2-Clause
5
5
  * @copyright DeftIO 2025
6
6
  */
7
- function e(e){if(e.length<3)return!1;for(let t=0;t<e.length;t++){const n=e[t];if("-"!==n){if(" "===n||"\t"===n){for(let n=t+1;n<e.length;n++)if(" "!==e[n]&&"\t"!==e[n])return!1;return t>=3}return!1}}return!0}const t="quikdown-",n="§CB",r={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},o={h1:"font-size:2em;font-weight:600;margin:.67em 0;text-align:left",h2:"font-size:1.5em;font-weight:600;margin:.83em 0",h3:"font-size:1.25em;font-weight:600;margin:1em 0",h4:"font-size:1em;font-weight:600;margin:1.33em 0",h5:"font-size:.875em;font-weight:600;margin:1.67em 0",h6:"font-size:.85em;font-weight:600;margin:2em 0",pre:"background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0",code:"background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace",blockquote:"border-left:4px solid #ddd;margin-left:0;padding-left:1em",table:"border-collapse:collapse;width:100%;margin:1em 0",th:"border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left",td:"border:1px solid #ddd;padding:8px;text-align:left",hr:"border:none;border-top:1px solid #ddd;margin:1em 0",img:"max-width:100%;height:auto",a:"color:#06c;text-decoration:underline",strong:"font-weight:bold",em:"font-style:italic",del:"text-decoration:line-through",ul:"margin:.5em 0;padding-left:2em",ol:"margin:.5em 0;padding-left:2em",li:"margin:.25em 0","task-item":"list-style:none","task-checkbox":"margin-right:.5em"};function l(l,a={}){if(!l||"string"!=typeof l)return"";const{fence_plugin:s,inline_styles:i=!1,bidirectional:p=!1,lazy_linefeeds:d=!1,allow_unsafe_html:g=!1}=a,f=function(e,n){return function(r,o=""){if(e){let e=n[r];return e||o?(o&&o.includes("text-align")&&e&&e.includes("text-align")&&(e=e.replace(/text-align:[^;]+;?/,"").trim(),e&&!e.endsWith(";")&&(e+=";")),` style="${o?e?`${e}${o}`:o:e}"`):""}{const e=` class="${t}${r}"`;return o?`${e} style="${o}"`:e}}}(i,o);function $(e){return e.replace(/[&<>"']/g,e=>r[e])}const u=p?e=>` data-qd="${$(e)}"`:()=>"";function h(e,t=!1){if(!e)return"";if(t)return e;const n=e.trim(),r=n.toLowerCase(),o=["javascript:","vbscript:","data:"];for(const e of o)if(r.startsWith(e))return"data:"===e&&r.startsWith("data:image/")?n:"#";return n}let m=l;const b=[],_=[];m=m.replace(/^(```|~~~)([^\n]*)\n([\s\S]*?)^\1$/gm,(e,t,r,o)=>{const l=`${n}${b.length}§`,a=r?r.trim():"";return s&&s.render&&"function"==typeof s.render?b.push({lang:a,code:o.trimEnd(),custom:!0,fence:t,hasReverse:!!s.reverse}):b.push({lang:a,code:$(o.trimEnd()),custom:!1,fence:t}),l}),m=m.replace(/`([^`]+)`/g,(e,t)=>{const n=`§IC${_.length}§`;return _.push($(t)),n}),g||(m=$(m)),m=function(e,t){const n=e.split("\n"),r=[];let o=!1,l=[];for(let e=0;e<n.length;e++){const a=n[e].trim();if(a.includes("|")&&(a.startsWith("|")||/[^\\|]/.test(a)))o||(o=!0,l=[]),l.push(a);else{if(o){const e=c(l,t);e?r.push(e):r.push(...l),o=!1,l=[]}r.push(n[e])}}if(o&&l.length>0){const e=c(l,t);e?r.push(e):r.push(...l)}return r.join("\n")}(m,f),m=function(t,n,r){const o=t.split("\n"),l=[];let a=0;for(;a<o.length;){const t=o[a];let c=0;for(;c<t.length&&c<7&&"#"===t[c];)c++;if(c>=1&&c<=6&&" "===t[c]){const e=t.slice(c+1).replace(/\s*#+\s*$/,""),o="h"+c;l.push(`<${o}${n(o)}${r("#".repeat(c))}>${e}</${o}>`),a++;continue}e(t)?(l.push(`<hr${n("hr")}>`),a++):/^&gt;\s+/.test(t)?(l.push(`<blockquote${n("blockquote")}>${t.replace(/^&gt;\s+/,"")}</blockquote>`),a++):(l.push(t),a++)}let c=l.join("\n");return c=c.replace(/<\/blockquote>\n<blockquote>/g,"\n"),c}(m,f,u),m=function(e,n,r,o){const l=e.split("\n"),a=[],c=[],s=e=>e.replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[e])),i=o?e=>` data-qd="${s(e)}"`:()=>"";for(let e=0;e<l.length;e++){const o=l[e],s=o.match(/^(\s*)([*\-+]|\d+\.)\s+(.+)$/);if(s){const[,e,o,l]=s,p=Math.floor(e.length/2),d=/^\d+\./.test(o),g=d?"ol":"ul";let f=l,$="";const u=l.match(/^\[([x ])\]\s+(.*)$/i);if(u&&!d){const[,e,n]=u,o="x"===e.toLowerCase();f=`<input type="checkbox"${r?' style="margin-right:.5em"':` class="${t}task-checkbox"`}${o?" checked":""} disabled> ${n}`,$=r?' style="list-style:none"':` class="${t}task-item"`}for(;c.length>p+1;){const e=c.pop();a.push(`</${e.type}>`)}if(c.length===p)c.push({type:g,level:p}),a.push(`<${g}${n(g)}>`);else if(c.length===p+1){const e=c[c.length-1];e.type!==g&&(a.push(`</${e.type}>`),c.pop(),c.push({type:g,level:p}),a.push(`<${g}${n(g)}>`))}const h=$||n("li");a.push(`<li${h}${i(o)}>${f}</li>`)}else{for(;c.length>0;){const e=c.pop();a.push(`</${e.type}>`)}a.push(o)}}for(;c.length>0;){const e=c.pop();a.push(`</${e.type}>`)}return a.join("\n")}(m,f,i,p),m=m.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,(e,t,n)=>{const r=h(n,a.allow_unsafe_urls),o=p&&t?` data-qd-alt="${$(t)}"`:"",l=p?` data-qd-src="${$(n)}"`:"";return`<img${f("img")} src="${r}" alt="${t}"${o}${l}${u("!")}>`}),m=m.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(e,t,n)=>{const r=h(n,a.allow_unsafe_urls),o=/^https?:\/\//i.test(r)?' rel="noopener noreferrer"':"",l=p?` data-qd-text="${$(t)}"`:"";return`<a${f("a")} href="${r}"${o}${l}${u("[")}>${t}</a>`}),m=m.replace(/(^|\s)(https?:\/\/[^\s<]+)/g,(e,t,n)=>{const r=h(n,a.allow_unsafe_urls);return`${t}<a${f("a")} href="${r}" rel="noopener noreferrer">${n}</a>`});if([[/\*\*(.+?)\*\*/g,"strong","**"],[/__(.+?)__/g,"strong","__"],[/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g,"em","*"],[/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g,"em","_"],[/~~(.+?)~~/g,"del","~~"]].forEach(([e,t,n])=>{m=m.replace(e,`<${t}${f(t)}${u(n)}>$1</${t}>`)}),d){const e=[];let t=0;m=m.replace(/<(table|[uo]l)[^>]*>[\s\S]*?<\/\1>/g,n=>(e[t]=n,`§B${t++}§`)),m=m.replace(/\n\n+/g,"§P§").replace(/(<\/(?:h[1-6]|blockquote|pre)>)\n/g,"$1§N§").replace(/(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)\n/g,"$1§N§").replace(/\n(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)/g,"§N§$1").replace(/\n(§B\d+§)/g,"§N§$1").replace(/(§B\d+§)\n/g,"$1§N§").replace(/\n/g,`<br${f("br")}>`).replace(/§N§/g,"\n").replace(/§P§/g,"</p><p>"),e.forEach((e,t)=>m=m.replace(`§B${t}§`,e)),m="<p>"+m+"</p>"}else m=m.replace(/ {2}$/gm,`<br${f("br")}>`),m=m.replace(/\n\n+/g,(e,t)=>m.substring(0,t).match(/<\/(h[1-6]|blockquote|ul|ol|table|pre|hr)>$/)?"<p>":"</p><p>"),m="<p>"+m+"</p>";return[[/<p><\/p>/g,""],[/<p>(<h[1-6][^>]*>)/g,"$1"],[/(<\/h[1-6]>)<\/p>/g,"$1"],[/<p>(<blockquote[^>]*>)/g,"$1"],[/(<\/blockquote>)<\/p>/g,"$1"],[/<p>(<ul[^>]*>|<ol[^>]*>)/g,"$1"],[/(<\/ul>|<\/ol>)<\/p>/g,"$1"],[/<p>(<hr[^>]*>)<\/p>/g,"$1"],[/<p>(<table[^>]*>)/g,"$1"],[/(<\/table>)<\/p>/g,"$1"],[/<p>(<pre[^>]*>)/g,"$1"],[/(<\/pre>)<\/p>/g,"$1"],[new RegExp(`<p>(${n}\\d+§)</p>`,"g"),"$1"]].forEach(([e,t])=>{m=m.replace(e,t)}),m=m.replace(/(<\/(?:h[1-6]|blockquote|ul|ol|table|pre|hr)>)\n([^<])/g,"$1\n<p>$2"),b.forEach((e,t)=>{let r;if(e.custom&&s&&s.render)if(r=s.render(e.code,e.lang),void 0===r){const t=!i&&e.lang?` class="language-${e.lang}"`:"",n=i?f("code"):t,o=p&&e.lang?` data-qd-lang="${$(e.lang)}"`:"",l=p?` data-qd-fence="${$(e.fence)}"`:"";r=`<pre${f("pre")}${l}${o}><code${n}>${$(e.code)}</code></pre>`}else p&&(r=r.replace(/^<(\w+)/,`<$1 data-qd-fence="${$(e.fence)}" data-qd-lang="${$(e.lang)}" data-qd-source="${$(e.code)}"`));else{const t=!i&&e.lang?` class="language-${e.lang}"`:"",n=i?f("code"):t,o=p&&e.lang?` data-qd-lang="${$(e.lang)}"`:"",l=p?` data-qd-fence="${$(e.fence)}"`:"";r=`<pre${f("pre")}${l}${o}><code${n}>${e.code}</code></pre>`}const o=`${n}${t}§`;m=m.replace(o,r)}),_.forEach((e,t)=>{const n=`§IC${t}§`;m=m.replace(n,`<code${f("code")}${u("`")}>${e}</code>`)}),m.trim()}function a(e,t){return[[/\*\*(.+?)\*\*/g,"strong"],[/__(.+?)__/g,"strong"],[/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g,"em"],[/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g,"em"],[/~~(.+?)~~/g,"del"],[/`([^`]+)`/g,"code"]].forEach(([n,r])=>{e=e.replace(n,`<${r}${t(r)}>$1</${r}>`)}),e}function c(e,t){if(e.length<2)return null;let n=-1;for(let t=1;t<e.length;t++)if(/^\|?[\s\-:|]+\|?$/.test(e[t])&&e[t].includes("-")){n=t;break}if(-1===n)return null;const r=e.slice(0,n),o=e.slice(n+1),l=e[n].trim().replace(/^\|/,"").replace(/\|$/,"").split("|").map(e=>{const t=e.trim();return t.startsWith(":")&&t.endsWith(":")?"center":t.endsWith(":")?"right":"left"});let c=`<table${t("table")}>\n`;return c+=`<thead${t("thead")}>\n`,r.forEach(e=>{c+=`<tr${t("tr")}>\n`;e.trim().replace(/^\|/,"").replace(/\|$/,"").split("|").forEach((e,n)=>{const r=l[n]&&"left"!==l[n]?`text-align:${l[n]}`:"",o=a(e.trim(),t);c+=`<th${t("th",r)}>${o}</th>\n`}),c+="</tr>\n"}),c+="</thead>\n",o.length>0&&(c+=`<tbody${t("tbody")}>\n`,o.forEach(e=>{c+=`<tr${t("tr")}>\n`;e.trim().replace(/^\|/,"").replace(/\|$/,"").split("|").forEach((e,n)=>{const r=l[n]&&"left"!==l[n]?`text-align:${l[n]}`:"",o=a(e.trim(),t);c+=`<td${t("td",r)}>${o}</td>\n`}),c+="</tr>\n"}),c+="</tbody>\n"),c+="</table>",c}l.emitStyles=function(e="quikdown-",t="light"){const n=o,r={"#f4f4f4":"#2a2a2a","#f0f0f0":"#2a2a2a","#f2f2f2":"#2a2a2a","#ddd":"#3a3a3a","#06c":"#6db3f2",_textColor:"#e0e0e0"},l={_textColor:"#333"};let a="";for(const[o,c]of Object.entries(n)){let n=c;if("dark"===t&&r){for(const[e,t]of Object.entries(r))e.startsWith("_")||(n=n.replaceAll(e,t));["h1","h2","h3","h4","h5","h6","td","li","blockquote"].includes(o)&&(n+=`;color:${r._textColor}`)}else if("light"===t&&l){["h1","h2","h3","h4","h5","h6","td","li","blockquote"].includes(o)&&(n+=`;color:${l._textColor}`)}a+=`.${e}${o} { ${n} }\n`}return a},l.configure=function(e){return function(t){return l(t,e)}},l.version="1.2.9","undefined"!=typeof module&&module.exports&&(module.exports=l),"undefined"!=typeof window&&(window.quikdown=l);export{l as default};
7
+ function e(e){if(e.length<3)return!1;for(let t=0;t<e.length;t++){const n=e[t];if("-"!==n){if(" "===n||"\t"===n){for(let n=t+1;n<e.length;n++)if(" "!==e[n]&&"\t"!==e[n])return!1;return t>=3}return!1}}return!0}const t="quikdown-",n="§CB",r="§HT",o={href:1,src:1,action:1,formaction:1},l={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},a={h1:"font-size:2em;font-weight:600;margin:.67em 0;text-align:left",h2:"font-size:1.5em;font-weight:600;margin:.83em 0",h3:"font-size:1.25em;font-weight:600;margin:1em 0",h4:"font-size:1em;font-weight:600;margin:1.33em 0",h5:"font-size:.875em;font-weight:600;margin:1.67em 0",h6:"font-size:.85em;font-weight:600;margin:2em 0",pre:"background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0",code:"background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace",blockquote:"border-left:4px solid #ddd;margin-left:0;padding-left:1em",table:"border-collapse:collapse;width:100%;margin:1em 0",th:"border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left",td:"border:1px solid #ddd;padding:8px;text-align:left",hr:"border:none;border-top:1px solid #ddd;margin:1em 0",img:"max-width:100%;height:auto",a:"color:#06c;text-decoration:underline",strong:"font-weight:bold",em:"font-style:italic",del:"text-decoration:line-through",ul:"margin:.5em 0;padding-left:2em",ol:"margin:.5em 0;padding-left:2em",li:"margin:.25em 0","task-item":"list-style:none","task-checkbox":"margin-right:.5em"};function c(c,s={}){if(!c||"string"!=typeof c)return"";const{fence_plugin:p,inline_styles:g=!1,bidirectional:d=!1,lazy_linefeeds:f=!1,allow_unsafe_html:u=!1}=s,$=function(e,n){return function(r,o=""){if(e){let e=n[r];return e||o?(o&&o.includes("text-align")&&e&&e.includes("text-align")&&(e=e.replace(/text-align:[^;]+;?/,"").trim(),e&&!e.endsWith(";")&&(e+=";")),` style="${o?e?`${e}${o}`:o:e}"`):""}{const e=` class="${t}${r}"`;return o?`${e} style="${o}"`:e}}}(g,a);function h(e){return e.replace(/[&<>"']/g,e=>l[e])}const m=d?e=>` data-qd="${h(e)}"`:()=>"";function b(e,t=!1){if(!e)return"";if(t)return e;const n=e.trim(),r=n.toLowerCase(),o=["javascript:","vbscript:","data:"];for(const e of o)if(r.startsWith(e))return"data:"===e&&r.startsWith("data:image/")?n:"#";return n}let _=c;const x=[],q=[];_=_.replace(/^(```|~~~)([^\n]*)\n([\s\S]*?)^\1$/gm,(e,t,r,o)=>{const l=`${n}${x.length}§`,a=r?r.trim():"";return p&&p.render&&"function"==typeof p.render?x.push({lang:a,code:o.trimEnd(),custom:!0,fence:t,hasReverse:!!p.reverse}):x.push({lang:a,code:h(o.trimEnd()),custom:!1,fence:t}),l}),_=_.replace(/`([^`]+)`/g,(e,t)=>{const n=`§IC${q.length}§`;return q.push(h(t)),n});const k=[],w=Array.isArray(u)?Object.fromEntries(u.map(e=>[e,1])):u&&"object"==typeof u?u:null;w&&(_=_.replace(/<!--[\s\S]*?-->/g,e=>{const t=k.length;return k.push(e),`${r}${t}§`}),_=_.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g,(e,t)=>{if(t.toLowerCase()in w){const t=function(e){if(!/\s/.test(e.replace(/<\/?[a-zA-Z][a-zA-Z0-9]*/,"").replace(/\/?>$/,"")))return e;const t=e.match(/^(<\/?[a-zA-Z][a-zA-Z0-9]*)([\s\S]*?)(\/?>)$/);if(!t)return e;const[,n,r,l]=t,a=/([a-zA-Z_][\w\-.:]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g,c=[];let s;for(;null!==(s=a.exec(r));){const e=s[1],t=void 0!==s[2]?s[2]:void 0!==s[3]?s[3]:s[4];if(!/^on/i.test(e))if(void 0===t)c.push(e);else{let n=t;e.toLowerCase()in o&&(n=b(t)),c.push(`${e}="${n}"`)}}return n+(c.length?" "+c.join(" "):"")+l}(e),n=k.length;return k.push(t),`${r}${n}§`}return e})),!0!==u&&(_=h(_)),w&&k.forEach((e,t)=>{_=_.replace(`${r}${t}§`,e)}),_=function(e,t){const n=e.split("\n"),r=[];let o=!1,l=[];for(let e=0;e<n.length;e++){const a=n[e].trim();if(a.includes("|")&&(a.startsWith("|")||/[^\\|]/.test(a)))o||(o=!0,l=[]),l.push(a);else{if(o){const e=i(l,t);e?r.push(e):r.push(...l),o=!1,l=[]}r.push(n[e])}}if(o&&l.length>0){const e=i(l,t);e?r.push(e):r.push(...l)}return r.join("\n")}(_,$),_=function(t,n,r){const o=t.split("\n"),l=[];let a=0;for(;a<o.length;){const t=o[a];if(/^\[\/\/\]: #/.test(t)){a++;continue}let c=0;for(;c<t.length&&c<7&&"#"===t[c];)c++;if(c>=1&&c<=6&&" "===t[c]){const e=t.slice(c+1).replace(/\s*#+\s*$/,""),o="h"+c;l.push(`<${o}${n(o)}${r("#".repeat(c))}>${e}</${o}>`),a++;continue}e(t)?(l.push(`<hr${n("hr")}>`),a++):/^&gt;\s+/.test(t)?(l.push(`<blockquote${n("blockquote")}>${t.replace(/^&gt;\s+/,"")}</blockquote>`),a++):(l.push(t),a++)}let c=l.join("\n");return c=c.replace(/<\/blockquote>\n<blockquote>/g,"\n"),c}(_,$,m),_=function(e,n,r,o){const l=e.split("\n"),a=[],c=[],s=e=>e.replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[e])),i=o?e=>` data-qd="${s(e)}"`:()=>"";for(let e=0;e<l.length;e++){const o=l[e],s=o.match(/^(\s*)([*\-+]|\d+\.)\s+(.+)$/);if(s){const[,e,o,l]=s,p=Math.floor(e.length/2),g=/^\d+\./.test(o),d=g?"ol":"ul";let f=l,u="";const $=l.match(/^\[([x ])\]\s+(.*)$/i);if($&&!g){const[,e,n]=$,o="x"===e.toLowerCase();f=`<input type="checkbox"${r?' style="margin-right:.5em"':` class="${t}task-checkbox"`}${o?" checked":""} disabled> ${n}`,u=r?' style="list-style:none"':` class="${t}task-item"`}for(;c.length>p+1;){const e=c.pop();a.push(`</${e.type}>`)}if(c.length===p)c.push({type:d,level:p}),a.push(`<${d}${n(d)}>`);else if(c.length===p+1){const e=c[c.length-1];e.type!==d&&(a.push(`</${e.type}>`),c.pop(),c.push({type:d,level:p}),a.push(`<${d}${n(d)}>`))}const h=u||n("li");a.push(`<li${h}${i(o)}>${f}</li>`)}else{for(;c.length>0;){const e=c.pop();a.push(`</${e.type}>`)}a.push(o)}}for(;c.length>0;){const e=c.pop();a.push(`</${e.type}>`)}return a.join("\n")}(_,$,g,d),_=_.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,(e,t,n)=>{const r=b(n,s.allow_unsafe_urls),o=d&&t?` data-qd-alt="${h(t)}"`:"",l=d?` data-qd-src="${h(n)}"`:"";return`<img${$("img")} src="${r}" alt="${t}"${o}${l}${m("!")}>`}),_=_.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(e,t,n)=>{const r=b(n,s.allow_unsafe_urls),o=/^https?:\/\//i.test(r)?' rel="noopener noreferrer"':"",l=d?` data-qd-text="${h(t)}"`:"";return`<a${$("a")} href="${r}"${o}${l}${m("[")}>${t}</a>`}),_=_.replace(/(^|\s)(https?:\/\/[^\s<]+)/g,(e,t,n)=>{const r=b(n,s.allow_unsafe_urls);return`${t}<a${$("a")} href="${r}" rel="noopener noreferrer">${n}</a>`});const y=[];_=_.replace(/<[^>]+>/g,e=>(y.push(e),`%%T${y.length-1}%%`));if([[/\*\*(.+?)\*\*/g,"strong","**"],[/__(.+?)__/g,"strong","__"],[/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g,"em","*"],[/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g,"em","_"],[/~~(.+?)~~/g,"del","~~"]].forEach(([e,t,n])=>{_=_.replace(e,`<${t}${$(t)}${m(n)}>$1</${t}>`)}),_=_.replace(/%%T(\d+)%%/g,(e,t)=>y[t]),f){const e=[];let t=0;_=_.replace(/<(table|[uo]l)[^>]*>[\s\S]*?<\/\1>/g,n=>(e[t]=n,`§B${t++}§`)),_=_.replace(/\n\n+/g,"§P§").replace(/(<\/(?:h[1-6]|blockquote|pre)>)\n/g,"$1§N§").replace(/(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)\n/g,"$1§N§").replace(/\n(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)/g,"§N§$1").replace(/\n(§B\d+§)/g,"§N§$1").replace(/(§B\d+§)\n/g,"$1§N§").replace(/\n/g,`<br${$("br")}>`).replace(/§N§/g,"\n").replace(/§P§/g,"</p><p>"),e.forEach((e,t)=>_=_.replace(`§B${t}§`,e)),_="<p>"+_+"</p>"}else _=_.replace(/ {2}$/gm,`<br${$("br")}>`),_=_.replace(/\n\n+/g,(e,t)=>_.substring(0,t).match(/<\/(h[1-6]|blockquote|ul|ol|table|pre|hr)>$/)?"<p>":"</p><p>"),_="<p>"+_+"</p>";return[[/<p><\/p>/g,""],[/<p>(<h[1-6][^>]*>)/g,"$1"],[/(<\/h[1-6]>)<\/p>/g,"$1"],[/<p>(<blockquote[^>]*>)/g,"$1"],[/(<\/blockquote>)<\/p>/g,"$1"],[/<p>(<ul[^>]*>|<ol[^>]*>)/g,"$1"],[/(<\/ul>|<\/ol>)<\/p>/g,"$1"],[/<p>(<hr[^>]*>)<\/p>/g,"$1"],[/<p>(<table[^>]*>)/g,"$1"],[/(<\/table>)<\/p>/g,"$1"],[/<p>(<pre[^>]*>)/g,"$1"],[/(<\/pre>)<\/p>/g,"$1"],[new RegExp(`<p>(${n}\\d+§)</p>`,"g"),"$1"]].forEach(([e,t])=>{_=_.replace(e,t)}),_=_.replace(/(<\/(?:h[1-6]|blockquote|ul|ol|table|pre|hr)>)\n([^<])/g,"$1\n<p>$2"),x.forEach((e,t)=>{let r;if(e.custom&&p&&p.render)if(r=p.render(e.code,e.lang),void 0===r){const t=!g&&e.lang?` class="language-${e.lang}"`:"",n=g?$("code"):t,o=d&&e.lang?` data-qd-lang="${h(e.lang)}"`:"",l=d?` data-qd-fence="${h(e.fence)}"`:"";r=`<pre${$("pre")}${l}${o}><code${n}>${h(e.code)}</code></pre>`}else d&&(r=r.replace(/^<(\w+)/,`<$1 data-qd-fence="${h(e.fence)}" data-qd-lang="${h(e.lang)}" data-qd-source="${h(e.code)}"`));else{const t=!g&&e.lang?` class="language-${e.lang}"`:"",n=g?$("code"):t,o=d&&e.lang?` data-qd-lang="${h(e.lang)}"`:"",l=d?` data-qd-fence="${h(e.fence)}"`:"";r=`<pre${$("pre")}${l}${o}><code${n}>${e.code}</code></pre>`}const o=`${n}${t}§`;_=_.replace(o,r)}),q.forEach((e,t)=>{const n=`§IC${t}§`;_=_.replace(n,`<code${$("code")}${m("`")}>${e}</code>`)}),_.trim()}function s(e,t){return[[/\*\*(.+?)\*\*/g,"strong"],[/__(.+?)__/g,"strong"],[/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g,"em"],[/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g,"em"],[/~~(.+?)~~/g,"del"],[/`([^`]+)`/g,"code"]].forEach(([n,r])=>{e=e.replace(n,`<${r}${t(r)}>$1</${r}>`)}),e}function i(e,t){if(e.length<2)return null;let n=-1;for(let t=1;t<e.length;t++)if(/^\|?[\s\-:|]+\|?$/.test(e[t])&&e[t].includes("-")){n=t;break}if(-1===n)return null;const r=e.slice(0,n),o=e.slice(n+1),l=e[n].trim().replace(/^\|/,"").replace(/\|$/,"").split("|").map(e=>{const t=e.trim();return t.startsWith(":")&&t.endsWith(":")?"center":t.endsWith(":")?"right":"left"});let a=`<table${t("table")}>\n`;return a+=`<thead${t("thead")}>\n`,r.forEach(e=>{a+=`<tr${t("tr")}>\n`;e.trim().replace(/^\|/,"").replace(/\|$/,"").split("|").forEach((e,n)=>{const r=l[n]&&"left"!==l[n]?`text-align:${l[n]}`:"",o=s(e.trim(),t);a+=`<th${t("th",r)}>${o}</th>\n`}),a+="</tr>\n"}),a+="</thead>\n",o.length>0&&(a+=`<tbody${t("tbody")}>\n`,o.forEach(e=>{a+=`<tr${t("tr")}>\n`;e.trim().replace(/^\|/,"").replace(/\|$/,"").split("|").forEach((e,n)=>{const r=l[n]&&"left"!==l[n]?`text-align:${l[n]}`:"",o=s(e.trim(),t);a+=`<td${t("td",r)}>${o}</td>\n`}),a+="</tr>\n"}),a+="</tbody>\n"),a+="</table>",a}c.emitStyles=function(e="quikdown-",t="light"){const n=a,r={"#f4f4f4":"#2a2a2a","#f0f0f0":"#2a2a2a","#f2f2f2":"#2a2a2a","#ddd":"#3a3a3a","#06c":"#6db3f2",_textColor:"#e0e0e0"},o={_textColor:"#333"};let l="";for(const[a,c]of Object.entries(n)){let n=c;if("dark"===t&&r){for(const[e,t]of Object.entries(r))e.startsWith("_")||(n=n.replaceAll(e,t));["h1","h2","h3","h4","h5","h6","td","li","blockquote"].includes(a)&&(n+=`;color:${r._textColor}`)}else if("light"===t&&o){["h1","h2","h3","h4","h5","h6","td","li","blockquote"].includes(a)&&(n+=`;color:${o._textColor}`)}l+=`.${e}${a} { ${n} }\n`}return l},c.configure=function(e){return function(t){return c(t,e)}},c.version="1.2.11","undefined"!=typeof module&&module.exports&&(module.exports=c),"undefined"!=typeof window&&(window.quikdown=c);export{c as default};
8
8
  //# sourceMappingURL=quikdown.esm.min.js.map
Binary file