quikdown 1.2.9 → 1.2.10

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 (67) hide show
  1. package/README.md +5 -5
  2. package/dist/quikdown.cjs +10 -4
  3. package/dist/quikdown.dark.css +1 -1
  4. package/dist/quikdown.esm.js +10 -4
  5. package/dist/quikdown.esm.min.js +2 -2
  6. package/dist/quikdown.esm.min.js.gz +0 -0
  7. package/dist/quikdown.esm.min.js.map +1 -1
  8. package/dist/quikdown.light.css +1 -1
  9. package/dist/quikdown.umd.js +10 -4
  10. package/dist/quikdown.umd.min.js +2 -2
  11. package/dist/quikdown.umd.min.js.gz +0 -0
  12. package/dist/quikdown.umd.min.js.map +1 -1
  13. package/dist/quikdown_ast.cjs +16 -28
  14. package/dist/quikdown_ast.esm.js +16 -28
  15. package/dist/quikdown_ast.esm.min.js +2 -2
  16. package/dist/quikdown_ast.esm.min.js.gz +0 -0
  17. package/dist/quikdown_ast.esm.min.js.map +1 -1
  18. package/dist/quikdown_ast.umd.js +16 -28
  19. package/dist/quikdown_ast.umd.min.js +2 -2
  20. package/dist/quikdown_ast.umd.min.js.gz +0 -0
  21. package/dist/quikdown_ast.umd.min.js.map +1 -1
  22. package/dist/quikdown_ast_html.cjs +17 -29
  23. package/dist/quikdown_ast_html.esm.js +17 -29
  24. package/dist/quikdown_ast_html.esm.min.js +2 -2
  25. package/dist/quikdown_ast_html.esm.min.js.gz +0 -0
  26. package/dist/quikdown_ast_html.esm.min.js.map +1 -1
  27. package/dist/quikdown_ast_html.umd.js +17 -29
  28. package/dist/quikdown_ast_html.umd.min.js +2 -2
  29. package/dist/quikdown_ast_html.umd.min.js.gz +0 -0
  30. package/dist/quikdown_ast_html.umd.min.js.map +1 -1
  31. package/dist/quikdown_bd.cjs +10 -4
  32. package/dist/quikdown_bd.esm.js +10 -4
  33. package/dist/quikdown_bd.esm.min.js +2 -2
  34. package/dist/quikdown_bd.esm.min.js.gz +0 -0
  35. package/dist/quikdown_bd.esm.min.js.map +1 -1
  36. package/dist/quikdown_bd.umd.js +10 -4
  37. package/dist/quikdown_bd.umd.min.js +2 -2
  38. package/dist/quikdown_bd.umd.min.js.gz +0 -0
  39. package/dist/quikdown_bd.umd.min.js.map +1 -1
  40. package/dist/quikdown_edit.cjs +14 -8
  41. package/dist/quikdown_edit.esm.js +14 -8
  42. package/dist/quikdown_edit.esm.min.js +3 -3
  43. package/dist/quikdown_edit.esm.min.js.gz +0 -0
  44. package/dist/quikdown_edit.esm.min.js.map +1 -1
  45. package/dist/quikdown_edit.umd.js +14 -8
  46. package/dist/quikdown_edit.umd.min.js +3 -3
  47. package/dist/quikdown_edit.umd.min.js.gz +0 -0
  48. package/dist/quikdown_edit.umd.min.js.map +1 -1
  49. package/dist/quikdown_json.cjs +17 -29
  50. package/dist/quikdown_json.esm.js +17 -29
  51. package/dist/quikdown_json.esm.min.js +2 -2
  52. package/dist/quikdown_json.esm.min.js.gz +0 -0
  53. package/dist/quikdown_json.esm.min.js.map +1 -1
  54. package/dist/quikdown_json.umd.js +17 -29
  55. package/dist/quikdown_json.umd.min.js +2 -2
  56. package/dist/quikdown_json.umd.min.js.gz +0 -0
  57. package/dist/quikdown_json.umd.min.js.map +1 -1
  58. package/dist/quikdown_yaml.cjs +17 -29
  59. package/dist/quikdown_yaml.esm.js +17 -29
  60. package/dist/quikdown_yaml.esm.min.js +2 -2
  61. package/dist/quikdown_yaml.esm.min.js.gz +0 -0
  62. package/dist/quikdown_yaml.esm.min.js.map +1 -1
  63. package/dist/quikdown_yaml.umd.js +17 -29
  64. package/dist/quikdown_yaml.umd.min.js +2 -2
  65. package/dist/quikdown_yaml.umd.min.js.gz +0 -0
  66. package/dist/quikdown_yaml.umd.min.js.map +1 -1
  67. package/package.json +9 -9
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-95.8%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-9.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.10
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.10';
118
118
 
119
119
  /** CSS class prefix used for all generated elements */
120
120
  const CLASS_PREFIX = 'quikdown-';
@@ -359,7 +359,6 @@ function quikdown(markdown, options = {}) {
359
359
  // Images (must come before links — ![alt](src) vs [text](url))
360
360
  html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
361
361
  const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
362
- // Bidirectional attributes are only exercised via quikdown_bd bundle.
363
362
  /* istanbul ignore next - bd-only branch */
364
363
  const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
365
364
  /* istanbul ignore next - bd-only branch */
@@ -383,8 +382,12 @@ function quikdown(markdown, options = {}) {
383
382
  return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
384
383
  });
385
384
 
385
+ // Protect rendered tags so emphasis regexes don't see attribute
386
+ // values — fixes #3 (underscores in URLs interpreted as emphasis).
387
+ const savedTags = [];
388
+ html = html.replace(/<[^>]+>/g, m => { savedTags.push(m); return `%%T${savedTags.length - 1}%%`; });
389
+
386
390
  // Bold, italic, strikethrough
387
- // Order matters: ** before * (so ** isn't consumed as two *s)
388
391
  const inlinePatterns = [
389
392
  [/\*\*(.+?)\*\*/g, 'strong', '**'],
390
393
  [/__(.+?)__/g, 'strong', '__'],
@@ -396,6 +399,9 @@ function quikdown(markdown, options = {}) {
396
399
  html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
397
400
  });
398
401
 
402
+ // Restore protected tags
403
+ html = html.replace(/%%T(\d+)%%/g, (_, i) => savedTags[i]);
404
+
399
405
  // ── Step 5: Line breaks + paragraph wrapping ──
400
406
  if (lazy_linefeeds) {
401
407
  // Lazy linefeeds mode: every single \n becomes <br> EXCEPT:
@@ -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.10
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.10
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.10';
116
116
 
117
117
  /** CSS class prefix used for all generated elements */
118
118
  const CLASS_PREFIX = 'quikdown-';
@@ -357,7 +357,6 @@ function quikdown(markdown, options = {}) {
357
357
  // Images (must come before links — ![alt](src) vs [text](url))
358
358
  html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
359
359
  const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
360
- // Bidirectional attributes are only exercised via quikdown_bd bundle.
361
360
  /* istanbul ignore next - bd-only branch */
362
361
  const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
363
362
  /* istanbul ignore next - bd-only branch */
@@ -381,8 +380,12 @@ function quikdown(markdown, options = {}) {
381
380
  return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
382
381
  });
383
382
 
383
+ // Protect rendered tags so emphasis regexes don't see attribute
384
+ // values — fixes #3 (underscores in URLs interpreted as emphasis).
385
+ const savedTags = [];
386
+ html = html.replace(/<[^>]+>/g, m => { savedTags.push(m); return `%%T${savedTags.length - 1}%%`; });
387
+
384
388
  // Bold, italic, strikethrough
385
- // Order matters: ** before * (so ** isn't consumed as two *s)
386
389
  const inlinePatterns = [
387
390
  [/\*\*(.+?)\*\*/g, 'strong', '**'],
388
391
  [/__(.+?)__/g, 'strong', '__'],
@@ -394,6 +397,9 @@ function quikdown(markdown, options = {}) {
394
397
  html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
395
398
  });
396
399
 
400
+ // Restore protected tags
401
+ html = html.replace(/%%T(\d+)%%/g, (_, i) => savedTags[i]);
402
+
397
403
  // ── Step 5: Line breaks + paragraph wrapping ──
398
404
  if (lazy_linefeeds) {
399
405
  // Lazy linefeeds mode: every single \n becomes <br> EXCEPT:
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * quikdown - Lightweight Markdown Parser
3
- * @version 1.2.9
3
+ * @version 1.2.10
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={"&":"&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>`});const x=[];m=m.replace(/<[^>]+>/g,e=>(x.push(e),`%%T${x.length-1}%%`));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}>`)}),m=m.replace(/%%T(\d+)%%/g,(e,t)=>x[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.10","undefined"!=typeof module&&module.exports&&(module.exports=l),"undefined"!=typeof window&&(window.quikdown=l);export{l as default};
8
8
  //# sourceMappingURL=quikdown.esm.min.js.map
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"quikdown.esm.min.js","sources":["../src/quikdown_classify.js","../src/quikdown.js"],"sourcesContent":["/**\n * quikdown_classify — Shared line-classification utilities\n * ═════════════════════════════════════════════════════════\n *\n * Pure functions for classifying markdown lines. Used by both the main\n * parser (quikdown.js) and the editor (quikdown_edit.js) so the logic\n * lives in one place.\n *\n * All functions operate on a **trimmed** line (caller must trim).\n * None use regexes with nested quantifiers — every check is either a\n * simple regex or a linear scan, so there is zero ReDoS risk.\n */\n\n/**\n * Full CommonMark HR check: three or more identical characters from\n * {-, *, _} with optional interspersed whitespace.\n *\n * Examples that return true: ---, ***, ___, ----, - - -, * * *, _ _ _\n * Examples that return false: --, - text, ---text, mixed -_*, empty\n *\n * Algorithm (O(n), single pass, no backtracking):\n * 1. Strip all whitespace\n * 2. Verify length >= 3\n * 3. First char must be -, *, or _\n * 4. Every remaining char must equal the first\n *\n * @param {string} trimmed The line, already trimmed\n * @returns {boolean}\n */\nexport function isHRLine(trimmed) {\n if (trimmed.length < 3) return false;\n\n // Strip whitespace via linear scan\n let stripped = '';\n for (let i = 0; i < trimmed.length; i++) {\n const ch = trimmed[i];\n if (ch !== ' ' && ch !== '\\t') stripped += ch;\n }\n\n if (stripped.length < 3) return false;\n\n const ch = stripped[0];\n if (ch !== '-' && ch !== '*' && ch !== '_') return false;\n\n for (let i = 1; i < stripped.length; i++) {\n if (stripped[i] !== ch) return false;\n }\n return true;\n}\n\n/**\n * Dash-only HR check — exact parity with the main parser's original\n * regex `/^---+\\s*$/`. Only matches lines of three or more dashes\n * with optional trailing whitespace (no interspersed spaces).\n *\n * @param {string} trimmed The line, already trimmed\n * @returns {boolean}\n */\nexport function isDashHRLine(trimmed) {\n if (trimmed.length < 3) return false;\n for (let i = 0; i < trimmed.length; i++) {\n const ch = trimmed[i];\n if (ch === '-') continue;\n // Allow trailing whitespace only\n if (ch === ' ' || ch === '\\t') {\n for (let j = i + 1; j < trimmed.length; j++) {\n if (trimmed[j] !== ' ' && trimmed[j] !== '\\t') return false;\n }\n return i >= 3; // at least 3 dashes before whitespace\n }\n return false;\n }\n return true; // all dashes\n}\n\n/**\n * Check if a trimmed line opens a code fence.\n * Returns { char, len, lang } if it does, or null otherwise.\n *\n * A fence opener is 3+ identical backticks or tildes at the start of a line,\n * optionally followed by a language tag.\n *\n * @param {string} trimmed The line, already trimmed\n * @returns {{ char: string, len: number, lang: string } | null}\n */\nexport function fenceOpen(trimmed) {\n if (trimmed.length < 3) return null;\n const ch = trimmed[0];\n if (ch !== '`' && ch !== '~') return null;\n\n let len = 1;\n while (len < trimmed.length && trimmed[len] === ch) len++;\n if (len < 3) return null;\n\n const lang = trimmed.slice(len).trim();\n return { char: ch, len, lang };\n}\n\n/**\n * Check if a trimmed line closes an open fence.\n * The closing fence must use the same character, be at least as long,\n * and have no content after (optional trailing whitespace only).\n *\n * @param {string} trimmed The line, already trimmed\n * @param {string} openChar The fence character ('`' or '~')\n * @param {number} openLen Length of the opening fence marker\n * @returns {boolean}\n */\nexport function isFenceClose(trimmed, openChar, openLen) {\n if (trimmed.length < openLen) return false;\n\n let len = 0;\n while (len < trimmed.length && trimmed[len] === openChar) len++;\n if (len < openLen) return false;\n\n // Rest must be whitespace only\n for (let i = len; i < trimmed.length; i++) {\n if (trimmed[i] !== ' ' && trimmed[i] !== '\\t') return false;\n }\n return true;\n}\n\n/**\n * Classify a content line into a category string.\n * Order matters: HR before list-ul (since `- - -` looks like a list start).\n *\n * @param {string} trimmed The line, already trimmed\n * @returns {string} One of: 'heading', 'hr', 'list-ol', 'list-ul',\n * 'blockquote', 'table', 'paragraph'\n */\nexport function classifyLine(trimmed) {\n if (/^#{1,6}\\s/.test(trimmed)) return 'heading';\n if (isHRLine(trimmed)) return 'hr';\n if (/^\\d+\\.\\s/.test(trimmed)) return 'list-ol';\n if (/^[-*+]\\s/.test(trimmed)) return 'list-ul';\n if (/^>/.test(trimmed)) return 'blockquote';\n if (/^\\|/.test(trimmed)) return 'table';\n return 'paragraph';\n}\n\n/**\n * Heuristic: does a line look like a markdown table row?\n * @param {string} line The line (trimmed or untrimmed)\n * @returns {boolean}\n */\nexport function looksLikeTableRow(line) {\n return line.includes('|');\n}\n","/**\n * quikdown — A compact, scanner-based markdown parser\n * ════════════════════════════════════════════════════\n *\n * Architecture overview (v1.2.8 — lexer rewrite)\n * ───────────────────────────────────────────────\n * Prior to v1.2.8, quikdown used a multi-pass regex pipeline: each block\n * type (headings, blockquotes, HR, lists, tables) and each inline format\n * (bold, italic, links, …) was handled by its own global regex applied\n * sequentially to the full document string. That worked but made the code\n * hard to extend and debug — a new construct meant adding another regex\n * pass, and ordering bugs between passes were subtle.\n *\n * Starting in v1.2.8 the parser uses a **line-scanning** approach for\n * block detection and a **per-block inline pass** for formatting:\n *\n * ┌─────────────────────────────────────────────────────────┐\n * │ Phase 1 — Code Extraction │\n * │ Scan for fenced code blocks (``` / ~~~) and inline │\n * │ code spans (`…`). Replace with §CB§ / §IC§ place- │\n * │ holders so code content is never touched by later │\n * │ phases. │\n * ├─────────────────────────────────────────────────────────┤\n * │ Phase 2 — HTML Escaping │\n * │ Escape &, <, >, \", ' in the remaining text to prevent │\n * │ XSS. (Skipped when allow_unsafe_html is true.) │\n * ├─────────────────────────────────────────────────────────┤\n * │ Phase 3 — Block Scanning │\n * │ Walk the text **line by line**. At each line, the │\n * │ scanner checks (in order): │\n * │ • table rows (|) │\n * │ • headings (#) │\n * │ • HR (---) │\n * │ • blockquotes (&gt;) │\n * │ • list items (-, *, +, 1.) │\n * │ • code-block placeholder (§CB…§) │\n * │ • paragraph text (everything else) │\n * │ │\n * │ Block text is run through the **inline formatter** │\n * │ which handles bold, italic, strikethrough, links, │\n * │ images, and autolinks. │\n * │ │\n * │ Paragraphs are wrapped in <p> tags. Lazy linefeeds │\n * │ (single \\n → <br>) are handled here too. │\n * ├─────────────────────────────────────────────────────────┤\n * │ Phase 4 — Code Restoration │\n * │ Replace §CB§ / §IC§ placeholders with rendered <pre> │\n * │ / <code> HTML, applying the fence_plugin if present. │\n * └─────────────────────────────────────────────────────────┘\n *\n * Why this design?\n * • Single pass over lines for block identification — no re-scanning.\n * • Each block type is a clearly separated branch, easy to add new ones.\n * • Inline formatting is confined to block text — can't accidentally\n * match across block boundaries or inside HTML tags.\n * • Code extraction still uses a simple regex (it's one pattern, not a\n * chain) because the §-placeholder approach is proven and simple.\n *\n * @param {string} markdown The markdown source text\n * @param {Object} options Configuration (see below)\n * @returns {string} Rendered HTML\n */\n\nimport { isDashHRLine } from './quikdown_classify.js';\n\n// ────────────────────────────────────────────────────────────────────\n// Constants\n// ────────────────────────────────────────────────────────────────────\n\n/** Build-time version stamp (injected by tools/updateVersion) */\nconst quikdownVersion = '__QUIKDOWN_VERSION__';\n\n/** CSS class prefix used for all generated elements */\nconst CLASS_PREFIX = 'quikdown-';\n\n/** Placeholder sigils — chosen to be extremely unlikely in real text */\nconst PLACEHOLDER_CB = '§CB'; // fenced code blocks\nconst PLACEHOLDER_IC = '§IC'; // inline code spans\n\n/** HTML entity escape map */\nconst ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'};\n\n// ────────────────────────────────────────────────────────────────────\n// Style definitions\n// ────────────────────────────────────────────────────────────────────\n\n/**\n * Inline styles for every element quikdown can emit.\n * When `inline_styles: true` these are injected as style=\"…\" attributes.\n * When `inline_styles: false` (default) we use class=\"quikdown-<tag>\"\n * and these same values are emitted by `quikdown.emitStyles()`.\n */\nconst QUIKDOWN_STYLES = {\n h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',\n h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',\n h3: 'font-size:1.25em;font-weight:600;margin:1em 0',\n h4: 'font-size:1em;font-weight:600;margin:1.33em 0',\n h5: 'font-size:.875em;font-weight:600;margin:1.67em 0',\n h6: 'font-size:.85em;font-weight:600;margin:2em 0',\n pre: 'background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0',\n code: 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace',\n blockquote: 'border-left:4px solid #ddd;margin-left:0;padding-left:1em',\n table: 'border-collapse:collapse;width:100%;margin:1em 0',\n th: 'border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left',\n td: 'border:1px solid #ddd;padding:8px;text-align:left',\n hr: 'border:none;border-top:1px solid #ddd;margin:1em 0',\n img: 'max-width:100%;height:auto',\n a: 'color:#06c;text-decoration:underline',\n strong: 'font-weight:bold',\n em: 'font-style:italic',\n del: 'text-decoration:line-through',\n ul: 'margin:.5em 0;padding-left:2em',\n ol: 'margin:.5em 0;padding-left:2em',\n li: 'margin:.25em 0',\n 'task-item': 'list-style:none',\n 'task-checkbox': 'margin-right:.5em'\n};\n\n// ────────────────────────────────────────────────────────────────────\n// Attribute factory\n// ────────────────────────────────────────────────────────────────────\n\n/**\n * Creates a `getAttr(tag, additionalStyle?)` helper that returns\n * either a class=\"…\" or style=\"…\" attribute string depending on mode.\n *\n * @param {boolean} inline_styles True → emit style=\"…\"; false → class=\"…\"\n * @param {Object} styles The QUIKDOWN_STYLES map\n * @returns {Function}\n */\nfunction createGetAttr(inline_styles, styles) {\n return function(tag, additionalStyle = '') {\n if (inline_styles) {\n let style = styles[tag];\n if (!style && !additionalStyle) return '';\n\n // When adding alignment that conflicts with the tag's default,\n // strip the default text-align first.\n if (additionalStyle && additionalStyle.includes('text-align') && style && style.includes('text-align')) {\n style = style.replace(/text-align:[^;]+;?/, '').trim();\n /* istanbul ignore next */\n if (style && !style.endsWith(';')) style += ';';\n }\n\n /* istanbul ignore next - defensive: additionalStyle without style doesn't occur with current tags */\n const fullStyle = additionalStyle ? (style ? `${style}${additionalStyle}` : additionalStyle) : style;\n return ` style=\"${fullStyle}\"`;\n } else {\n const classAttr = ` class=\"${CLASS_PREFIX}${tag}\"`;\n if (additionalStyle) {\n return `${classAttr} style=\"${additionalStyle}\"`;\n }\n return classAttr;\n }\n };\n}\n\n// ════════════════════════════════════════════════════════════════════\n// Main parser function\n// ════════════════════════════════════════════════════════════════════\n\nfunction quikdown(markdown, options = {}) {\n // ── Guard: only process non-empty strings ──\n if (!markdown || typeof markdown !== 'string') {\n return '';\n }\n\n // ── Unpack options ──\n const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false, allow_unsafe_html = false } = options;\n const styles = QUIKDOWN_STYLES;\n const getAttr = createGetAttr(inline_styles, styles);\n\n // ── Helpers (closed over options) ──\n\n /** Escape the five HTML-special characters. */\n function escapeHtml(text) {\n return text.replace(/[&<>\"']/g, m => ESC_MAP[m]);\n }\n\n /**\n * Bidirectional marker helper.\n * When bidirectional mode is on, returns ` data-qd=\"…\"`.\n * The non-bidirectional branch is a trivial no-op arrow; it is\n * exercised in the core bundle but never in quikdown_bd.\n */\n /* istanbul ignore next - trivial no-op fallback */\n const dataQd = bidirectional ? (marker) => ` data-qd=\"${escapeHtml(marker)}\"` : () => '';\n\n /**\n * Sanitize a URL to block javascript:, vbscript:, and non-image data: URIs.\n * Returns '#' for blocked URLs.\n */\n function sanitizeUrl(url, allowUnsafe = false) {\n /* istanbul ignore next - defensive programming, regex ensures url is never empty */\n if (!url) return '';\n if (allowUnsafe) return url;\n\n const trimmedUrl = url.trim();\n const lowerUrl = trimmedUrl.toLowerCase();\n const dangerousProtocols = ['javascript:', 'vbscript:', 'data:'];\n\n for (const protocol of dangerousProtocols) {\n if (lowerUrl.startsWith(protocol)) {\n if (protocol === 'data:' && lowerUrl.startsWith('data:image/')) {\n return trimmedUrl;\n }\n return '#';\n }\n }\n return trimmedUrl;\n }\n\n // ────────────────────────────────────────────────────────────────\n // Phase 1 — Code Extraction\n // ────────────────────────────────────────────────────────────────\n // Why extract code first? Fenced blocks and inline code spans can\n // contain markdown-like characters (*, _, #, |, etc.) that must NOT\n // be interpreted as formatting. By pulling them out and replacing\n // with unique placeholders, the rest of the pipeline never sees them.\n\n let html = markdown;\n const codeBlocks = []; // Array of {lang, code, custom, fence, hasReverse}\n const inlineCodes = []; // Array of escaped-HTML strings\n\n // ── Fenced code blocks ──\n // Matches paired fences: ``` with ``` and ~~~ with ~~~.\n // The fence must start at column 0 of a line (^ with /m flag).\n // Group 1 = fence marker, Group 2 = language hint, Group 3 = code body.\n html = html.replace(/^(```|~~~)([^\\n]*)\\n([\\s\\S]*?)^\\1$/gm, (match, fence, lang, code) => {\n const placeholder = `${PLACEHOLDER_CB}${codeBlocks.length}§`;\n const langTrimmed = lang ? lang.trim() : '';\n\n if (fence_plugin && fence_plugin.render && typeof fence_plugin.render === 'function') {\n // Custom plugin — store raw code (un-escaped) so the plugin\n // receives the original source.\n codeBlocks.push({\n lang: langTrimmed,\n code: code.trimEnd(),\n custom: true,\n fence: fence,\n hasReverse: !!fence_plugin.reverse\n });\n } else {\n // Default — pre-escape the code for safe HTML output.\n codeBlocks.push({\n lang: langTrimmed,\n code: escapeHtml(code.trimEnd()),\n custom: false,\n fence: fence\n });\n }\n return placeholder;\n });\n\n // ── Inline code spans ──\n // Matches a single backtick pair: `content`.\n // Content is captured and HTML-escaped immediately.\n html = html.replace(/`([^`]+)`/g, (match, code) => {\n const placeholder = `${PLACEHOLDER_IC}${inlineCodes.length}§`;\n inlineCodes.push(escapeHtml(code));\n return placeholder;\n });\n\n // ────────────────────────────────────────────────────────────────\n // Phase 2 — HTML Escaping\n // ────────────────────────────────────────────────────────────────\n // All remaining text (everything except code placeholders) is escaped\n // to prevent XSS. The `allow_unsafe_html` option skips this for\n // trusted pipelines that intentionally embed raw HTML.\n\n if (!allow_unsafe_html) {\n html = escapeHtml(html);\n }\n\n // ────────────────────────────────────────────────────────────────\n // Phase 3 — Block Scanning + Inline Formatting + Paragraphs\n // ────────────────────────────────────────────────────────────────\n // This is the heart of the lexer rewrite. Instead of applying\n // 10+ global regex passes, we:\n // 1. Process tables (line walker — tables need multi-line lookahead)\n // 2. Scan remaining lines for headings, HR, blockquotes\n // 3. Process lists (line walker — lists need indent tracking)\n // 4. Apply inline formatting to all text content\n // 5. Wrap remaining text in <p> tags\n //\n // Steps 1 and 3 are line-walkers that process the full text in a\n // single pass each. Step 2 replaces global regex with a per-line\n // scanner. Steps 4-5 are applied to the result.\n //\n // Total: 3 structured passes instead of 10+ regex passes.\n\n // ── Step 1: Tables ──\n // Tables need multi-line lookahead (header → separator → body rows)\n // so they're handled by a dedicated line-walker first.\n html = processTable(html, getAttr);\n\n // ── Step 2: Headings, HR, Blockquotes ──\n // These are simple line-level constructs. We scan each line once\n // and replace matching lines with their HTML representation.\n html = scanLineBlocks(html, getAttr, dataQd);\n\n // ── Step 3: Lists ──\n // Lists need indent-level tracking across lines, so they get their\n // own line-walker.\n html = processLists(html, getAttr, inline_styles, bidirectional);\n\n // ── Step 4: Inline formatting ──\n // Apply bold, italic, strikethrough, images, links, and autolinks\n // to all text content. This runs on the output of steps 1-3, so\n // it sees text inside headings, blockquotes, table cells, list\n // items, and paragraph text.\n\n // Images (must come before links — ![alt](src) vs [text](url))\n html = html.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, (match, alt, src) => {\n const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);\n // Bidirectional attributes are only exercised via quikdown_bd bundle.\n /* istanbul ignore next - bd-only branch */\n const altAttr = bidirectional && alt ? ` data-qd-alt=\"${escapeHtml(alt)}\"` : '';\n /* istanbul ignore next - bd-only branch */\n const srcAttr = bidirectional ? ` data-qd-src=\"${escapeHtml(src)}\"` : '';\n return `<img${getAttr('img')} src=\"${sanitizedSrc}\" alt=\"${alt}\"${altAttr}${srcAttr}${dataQd('!')}>`;\n });\n\n // Links\n html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (match, text, href) => {\n const sanitizedHref = sanitizeUrl(href, options.allow_unsafe_urls);\n const isExternal = /^https?:\\/\\//i.test(sanitizedHref);\n const rel = isExternal ? ' rel=\"noopener noreferrer\"' : '';\n /* istanbul ignore next - bd-only branch */\n const textAttr = bidirectional ? ` data-qd-text=\"${escapeHtml(text)}\"` : '';\n return `<a${getAttr('a')} href=\"${sanitizedHref}\"${rel}${textAttr}${dataQd('[')}>${text}</a>`;\n });\n\n // Autolinks — bare https?:// URLs become clickable <a> tags\n html = html.replace(/(^|\\s)(https?:\\/\\/[^\\s<]+)/g, (match, prefix, url) => {\n const sanitizedUrl = sanitizeUrl(url, options.allow_unsafe_urls);\n return `${prefix}<a${getAttr('a')} href=\"${sanitizedUrl}\" rel=\"noopener noreferrer\">${url}</a>`;\n });\n\n // Bold, italic, strikethrough\n // Order matters: ** before * (so ** isn't consumed as two *s)\n const inlinePatterns = [\n [/\\*\\*(.+?)\\*\\*/g, 'strong', '**'],\n [/__(.+?)__/g, 'strong', '__'],\n [/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, 'em', '*'],\n [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em', '_'],\n [/~~(.+?)~~/g, 'del', '~~']\n ];\n inlinePatterns.forEach(([pattern, tag, marker]) => {\n html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);\n });\n\n // ── Step 5: Line breaks + paragraph wrapping ──\n if (lazy_linefeeds) {\n // Lazy linefeeds mode: every single \\n becomes <br> EXCEPT:\n // • Double newlines → paragraph break\n // • Newlines adjacent to block elements (h, blockquote, pre, hr, table, list)\n //\n // Strategy: protect block-adjacent newlines with §N§, convert\n // the rest, then restore.\n\n const blocks = [];\n let bi = 0;\n\n // Protect tables and lists from <br> injection\n html = html.replace(/<(table|[uo]l)[^>]*>[\\s\\S]*?<\\/\\1>/g, m => {\n blocks[bi] = m;\n return `§B${bi++}§`;\n });\n\n html = html.replace(/\\n\\n+/g, '§P§')\n // After block-level closing tags\n .replace(/(<\\/(?:h[1-6]|blockquote|pre)>)\\n/g, '$1§N§')\n .replace(/(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)\\n/g, '$1§N§')\n // Before block-level opening tags\n .replace(/\\n(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)/g, '§N§$1')\n .replace(/\\n(§B\\d+§)/g, '§N§$1')\n .replace(/(§B\\d+§)\\n/g, '$1§N§')\n // Convert surviving newlines to <br>\n .replace(/\\n/g, `<br${getAttr('br')}>`)\n // Restore\n .replace(/§N§/g, '\\n')\n .replace(/§P§/g, '</p><p>');\n\n // Restore protected blocks\n blocks.forEach((b, i) => html = html.replace(`§B${i}§`, b));\n\n html = '<p>' + html + '</p>';\n } else {\n // Standard mode: two trailing spaces → <br>, double newline → new paragraph\n html = html.replace(/ {2}$/gm, `<br${getAttr('br')}>`);\n\n html = html.replace(/\\n\\n+/g, (match, offset) => {\n const before = html.substring(0, offset);\n if (before.match(/<\\/(h[1-6]|blockquote|ul|ol|table|pre|hr)>$/)) {\n return '<p>';\n }\n return '</p><p>';\n });\n html = '<p>' + html + '</p>';\n }\n\n // ── Step 6: Cleanup ──\n // Remove <p> wrappers that accidentally enclose block elements.\n // This is simpler than trying to prevent them during wrapping.\n const cleanupPatterns = [\n [/<p><\\/p>/g, ''],\n [/<p>(<h[1-6][^>]*>)/g, '$1'],\n [/(<\\/h[1-6]>)<\\/p>/g, '$1'],\n [/<p>(<blockquote[^>]*>)/g, '$1'],\n [/(<\\/blockquote>)<\\/p>/g, '$1'],\n [/<p>(<ul[^>]*>|<ol[^>]*>)/g, '$1'],\n [/(<\\/ul>|<\\/ol>)<\\/p>/g, '$1'],\n [/<p>(<hr[^>]*>)<\\/p>/g, '$1'],\n [/<p>(<table[^>]*>)/g, '$1'],\n [/(<\\/table>)<\\/p>/g, '$1'],\n [/<p>(<pre[^>]*>)/g, '$1'],\n [/(<\\/pre>)<\\/p>/g, '$1'],\n [new RegExp(`<p>(${PLACEHOLDER_CB}\\\\d+§)</p>`, 'g'), '$1']\n ];\n cleanupPatterns.forEach(([pattern, replacement]) => {\n html = html.replace(pattern, replacement);\n });\n\n // When a block element is followed by a newline and then text, open a <p>.\n html = html.replace(/(<\\/(?:h[1-6]|blockquote|ul|ol|table|pre|hr)>)\\n([^<])/g, '$1\\n<p>$2');\n\n // ────────────────────────────────────────────────────────────────\n // Phase 4 — Code Restoration\n // ────────────────────────────────────────────────────────────────\n // Replace placeholders with rendered HTML. For fenced blocks this\n // means wrapping in <pre><code>…</code></pre> (or calling the\n // fence_plugin). For inline code it means <code>…</code>.\n\n codeBlocks.forEach((block, i) => {\n let replacement;\n\n if (block.custom && fence_plugin && fence_plugin.render) {\n // Delegate to the user-provided fence plugin.\n replacement = fence_plugin.render(block.code, block.lang);\n\n if (replacement === undefined) {\n // Plugin declined — fall back to default rendering.\n const langClass = !inline_styles && block.lang ? ` class=\"language-${block.lang}\"` : '';\n const codeAttr = inline_styles ? getAttr('code') : langClass;\n /* istanbul ignore next - bd-only branch */\n const langAttr = bidirectional && block.lang ? ` data-qd-lang=\"${escapeHtml(block.lang)}\"` : '';\n /* istanbul ignore next - bd-only branch */\n const fenceAttr = bidirectional ? ` data-qd-fence=\"${escapeHtml(block.fence)}\"` : '';\n replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${escapeHtml(block.code)}</code></pre>`;\n } else /* istanbul ignore next - bd-only branch */ if (bidirectional) {\n // Plugin returned HTML — inject data attributes for roundtrip.\n replacement = replacement.replace(/^<(\\w+)/,\n `<$1 data-qd-fence=\"${escapeHtml(block.fence)}\" data-qd-lang=\"${escapeHtml(block.lang)}\" data-qd-source=\"${escapeHtml(block.code)}\"`);\n }\n } else {\n // Default rendering — wrap in <pre><code>.\n const langClass = !inline_styles && block.lang ? ` class=\"language-${block.lang}\"` : '';\n const codeAttr = inline_styles ? getAttr('code') : langClass;\n /* istanbul ignore next - bd-only branch */\n const langAttr = bidirectional && block.lang ? ` data-qd-lang=\"${escapeHtml(block.lang)}\"` : '';\n /* istanbul ignore next - bd-only branch */\n const fenceAttr = bidirectional ? ` data-qd-fence=\"${escapeHtml(block.fence)}\"` : '';\n replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${block.code}</code></pre>`;\n }\n\n const placeholder = `${PLACEHOLDER_CB}${i}§`;\n html = html.replace(placeholder, replacement);\n });\n\n // Restore inline code spans\n inlineCodes.forEach((code, i) => {\n const placeholder = `${PLACEHOLDER_IC}${i}§`;\n html = html.replace(placeholder, `<code${getAttr('code')}${dataQd('`')}>${code}</code>`);\n });\n\n return html.trim();\n}\n\n// ════════════════════════════════════════════════════════════════════\n// Block-level line scanner\n// ════════════════════════════════════════════════════════════════════\n\n/**\n * scanLineBlocks — single-pass line scanner for headings, HR, blockquotes\n *\n * Walks the text line by line. For each line it checks (in order):\n * 1. Heading — starts with 1-6 '#' followed by a space\n * 2. HR — line is entirely '---…' (3+ dashes, optional trailing space)\n * 3. Blockquote — starts with '&gt; ' (the > was already HTML-escaped)\n *\n * Lines that don't match any block pattern are passed through unchanged.\n *\n * This replaces three separate global regex passes from the pre-1.2.8\n * architecture with one structured scan.\n *\n * @param {string} text The document text (HTML-escaped, code extracted)\n * @param {Function} getAttr Attribute factory (class or style)\n * @param {Function} dataQd Bidirectional marker factory\n * @returns {string} Text with block-level elements rendered\n */\nfunction scanLineBlocks(text, getAttr, dataQd) {\n const lines = text.split('\\n');\n const result = [];\n let i = 0;\n\n while (i < lines.length) {\n const line = lines[i];\n\n // ── Heading ──\n // Count leading '#' characters. Valid heading: 1-6 hashes then a space.\n // Example: \"## Hello World ##\" → <h2>Hello World</h2>\n let hashCount = 0;\n while (hashCount < line.length && hashCount < 7 && line[hashCount] === '#') {\n hashCount++;\n }\n if (hashCount >= 1 && hashCount <= 6 && line[hashCount] === ' ') {\n // Extract content after \"# \" and strip trailing hashes\n const content = line.slice(hashCount + 1).replace(/\\s*#+\\s*$/, '');\n const tag = 'h' + hashCount;\n result.push(`<${tag}${getAttr(tag)}${dataQd('#'.repeat(hashCount))}>${content}</${tag}>`);\n i++;\n continue;\n }\n\n // ── Horizontal Rule ──\n // Three or more dashes, optional trailing whitespace, nothing else.\n if (isDashHRLine(line)) {\n result.push(`<hr${getAttr('hr')}>`);\n i++;\n continue;\n }\n\n // ── Blockquote ──\n // After Phase 2, the '>' character has been escaped to '&gt;'.\n // Pattern: \"&gt; content\" or merged consecutive blockquotes.\n if (/^&gt;\\s+/.test(line)) {\n result.push(`<blockquote${getAttr('blockquote')}>${line.replace(/^&gt;\\s+/, '')}</blockquote>`);\n i++;\n continue;\n }\n\n // ── Pass-through ──\n result.push(line);\n i++;\n }\n\n // Merge consecutive blockquotes into a single element.\n // <blockquote>A</blockquote>\\n<blockquote>B</blockquote>\n // → <blockquote>A\\nB</blockquote>\n let joined = result.join('\\n');\n joined = joined.replace(/<\\/blockquote>\\n<blockquote>/g, '\\n');\n return joined;\n}\n\n// ════════════════════════════════════════════════════════════════════\n// Table processing (line walker)\n// ════════════════════════════════════════════════════════════════════\n\n/**\n * Inline markdown formatter for table cells.\n * Handles bold, italic, strikethrough, and code within cell text.\n * Links / images / autolinks are handled by the global inline pass\n * (Phase 3 Step 4) which runs after table processing.\n */\nfunction processInlineMarkdown(text, getAttr) {\n const patterns = [\n [/\\*\\*(.+?)\\*\\*/g, 'strong'],\n [/__(.+?)__/g, 'strong'],\n [/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, 'em'],\n [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em'],\n [/~~(.+?)~~/g, 'del'],\n [/`([^`]+)`/g, 'code']\n ];\n patterns.forEach(([pattern, tag]) => {\n text = text.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);\n });\n return text;\n}\n\n/**\n * processTable — line walker for markdown tables\n *\n * Walks through lines looking for runs of pipe-containing lines.\n * Each run is validated (must contain a separator row: |---|---|)\n * and rendered as an HTML <table>. Invalid runs are restored as-is.\n *\n * @param {string} text Full document text\n * @param {Function} getAttr Attribute factory\n * @returns {string} Text with tables rendered\n */\nfunction processTable(text, getAttr) {\n const lines = text.split('\\n');\n const result = [];\n let inTable = false;\n let tableLines = [];\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n\n if (line.includes('|') && (line.startsWith('|') || /[^\\\\|]/.test(line))) {\n if (!inTable) {\n inTable = true;\n tableLines = [];\n }\n tableLines.push(line);\n } else {\n if (inTable) {\n const tableHtml = buildTable(tableLines, getAttr);\n if (tableHtml) {\n result.push(tableHtml);\n } else {\n result.push(...tableLines);\n }\n inTable = false;\n tableLines = [];\n }\n result.push(lines[i]);\n }\n }\n\n // Handle table at end of document\n if (inTable && tableLines.length > 0) {\n const tableHtml = buildTable(tableLines, getAttr);\n if (tableHtml) {\n result.push(tableHtml);\n } else {\n result.push(...tableLines);\n }\n }\n\n return result.join('\\n');\n}\n\n/**\n * buildTable — validate and render a table from accumulated lines\n *\n * @param {string[]} lines Array of pipe-containing lines\n * @param {Function} getAttr Attribute factory\n * @returns {string|null} HTML table string, or null if invalid\n */\nfunction buildTable(lines, getAttr) {\n if (lines.length < 2) return null;\n\n // Find the separator row (---|---|)\n let separatorIndex = -1;\n for (let i = 1; i < lines.length; i++) {\n if (/^\\|?[\\s\\-:|]+\\|?$/.test(lines[i]) && lines[i].includes('-')) {\n separatorIndex = i;\n break;\n }\n }\n if (separatorIndex === -1) return null;\n\n const headerLines = lines.slice(0, separatorIndex);\n const bodyLines = lines.slice(separatorIndex + 1);\n\n // Parse alignment from separator cells (:--- = left, :---: = center, ---: = right)\n const separator = lines[separatorIndex];\n const separatorCells = separator.trim().replace(/^\\|/, '').replace(/\\|$/, '').split('|');\n const alignments = separatorCells.map(cell => {\n const trimmed = cell.trim();\n if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';\n if (trimmed.endsWith(':')) return 'right';\n return 'left';\n });\n\n let html = `<table${getAttr('table')}>\\n`;\n\n // Header\n html += `<thead${getAttr('thead')}>\\n`;\n headerLines.forEach(line => {\n html += `<tr${getAttr('tr')}>\\n`;\n const cells = line.trim().replace(/^\\|/, '').replace(/\\|$/, '').split('|');\n cells.forEach((cell, i) => {\n const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';\n const processedCell = processInlineMarkdown(cell.trim(), getAttr);\n html += `<th${getAttr('th', alignStyle)}>${processedCell}</th>\\n`;\n });\n html += '</tr>\\n';\n });\n html += '</thead>\\n';\n\n // Body\n if (bodyLines.length > 0) {\n html += `<tbody${getAttr('tbody')}>\\n`;\n bodyLines.forEach(line => {\n html += `<tr${getAttr('tr')}>\\n`;\n const cells = line.trim().replace(/^\\|/, '').replace(/\\|$/, '').split('|');\n cells.forEach((cell, i) => {\n const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';\n const processedCell = processInlineMarkdown(cell.trim(), getAttr);\n html += `<td${getAttr('td', alignStyle)}>${processedCell}</td>\\n`;\n });\n html += '</tr>\\n';\n });\n html += '</tbody>\\n';\n }\n\n html += '</table>';\n return html;\n}\n\n// ════════════════════════════════════════════════════════════════════\n// List processing (line walker)\n// ════════════════════════════════════════════════════════════════════\n\n/**\n * processLists — line walker for ordered, unordered, and task lists\n *\n * Scans each line for list markers (-, *, +, 1., 2., etc.) with\n * optional leading indentation for nesting. Non-list lines close\n * any open lists and pass through unchanged.\n *\n * Task lists (- [ ] / - [x]) are detected and rendered with\n * checkbox inputs.\n *\n * @param {string} text Full document text\n * @param {Function} getAttr Attribute factory\n * @param {boolean} inline_styles Whether to use inline styles\n * @param {boolean} bidirectional Whether to add data-qd markers\n * @returns {string} Text with lists rendered\n */\nfunction processLists(text, getAttr, inline_styles, bidirectional) {\n const lines = text.split('\\n');\n const result = [];\n const listStack = []; // tracks nesting: [{type:'ul', level:0}, …]\n\n // Helper to escape HTML for data-qd attributes. List markers (`-`, `*`,\n // `+`, `1.`, etc.) never contain HTML-special chars, so the replace\n // callback is defensive-only and never actually fires in practice.\n /* istanbul ignore next - defensive: list markers never trigger escaping */\n const escapeHtml = (text) => text.replace(/[&<>\"']/g,\n /* istanbul ignore next - defensive: list markers never contain HTML specials */\n m => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'})[m]);\n /* istanbul ignore next - trivial no-op fallback; not exercised via bd bundle */\n const dataQd = bidirectional ? (marker) => ` data-qd=\"${escapeHtml(marker)}\"` : () => '';\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const match = line.match(/^(\\s*)([*\\-+]|\\d+\\.)\\s+(.+)$/);\n\n if (match) {\n const [, indent, marker, content] = match;\n const level = Math.floor(indent.length / 2);\n const isOrdered = /^\\d+\\./.test(marker);\n const listType = isOrdered ? 'ol' : 'ul';\n\n // Task list detection (only in unordered lists)\n let listItemContent = content;\n let taskListClass = '';\n const taskMatch = content.match(/^\\[([x ])\\]\\s+(.*)$/i);\n if (taskMatch && !isOrdered) {\n const [, checked, taskContent] = taskMatch;\n const isChecked = checked.toLowerCase() === 'x';\n const checkboxAttr = inline_styles\n ? ' style=\"margin-right:.5em\"'\n : ` class=\"${CLASS_PREFIX}task-checkbox\"`;\n listItemContent = `<input type=\"checkbox\"${checkboxAttr}${isChecked ? ' checked' : ''} disabled> ${taskContent}`;\n taskListClass = inline_styles ? ' style=\"list-style:none\"' : ` class=\"${CLASS_PREFIX}task-item\"`;\n }\n\n // Close deeper nesting levels\n while (listStack.length > level + 1) {\n const list = listStack.pop();\n result.push(`</${list.type}>`);\n }\n\n // Open new list or switch type at current level\n if (listStack.length === level) {\n listStack.push({ type: listType, level });\n result.push(`<${listType}${getAttr(listType)}>`);\n } else if (listStack.length === level + 1) {\n const currentList = listStack[listStack.length - 1];\n if (currentList.type !== listType) {\n result.push(`</${currentList.type}>`);\n listStack.pop();\n listStack.push({ type: listType, level });\n result.push(`<${listType}${getAttr(listType)}>`);\n }\n }\n\n const liAttr = taskListClass || getAttr('li');\n result.push(`<li${liAttr}${dataQd(marker)}>${listItemContent}</li>`);\n } else {\n // Not a list item — close all open lists\n while (listStack.length > 0) {\n const list = listStack.pop();\n result.push(`</${list.type}>`);\n }\n result.push(line);\n }\n }\n\n // Close any remaining open lists\n while (listStack.length > 0) {\n const list = listStack.pop();\n result.push(`</${list.type}>`);\n }\n\n return result.join('\\n');\n}\n\n// ════════════════════════════════════════════════════════════════════\n// Static API\n// ════════════════════════════════════════════════════════════════════\n\n/**\n * Emit CSS rules for all quikdown elements.\n *\n * @param {string} prefix Class prefix (default: 'quikdown-')\n * @param {string} theme 'light' (default) or 'dark'\n * @returns {string} CSS text\n */\nquikdown.emitStyles = function(prefix = 'quikdown-', theme = 'light') {\n const styles = QUIKDOWN_STYLES;\n\n const themeOverrides = {\n dark: {\n '#f4f4f4': '#2a2a2a', // pre background\n '#f0f0f0': '#2a2a2a', // code background\n '#f2f2f2': '#2a2a2a', // th background\n '#ddd': '#3a3a3a', // borders\n '#06c': '#6db3f2', // links\n _textColor: '#e0e0e0'\n },\n light: {\n _textColor: '#333'\n }\n };\n\n let css = '';\n for (const [tag, style] of Object.entries(styles)) {\n let themedStyle = style;\n\n if (theme === 'dark' && themeOverrides.dark) {\n for (const [oldColor, newColor] of Object.entries(themeOverrides.dark)) {\n if (!oldColor.startsWith('_')) {\n themedStyle = themedStyle.replaceAll(oldColor, newColor);\n }\n }\n const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];\n if (needsTextColor.includes(tag)) {\n themedStyle += `;color:${themeOverrides.dark._textColor}`;\n }\n } else if (theme === 'light' && themeOverrides.light) {\n const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];\n if (needsTextColor.includes(tag)) {\n themedStyle += `;color:${themeOverrides.light._textColor}`;\n }\n }\n\n css += `.${prefix}${tag} { ${themedStyle} }\\n`;\n }\n\n return css;\n};\n\n/**\n * Create a pre-configured parser with baked-in options.\n *\n * @param {Object} options Options to bake in\n * @returns {Function} Configured quikdown(markdown) function\n */\nquikdown.configure = function(options) {\n return function(markdown) {\n return quikdown(markdown, options);\n };\n};\n\n/** Semantic version (injected at build time) */\nquikdown.version = quikdownVersion;\n\n// ════════════════════════════════════════════════════════════════════\n// Exports\n// ════════════════════════════════════════════════════════════════════\n\n/* istanbul ignore next */\nif (typeof module !== 'undefined' && module.exports) {\n module.exports = quikdown;\n}\n\n/* istanbul ignore next */\nif (typeof window !== 'undefined') {\n window.quikdown = quikdown;\n}\n\nexport default quikdown;\n"],"names":["isDashHRLine","trimmed","length","i","ch","j","CLASS_PREFIX","PLACEHOLDER_CB","ESC_MAP","QUIKDOWN_STYLES","h1","h2","h3","h4","h5","h6","pre","code","blockquote","table","th","td","hr","img","a","strong","em","del","ul","ol","li","quikdown","markdown","options","fence_plugin","inline_styles","bidirectional","lazy_linefeeds","allow_unsafe_html","getAttr","styles","tag","additionalStyle","style","includes","replace","trim","endsWith","classAttr","createGetAttr","escapeHtml","text","m","dataQd","marker","sanitizeUrl","url","allowUnsafe","trimmedUrl","lowerUrl","toLowerCase","dangerousProtocols","protocol","startsWith","html","codeBlocks","inlineCodes","match","fence","lang","placeholder","langTrimmed","render","push","trimEnd","custom","hasReverse","reverse","lines","split","result","inTable","tableLines","line","test","tableHtml","buildTable","join","processTable","hashCount","content","slice","repeat","joined","scanLineBlocks","listStack","indent","level","Math","floor","isOrdered","listType","listItemContent","taskListClass","taskMatch","checked","taskContent","isChecked","list","pop","type","currentList","liAttr","processLists","alt","src","sanitizedSrc","allow_unsafe_urls","altAttr","srcAttr","href","sanitizedHref","rel","textAttr","prefix","sanitizedUrl","forEach","pattern","blocks","bi","b","offset","substring","RegExp","replacement","block","undefined","langClass","codeAttr","langAttr","fenceAttr","processInlineMarkdown","separatorIndex","headerLines","bodyLines","alignments","map","cell","alignStyle","processedCell","emitStyles","theme","themeOverrides","_textColor","css","Object","entries","themedStyle","oldColor","newColor","replaceAll","configure","version","module","exports","window"],"mappings":"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"}
1
+ {"version":3,"file":"quikdown.esm.min.js","sources":["../src/quikdown_classify.js","../src/quikdown.js"],"sourcesContent":["/**\n * quikdown_classify — Shared line-classification utilities\n * ═════════════════════════════════════════════════════════\n *\n * Pure functions for classifying markdown lines. Used by both the main\n * parser (quikdown.js) and the editor (quikdown_edit.js) so the logic\n * lives in one place.\n *\n * All functions operate on a **trimmed** line (caller must trim).\n * None use regexes with nested quantifiers — every check is either a\n * simple regex or a linear scan, so there is zero ReDoS risk.\n */\n\n/**\n * Full CommonMark HR check: three or more identical characters from\n * {-, *, _} with optional interspersed whitespace.\n *\n * Examples that return true: ---, ***, ___, ----, - - -, * * *, _ _ _\n * Examples that return false: --, - text, ---text, mixed -_*, empty\n *\n * Algorithm (O(n), single pass, no backtracking):\n * 1. Strip all whitespace\n * 2. Verify length >= 3\n * 3. First char must be -, *, or _\n * 4. Every remaining char must equal the first\n *\n * @param {string} trimmed The line, already trimmed\n * @returns {boolean}\n */\nexport function isHRLine(trimmed) {\n if (trimmed.length < 3) return false;\n\n // Strip whitespace via linear scan\n let stripped = '';\n for (let i = 0; i < trimmed.length; i++) {\n const ch = trimmed[i];\n if (ch !== ' ' && ch !== '\\t') stripped += ch;\n }\n\n if (stripped.length < 3) return false;\n\n const ch = stripped[0];\n if (ch !== '-' && ch !== '*' && ch !== '_') return false;\n\n for (let i = 1; i < stripped.length; i++) {\n if (stripped[i] !== ch) return false;\n }\n return true;\n}\n\n/**\n * Dash-only HR check — exact parity with the main parser's original\n * regex `/^---+\\s*$/`. Only matches lines of three or more dashes\n * with optional trailing whitespace (no interspersed spaces).\n *\n * @param {string} trimmed The line, already trimmed\n * @returns {boolean}\n */\nexport function isDashHRLine(trimmed) {\n if (trimmed.length < 3) return false;\n for (let i = 0; i < trimmed.length; i++) {\n const ch = trimmed[i];\n if (ch === '-') continue;\n // Allow trailing whitespace only\n if (ch === ' ' || ch === '\\t') {\n for (let j = i + 1; j < trimmed.length; j++) {\n if (trimmed[j] !== ' ' && trimmed[j] !== '\\t') return false;\n }\n return i >= 3; // at least 3 dashes before whitespace\n }\n return false;\n }\n return true; // all dashes\n}\n\n/**\n * Check if a trimmed line opens a code fence.\n * Returns { char, len, lang } if it does, or null otherwise.\n *\n * A fence opener is 3+ identical backticks or tildes at the start of a line,\n * optionally followed by a language tag.\n *\n * @param {string} trimmed The line, already trimmed\n * @returns {{ char: string, len: number, lang: string } | null}\n */\nexport function fenceOpen(trimmed) {\n if (trimmed.length < 3) return null;\n const ch = trimmed[0];\n if (ch !== '`' && ch !== '~') return null;\n\n let len = 1;\n while (len < trimmed.length && trimmed[len] === ch) len++;\n if (len < 3) return null;\n\n const lang = trimmed.slice(len).trim();\n return { char: ch, len, lang };\n}\n\n/**\n * Check if a trimmed line closes an open fence.\n * The closing fence must use the same character, be at least as long,\n * and have no content after (optional trailing whitespace only).\n *\n * @param {string} trimmed The line, already trimmed\n * @param {string} openChar The fence character ('`' or '~')\n * @param {number} openLen Length of the opening fence marker\n * @returns {boolean}\n */\nexport function isFenceClose(trimmed, openChar, openLen) {\n if (trimmed.length < openLen) return false;\n\n let len = 0;\n while (len < trimmed.length && trimmed[len] === openChar) len++;\n if (len < openLen) return false;\n\n // Rest must be whitespace only\n for (let i = len; i < trimmed.length; i++) {\n if (trimmed[i] !== ' ' && trimmed[i] !== '\\t') return false;\n }\n return true;\n}\n\n/**\n * Classify a content line into a category string.\n * Order matters: HR before list-ul (since `- - -` looks like a list start).\n *\n * @param {string} trimmed The line, already trimmed\n * @returns {string} One of: 'heading', 'hr', 'list-ol', 'list-ul',\n * 'blockquote', 'table', 'paragraph'\n */\nexport function classifyLine(trimmed) {\n if (/^#{1,6}\\s/.test(trimmed)) return 'heading';\n if (isHRLine(trimmed)) return 'hr';\n if (/^\\d+\\.\\s/.test(trimmed)) return 'list-ol';\n if (/^[-*+]\\s/.test(trimmed)) return 'list-ul';\n if (/^>/.test(trimmed)) return 'blockquote';\n if (/^\\|/.test(trimmed)) return 'table';\n return 'paragraph';\n}\n\n/**\n * Heuristic: does a line look like a markdown table row?\n * @param {string} line The line (trimmed or untrimmed)\n * @returns {boolean}\n */\nexport function looksLikeTableRow(line) {\n return line.includes('|');\n}\n","/**\n * quikdown — A compact, scanner-based markdown parser\n * ════════════════════════════════════════════════════\n *\n * Architecture overview (v1.2.8 — lexer rewrite)\n * ───────────────────────────────────────────────\n * Prior to v1.2.8, quikdown used a multi-pass regex pipeline: each block\n * type (headings, blockquotes, HR, lists, tables) and each inline format\n * (bold, italic, links, …) was handled by its own global regex applied\n * sequentially to the full document string. That worked but made the code\n * hard to extend and debug — a new construct meant adding another regex\n * pass, and ordering bugs between passes were subtle.\n *\n * Starting in v1.2.8 the parser uses a **line-scanning** approach for\n * block detection and a **per-block inline pass** for formatting:\n *\n * ┌─────────────────────────────────────────────────────────┐\n * │ Phase 1 — Code Extraction │\n * │ Scan for fenced code blocks (``` / ~~~) and inline │\n * │ code spans (`…`). Replace with §CB§ / §IC§ place- │\n * │ holders so code content is never touched by later │\n * │ phases. │\n * ├─────────────────────────────────────────────────────────┤\n * │ Phase 2 — HTML Escaping │\n * │ Escape &, <, >, \", ' in the remaining text to prevent │\n * │ XSS. (Skipped when allow_unsafe_html is true.) │\n * ├─────────────────────────────────────────────────────────┤\n * │ Phase 3 — Block Scanning │\n * │ Walk the text **line by line**. At each line, the │\n * │ scanner checks (in order): │\n * │ • table rows (|) │\n * │ • headings (#) │\n * │ • HR (---) │\n * │ • blockquotes (&gt;) │\n * │ • list items (-, *, +, 1.) │\n * │ • code-block placeholder (§CB…§) │\n * │ • paragraph text (everything else) │\n * │ │\n * │ Block text is run through the **inline formatter** │\n * │ which handles bold, italic, strikethrough, links, │\n * │ images, and autolinks. │\n * │ │\n * │ Paragraphs are wrapped in <p> tags. Lazy linefeeds │\n * │ (single \\n → <br>) are handled here too. │\n * ├─────────────────────────────────────────────────────────┤\n * │ Phase 4 — Code Restoration │\n * │ Replace §CB§ / §IC§ placeholders with rendered <pre> │\n * │ / <code> HTML, applying the fence_plugin if present. │\n * └─────────────────────────────────────────────────────────┘\n *\n * Why this design?\n * • Single pass over lines for block identification — no re-scanning.\n * • Each block type is a clearly separated branch, easy to add new ones.\n * • Inline formatting is confined to block text — can't accidentally\n * match across block boundaries or inside HTML tags.\n * • Code extraction still uses a simple regex (it's one pattern, not a\n * chain) because the §-placeholder approach is proven and simple.\n *\n * @param {string} markdown The markdown source text\n * @param {Object} options Configuration (see below)\n * @returns {string} Rendered HTML\n */\n\nimport { isDashHRLine } from './quikdown_classify.js';\n\n// ────────────────────────────────────────────────────────────────────\n// Constants\n// ────────────────────────────────────────────────────────────────────\n\n/** Build-time version stamp (injected by tools/updateVersion) */\nconst quikdownVersion = '__QUIKDOWN_VERSION__';\n\n/** CSS class prefix used for all generated elements */\nconst CLASS_PREFIX = 'quikdown-';\n\n/** Placeholder sigils — chosen to be extremely unlikely in real text */\nconst PLACEHOLDER_CB = '§CB'; // fenced code blocks\nconst PLACEHOLDER_IC = '§IC'; // inline code spans\n\n/** HTML entity escape map */\nconst ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'};\n\n// ────────────────────────────────────────────────────────────────────\n// Style definitions\n// ────────────────────────────────────────────────────────────────────\n\n/**\n * Inline styles for every element quikdown can emit.\n * When `inline_styles: true` these are injected as style=\"…\" attributes.\n * When `inline_styles: false` (default) we use class=\"quikdown-<tag>\"\n * and these same values are emitted by `quikdown.emitStyles()`.\n */\nconst QUIKDOWN_STYLES = {\n h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',\n h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',\n h3: 'font-size:1.25em;font-weight:600;margin:1em 0',\n h4: 'font-size:1em;font-weight:600;margin:1.33em 0',\n h5: 'font-size:.875em;font-weight:600;margin:1.67em 0',\n h6: 'font-size:.85em;font-weight:600;margin:2em 0',\n pre: 'background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0',\n code: 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace',\n blockquote: 'border-left:4px solid #ddd;margin-left:0;padding-left:1em',\n table: 'border-collapse:collapse;width:100%;margin:1em 0',\n th: 'border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left',\n td: 'border:1px solid #ddd;padding:8px;text-align:left',\n hr: 'border:none;border-top:1px solid #ddd;margin:1em 0',\n img: 'max-width:100%;height:auto',\n a: 'color:#06c;text-decoration:underline',\n strong: 'font-weight:bold',\n em: 'font-style:italic',\n del: 'text-decoration:line-through',\n ul: 'margin:.5em 0;padding-left:2em',\n ol: 'margin:.5em 0;padding-left:2em',\n li: 'margin:.25em 0',\n 'task-item': 'list-style:none',\n 'task-checkbox': 'margin-right:.5em'\n};\n\n// ────────────────────────────────────────────────────────────────────\n// Attribute factory\n// ────────────────────────────────────────────────────────────────────\n\n/**\n * Creates a `getAttr(tag, additionalStyle?)` helper that returns\n * either a class=\"…\" or style=\"…\" attribute string depending on mode.\n *\n * @param {boolean} inline_styles True → emit style=\"…\"; false → class=\"…\"\n * @param {Object} styles The QUIKDOWN_STYLES map\n * @returns {Function}\n */\nfunction createGetAttr(inline_styles, styles) {\n return function(tag, additionalStyle = '') {\n if (inline_styles) {\n let style = styles[tag];\n if (!style && !additionalStyle) return '';\n\n // When adding alignment that conflicts with the tag's default,\n // strip the default text-align first.\n if (additionalStyle && additionalStyle.includes('text-align') && style && style.includes('text-align')) {\n style = style.replace(/text-align:[^;]+;?/, '').trim();\n /* istanbul ignore next */\n if (style && !style.endsWith(';')) style += ';';\n }\n\n /* istanbul ignore next - defensive: additionalStyle without style doesn't occur with current tags */\n const fullStyle = additionalStyle ? (style ? `${style}${additionalStyle}` : additionalStyle) : style;\n return ` style=\"${fullStyle}\"`;\n } else {\n const classAttr = ` class=\"${CLASS_PREFIX}${tag}\"`;\n if (additionalStyle) {\n return `${classAttr} style=\"${additionalStyle}\"`;\n }\n return classAttr;\n }\n };\n}\n\n// ════════════════════════════════════════════════════════════════════\n// Main parser function\n// ════════════════════════════════════════════════════════════════════\n\nfunction quikdown(markdown, options = {}) {\n // ── Guard: only process non-empty strings ──\n if (!markdown || typeof markdown !== 'string') {\n return '';\n }\n\n // ── Unpack options ──\n const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false, allow_unsafe_html = false } = options;\n const styles = QUIKDOWN_STYLES;\n const getAttr = createGetAttr(inline_styles, styles);\n\n // ── Helpers (closed over options) ──\n\n /** Escape the five HTML-special characters. */\n function escapeHtml(text) {\n return text.replace(/[&<>\"']/g, m => ESC_MAP[m]);\n }\n\n /**\n * Bidirectional marker helper.\n * When bidirectional mode is on, returns ` data-qd=\"…\"`.\n * The non-bidirectional branch is a trivial no-op arrow; it is\n * exercised in the core bundle but never in quikdown_bd.\n */\n /* istanbul ignore next - trivial no-op fallback */\n const dataQd = bidirectional ? (marker) => ` data-qd=\"${escapeHtml(marker)}\"` : () => '';\n\n /**\n * Sanitize a URL to block javascript:, vbscript:, and non-image data: URIs.\n * Returns '#' for blocked URLs.\n */\n function sanitizeUrl(url, allowUnsafe = false) {\n /* istanbul ignore next - defensive programming, regex ensures url is never empty */\n if (!url) return '';\n if (allowUnsafe) return url;\n\n const trimmedUrl = url.trim();\n const lowerUrl = trimmedUrl.toLowerCase();\n const dangerousProtocols = ['javascript:', 'vbscript:', 'data:'];\n\n for (const protocol of dangerousProtocols) {\n if (lowerUrl.startsWith(protocol)) {\n if (protocol === 'data:' && lowerUrl.startsWith('data:image/')) {\n return trimmedUrl;\n }\n return '#';\n }\n }\n return trimmedUrl;\n }\n\n // ────────────────────────────────────────────────────────────────\n // Phase 1 — Code Extraction\n // ────────────────────────────────────────────────────────────────\n // Why extract code first? Fenced blocks and inline code spans can\n // contain markdown-like characters (*, _, #, |, etc.) that must NOT\n // be interpreted as formatting. By pulling them out and replacing\n // with unique placeholders, the rest of the pipeline never sees them.\n\n let html = markdown;\n const codeBlocks = []; // Array of {lang, code, custom, fence, hasReverse}\n const inlineCodes = []; // Array of escaped-HTML strings\n\n // ── Fenced code blocks ──\n // Matches paired fences: ``` with ``` and ~~~ with ~~~.\n // The fence must start at column 0 of a line (^ with /m flag).\n // Group 1 = fence marker, Group 2 = language hint, Group 3 = code body.\n html = html.replace(/^(```|~~~)([^\\n]*)\\n([\\s\\S]*?)^\\1$/gm, (match, fence, lang, code) => {\n const placeholder = `${PLACEHOLDER_CB}${codeBlocks.length}§`;\n const langTrimmed = lang ? lang.trim() : '';\n\n if (fence_plugin && fence_plugin.render && typeof fence_plugin.render === 'function') {\n // Custom plugin — store raw code (un-escaped) so the plugin\n // receives the original source.\n codeBlocks.push({\n lang: langTrimmed,\n code: code.trimEnd(),\n custom: true,\n fence: fence,\n hasReverse: !!fence_plugin.reverse\n });\n } else {\n // Default — pre-escape the code for safe HTML output.\n codeBlocks.push({\n lang: langTrimmed,\n code: escapeHtml(code.trimEnd()),\n custom: false,\n fence: fence\n });\n }\n return placeholder;\n });\n\n // ── Inline code spans ──\n // Matches a single backtick pair: `content`.\n // Content is captured and HTML-escaped immediately.\n html = html.replace(/`([^`]+)`/g, (match, code) => {\n const placeholder = `${PLACEHOLDER_IC}${inlineCodes.length}§`;\n inlineCodes.push(escapeHtml(code));\n return placeholder;\n });\n\n // ────────────────────────────────────────────────────────────────\n // Phase 2 — HTML Escaping\n // ────────────────────────────────────────────────────────────────\n // All remaining text (everything except code placeholders) is escaped\n // to prevent XSS. The `allow_unsafe_html` option skips this for\n // trusted pipelines that intentionally embed raw HTML.\n\n if (!allow_unsafe_html) {\n html = escapeHtml(html);\n }\n\n // ────────────────────────────────────────────────────────────────\n // Phase 3 — Block Scanning + Inline Formatting + Paragraphs\n // ────────────────────────────────────────────────────────────────\n // This is the heart of the lexer rewrite. Instead of applying\n // 10+ global regex passes, we:\n // 1. Process tables (line walker — tables need multi-line lookahead)\n // 2. Scan remaining lines for headings, HR, blockquotes\n // 3. Process lists (line walker — lists need indent tracking)\n // 4. Apply inline formatting to all text content\n // 5. Wrap remaining text in <p> tags\n //\n // Steps 1 and 3 are line-walkers that process the full text in a\n // single pass each. Step 2 replaces global regex with a per-line\n // scanner. Steps 4-5 are applied to the result.\n //\n // Total: 3 structured passes instead of 10+ regex passes.\n\n // ── Step 1: Tables ──\n // Tables need multi-line lookahead (header → separator → body rows)\n // so they're handled by a dedicated line-walker first.\n html = processTable(html, getAttr);\n\n // ── Step 2: Headings, HR, Blockquotes ──\n // These are simple line-level constructs. We scan each line once\n // and replace matching lines with their HTML representation.\n html = scanLineBlocks(html, getAttr, dataQd);\n\n // ── Step 3: Lists ──\n // Lists need indent-level tracking across lines, so they get their\n // own line-walker.\n html = processLists(html, getAttr, inline_styles, bidirectional);\n\n // ── Step 4: Inline formatting ──\n // Apply bold, italic, strikethrough, images, links, and autolinks\n // to all text content. This runs on the output of steps 1-3, so\n // it sees text inside headings, blockquotes, table cells, list\n // items, and paragraph text.\n\n // Images (must come before links — ![alt](src) vs [text](url))\n html = html.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, (match, alt, src) => {\n const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);\n /* istanbul ignore next - bd-only branch */\n const altAttr = bidirectional && alt ? ` data-qd-alt=\"${escapeHtml(alt)}\"` : '';\n /* istanbul ignore next - bd-only branch */\n const srcAttr = bidirectional ? ` data-qd-src=\"${escapeHtml(src)}\"` : '';\n return `<img${getAttr('img')} src=\"${sanitizedSrc}\" alt=\"${alt}\"${altAttr}${srcAttr}${dataQd('!')}>`;\n });\n\n // Links\n html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (match, text, href) => {\n const sanitizedHref = sanitizeUrl(href, options.allow_unsafe_urls);\n const isExternal = /^https?:\\/\\//i.test(sanitizedHref);\n const rel = isExternal ? ' rel=\"noopener noreferrer\"' : '';\n /* istanbul ignore next - bd-only branch */\n const textAttr = bidirectional ? ` data-qd-text=\"${escapeHtml(text)}\"` : '';\n return `<a${getAttr('a')} href=\"${sanitizedHref}\"${rel}${textAttr}${dataQd('[')}>${text}</a>`;\n });\n\n // Autolinks — bare https?:// URLs become clickable <a> tags\n html = html.replace(/(^|\\s)(https?:\\/\\/[^\\s<]+)/g, (match, prefix, url) => {\n const sanitizedUrl = sanitizeUrl(url, options.allow_unsafe_urls);\n return `${prefix}<a${getAttr('a')} href=\"${sanitizedUrl}\" rel=\"noopener noreferrer\">${url}</a>`;\n });\n\n // Protect rendered tags so emphasis regexes don't see attribute\n // values — fixes #3 (underscores in URLs interpreted as emphasis).\n const savedTags = [];\n html = html.replace(/<[^>]+>/g, m => { savedTags.push(m); return `%%T${savedTags.length - 1}%%`; });\n\n // Bold, italic, strikethrough\n const inlinePatterns = [\n [/\\*\\*(.+?)\\*\\*/g, 'strong', '**'],\n [/__(.+?)__/g, 'strong', '__'],\n [/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, 'em', '*'],\n [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em', '_'],\n [/~~(.+?)~~/g, 'del', '~~']\n ];\n inlinePatterns.forEach(([pattern, tag, marker]) => {\n html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);\n });\n\n // Restore protected tags\n html = html.replace(/%%T(\\d+)%%/g, (_, i) => savedTags[i]);\n\n // ── Step 5: Line breaks + paragraph wrapping ──\n if (lazy_linefeeds) {\n // Lazy linefeeds mode: every single \\n becomes <br> EXCEPT:\n // • Double newlines → paragraph break\n // • Newlines adjacent to block elements (h, blockquote, pre, hr, table, list)\n //\n // Strategy: protect block-adjacent newlines with §N§, convert\n // the rest, then restore.\n\n const blocks = [];\n let bi = 0;\n\n // Protect tables and lists from <br> injection\n html = html.replace(/<(table|[uo]l)[^>]*>[\\s\\S]*?<\\/\\1>/g, m => {\n blocks[bi] = m;\n return `§B${bi++}§`;\n });\n\n html = html.replace(/\\n\\n+/g, '§P§')\n // After block-level closing tags\n .replace(/(<\\/(?:h[1-6]|blockquote|pre)>)\\n/g, '$1§N§')\n .replace(/(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)\\n/g, '$1§N§')\n // Before block-level opening tags\n .replace(/\\n(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)/g, '§N§$1')\n .replace(/\\n(§B\\d+§)/g, '§N§$1')\n .replace(/(§B\\d+§)\\n/g, '$1§N§')\n // Convert surviving newlines to <br>\n .replace(/\\n/g, `<br${getAttr('br')}>`)\n // Restore\n .replace(/§N§/g, '\\n')\n .replace(/§P§/g, '</p><p>');\n\n // Restore protected blocks\n blocks.forEach((b, i) => html = html.replace(`§B${i}§`, b));\n\n html = '<p>' + html + '</p>';\n } else {\n // Standard mode: two trailing spaces → <br>, double newline → new paragraph\n html = html.replace(/ {2}$/gm, `<br${getAttr('br')}>`);\n\n html = html.replace(/\\n\\n+/g, (match, offset) => {\n const before = html.substring(0, offset);\n if (before.match(/<\\/(h[1-6]|blockquote|ul|ol|table|pre|hr)>$/)) {\n return '<p>';\n }\n return '</p><p>';\n });\n html = '<p>' + html + '</p>';\n }\n\n // ── Step 6: Cleanup ──\n // Remove <p> wrappers that accidentally enclose block elements.\n // This is simpler than trying to prevent them during wrapping.\n const cleanupPatterns = [\n [/<p><\\/p>/g, ''],\n [/<p>(<h[1-6][^>]*>)/g, '$1'],\n [/(<\\/h[1-6]>)<\\/p>/g, '$1'],\n [/<p>(<blockquote[^>]*>)/g, '$1'],\n [/(<\\/blockquote>)<\\/p>/g, '$1'],\n [/<p>(<ul[^>]*>|<ol[^>]*>)/g, '$1'],\n [/(<\\/ul>|<\\/ol>)<\\/p>/g, '$1'],\n [/<p>(<hr[^>]*>)<\\/p>/g, '$1'],\n [/<p>(<table[^>]*>)/g, '$1'],\n [/(<\\/table>)<\\/p>/g, '$1'],\n [/<p>(<pre[^>]*>)/g, '$1'],\n [/(<\\/pre>)<\\/p>/g, '$1'],\n [new RegExp(`<p>(${PLACEHOLDER_CB}\\\\d+§)</p>`, 'g'), '$1']\n ];\n cleanupPatterns.forEach(([pattern, replacement]) => {\n html = html.replace(pattern, replacement);\n });\n\n // When a block element is followed by a newline and then text, open a <p>.\n html = html.replace(/(<\\/(?:h[1-6]|blockquote|ul|ol|table|pre|hr)>)\\n([^<])/g, '$1\\n<p>$2');\n\n // ────────────────────────────────────────────────────────────────\n // Phase 4 — Code Restoration\n // ────────────────────────────────────────────────────────────────\n // Replace placeholders with rendered HTML. For fenced blocks this\n // means wrapping in <pre><code>…</code></pre> (or calling the\n // fence_plugin). For inline code it means <code>…</code>.\n\n codeBlocks.forEach((block, i) => {\n let replacement;\n\n if (block.custom && fence_plugin && fence_plugin.render) {\n // Delegate to the user-provided fence plugin.\n replacement = fence_plugin.render(block.code, block.lang);\n\n if (replacement === undefined) {\n // Plugin declined — fall back to default rendering.\n const langClass = !inline_styles && block.lang ? ` class=\"language-${block.lang}\"` : '';\n const codeAttr = inline_styles ? getAttr('code') : langClass;\n /* istanbul ignore next - bd-only branch */\n const langAttr = bidirectional && block.lang ? ` data-qd-lang=\"${escapeHtml(block.lang)}\"` : '';\n /* istanbul ignore next - bd-only branch */\n const fenceAttr = bidirectional ? ` data-qd-fence=\"${escapeHtml(block.fence)}\"` : '';\n replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${escapeHtml(block.code)}</code></pre>`;\n } else /* istanbul ignore next - bd-only branch */ if (bidirectional) {\n // Plugin returned HTML — inject data attributes for roundtrip.\n replacement = replacement.replace(/^<(\\w+)/,\n `<$1 data-qd-fence=\"${escapeHtml(block.fence)}\" data-qd-lang=\"${escapeHtml(block.lang)}\" data-qd-source=\"${escapeHtml(block.code)}\"`);\n }\n } else {\n // Default rendering — wrap in <pre><code>.\n const langClass = !inline_styles && block.lang ? ` class=\"language-${block.lang}\"` : '';\n const codeAttr = inline_styles ? getAttr('code') : langClass;\n /* istanbul ignore next - bd-only branch */\n const langAttr = bidirectional && block.lang ? ` data-qd-lang=\"${escapeHtml(block.lang)}\"` : '';\n /* istanbul ignore next - bd-only branch */\n const fenceAttr = bidirectional ? ` data-qd-fence=\"${escapeHtml(block.fence)}\"` : '';\n replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${block.code}</code></pre>`;\n }\n\n const placeholder = `${PLACEHOLDER_CB}${i}§`;\n html = html.replace(placeholder, replacement);\n });\n\n // Restore inline code spans\n inlineCodes.forEach((code, i) => {\n const placeholder = `${PLACEHOLDER_IC}${i}§`;\n html = html.replace(placeholder, `<code${getAttr('code')}${dataQd('`')}>${code}</code>`);\n });\n\n return html.trim();\n}\n\n// ════════════════════════════════════════════════════════════════════\n// Block-level line scanner\n// ════════════════════════════════════════════════════════════════════\n\n/**\n * scanLineBlocks — single-pass line scanner for headings, HR, blockquotes\n *\n * Walks the text line by line. For each line it checks (in order):\n * 1. Heading — starts with 1-6 '#' followed by a space\n * 2. HR — line is entirely '---…' (3+ dashes, optional trailing space)\n * 3. Blockquote — starts with '&gt; ' (the > was already HTML-escaped)\n *\n * Lines that don't match any block pattern are passed through unchanged.\n *\n * This replaces three separate global regex passes from the pre-1.2.8\n * architecture with one structured scan.\n *\n * @param {string} text The document text (HTML-escaped, code extracted)\n * @param {Function} getAttr Attribute factory (class or style)\n * @param {Function} dataQd Bidirectional marker factory\n * @returns {string} Text with block-level elements rendered\n */\nfunction scanLineBlocks(text, getAttr, dataQd) {\n const lines = text.split('\\n');\n const result = [];\n let i = 0;\n\n while (i < lines.length) {\n const line = lines[i];\n\n // ── Heading ──\n // Count leading '#' characters. Valid heading: 1-6 hashes then a space.\n // Example: \"## Hello World ##\" → <h2>Hello World</h2>\n let hashCount = 0;\n while (hashCount < line.length && hashCount < 7 && line[hashCount] === '#') {\n hashCount++;\n }\n if (hashCount >= 1 && hashCount <= 6 && line[hashCount] === ' ') {\n // Extract content after \"# \" and strip trailing hashes\n const content = line.slice(hashCount + 1).replace(/\\s*#+\\s*$/, '');\n const tag = 'h' + hashCount;\n result.push(`<${tag}${getAttr(tag)}${dataQd('#'.repeat(hashCount))}>${content}</${tag}>`);\n i++;\n continue;\n }\n\n // ── Horizontal Rule ──\n // Three or more dashes, optional trailing whitespace, nothing else.\n if (isDashHRLine(line)) {\n result.push(`<hr${getAttr('hr')}>`);\n i++;\n continue;\n }\n\n // ── Blockquote ──\n // After Phase 2, the '>' character has been escaped to '&gt;'.\n // Pattern: \"&gt; content\" or merged consecutive blockquotes.\n if (/^&gt;\\s+/.test(line)) {\n result.push(`<blockquote${getAttr('blockquote')}>${line.replace(/^&gt;\\s+/, '')}</blockquote>`);\n i++;\n continue;\n }\n\n // ── Pass-through ──\n result.push(line);\n i++;\n }\n\n // Merge consecutive blockquotes into a single element.\n // <blockquote>A</blockquote>\\n<blockquote>B</blockquote>\n // → <blockquote>A\\nB</blockquote>\n let joined = result.join('\\n');\n joined = joined.replace(/<\\/blockquote>\\n<blockquote>/g, '\\n');\n return joined;\n}\n\n// ════════════════════════════════════════════════════════════════════\n// Table processing (line walker)\n// ════════════════════════════════════════════════════════════════════\n\n/**\n * Inline markdown formatter for table cells.\n * Handles bold, italic, strikethrough, and code within cell text.\n * Links / images / autolinks are handled by the global inline pass\n * (Phase 3 Step 4) which runs after table processing.\n */\nfunction processInlineMarkdown(text, getAttr) {\n const patterns = [\n [/\\*\\*(.+?)\\*\\*/g, 'strong'],\n [/__(.+?)__/g, 'strong'],\n [/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, 'em'],\n [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em'],\n [/~~(.+?)~~/g, 'del'],\n [/`([^`]+)`/g, 'code']\n ];\n patterns.forEach(([pattern, tag]) => {\n text = text.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);\n });\n return text;\n}\n\n/**\n * processTable — line walker for markdown tables\n *\n * Walks through lines looking for runs of pipe-containing lines.\n * Each run is validated (must contain a separator row: |---|---|)\n * and rendered as an HTML <table>. Invalid runs are restored as-is.\n *\n * @param {string} text Full document text\n * @param {Function} getAttr Attribute factory\n * @returns {string} Text with tables rendered\n */\nfunction processTable(text, getAttr) {\n const lines = text.split('\\n');\n const result = [];\n let inTable = false;\n let tableLines = [];\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n\n if (line.includes('|') && (line.startsWith('|') || /[^\\\\|]/.test(line))) {\n if (!inTable) {\n inTable = true;\n tableLines = [];\n }\n tableLines.push(line);\n } else {\n if (inTable) {\n const tableHtml = buildTable(tableLines, getAttr);\n if (tableHtml) {\n result.push(tableHtml);\n } else {\n result.push(...tableLines);\n }\n inTable = false;\n tableLines = [];\n }\n result.push(lines[i]);\n }\n }\n\n // Handle table at end of document\n if (inTable && tableLines.length > 0) {\n const tableHtml = buildTable(tableLines, getAttr);\n if (tableHtml) {\n result.push(tableHtml);\n } else {\n result.push(...tableLines);\n }\n }\n\n return result.join('\\n');\n}\n\n/**\n * buildTable — validate and render a table from accumulated lines\n *\n * @param {string[]} lines Array of pipe-containing lines\n * @param {Function} getAttr Attribute factory\n * @returns {string|null} HTML table string, or null if invalid\n */\nfunction buildTable(lines, getAttr) {\n if (lines.length < 2) return null;\n\n // Find the separator row (---|---|)\n let separatorIndex = -1;\n for (let i = 1; i < lines.length; i++) {\n if (/^\\|?[\\s\\-:|]+\\|?$/.test(lines[i]) && lines[i].includes('-')) {\n separatorIndex = i;\n break;\n }\n }\n if (separatorIndex === -1) return null;\n\n const headerLines = lines.slice(0, separatorIndex);\n const bodyLines = lines.slice(separatorIndex + 1);\n\n // Parse alignment from separator cells (:--- = left, :---: = center, ---: = right)\n const separator = lines[separatorIndex];\n const separatorCells = separator.trim().replace(/^\\|/, '').replace(/\\|$/, '').split('|');\n const alignments = separatorCells.map(cell => {\n const trimmed = cell.trim();\n if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';\n if (trimmed.endsWith(':')) return 'right';\n return 'left';\n });\n\n let html = `<table${getAttr('table')}>\\n`;\n\n // Header\n html += `<thead${getAttr('thead')}>\\n`;\n headerLines.forEach(line => {\n html += `<tr${getAttr('tr')}>\\n`;\n const cells = line.trim().replace(/^\\|/, '').replace(/\\|$/, '').split('|');\n cells.forEach((cell, i) => {\n const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';\n const processedCell = processInlineMarkdown(cell.trim(), getAttr);\n html += `<th${getAttr('th', alignStyle)}>${processedCell}</th>\\n`;\n });\n html += '</tr>\\n';\n });\n html += '</thead>\\n';\n\n // Body\n if (bodyLines.length > 0) {\n html += `<tbody${getAttr('tbody')}>\\n`;\n bodyLines.forEach(line => {\n html += `<tr${getAttr('tr')}>\\n`;\n const cells = line.trim().replace(/^\\|/, '').replace(/\\|$/, '').split('|');\n cells.forEach((cell, i) => {\n const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';\n const processedCell = processInlineMarkdown(cell.trim(), getAttr);\n html += `<td${getAttr('td', alignStyle)}>${processedCell}</td>\\n`;\n });\n html += '</tr>\\n';\n });\n html += '</tbody>\\n';\n }\n\n html += '</table>';\n return html;\n}\n\n// ════════════════════════════════════════════════════════════════════\n// List processing (line walker)\n// ════════════════════════════════════════════════════════════════════\n\n/**\n * processLists — line walker for ordered, unordered, and task lists\n *\n * Scans each line for list markers (-, *, +, 1., 2., etc.) with\n * optional leading indentation for nesting. Non-list lines close\n * any open lists and pass through unchanged.\n *\n * Task lists (- [ ] / - [x]) are detected and rendered with\n * checkbox inputs.\n *\n * @param {string} text Full document text\n * @param {Function} getAttr Attribute factory\n * @param {boolean} inline_styles Whether to use inline styles\n * @param {boolean} bidirectional Whether to add data-qd markers\n * @returns {string} Text with lists rendered\n */\nfunction processLists(text, getAttr, inline_styles, bidirectional) {\n const lines = text.split('\\n');\n const result = [];\n const listStack = []; // tracks nesting: [{type:'ul', level:0}, …]\n\n // Helper to escape HTML for data-qd attributes. List markers (`-`, `*`,\n // `+`, `1.`, etc.) never contain HTML-special chars, so the replace\n // callback is defensive-only and never actually fires in practice.\n /* istanbul ignore next - defensive: list markers never trigger escaping */\n const escapeHtml = (text) => text.replace(/[&<>\"']/g,\n /* istanbul ignore next - defensive: list markers never contain HTML specials */\n m => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'})[m]);\n /* istanbul ignore next - trivial no-op fallback; not exercised via bd bundle */\n const dataQd = bidirectional ? (marker) => ` data-qd=\"${escapeHtml(marker)}\"` : () => '';\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const match = line.match(/^(\\s*)([*\\-+]|\\d+\\.)\\s+(.+)$/);\n\n if (match) {\n const [, indent, marker, content] = match;\n const level = Math.floor(indent.length / 2);\n const isOrdered = /^\\d+\\./.test(marker);\n const listType = isOrdered ? 'ol' : 'ul';\n\n // Task list detection (only in unordered lists)\n let listItemContent = content;\n let taskListClass = '';\n const taskMatch = content.match(/^\\[([x ])\\]\\s+(.*)$/i);\n if (taskMatch && !isOrdered) {\n const [, checked, taskContent] = taskMatch;\n const isChecked = checked.toLowerCase() === 'x';\n const checkboxAttr = inline_styles\n ? ' style=\"margin-right:.5em\"'\n : ` class=\"${CLASS_PREFIX}task-checkbox\"`;\n listItemContent = `<input type=\"checkbox\"${checkboxAttr}${isChecked ? ' checked' : ''} disabled> ${taskContent}`;\n taskListClass = inline_styles ? ' style=\"list-style:none\"' : ` class=\"${CLASS_PREFIX}task-item\"`;\n }\n\n // Close deeper nesting levels\n while (listStack.length > level + 1) {\n const list = listStack.pop();\n result.push(`</${list.type}>`);\n }\n\n // Open new list or switch type at current level\n if (listStack.length === level) {\n listStack.push({ type: listType, level });\n result.push(`<${listType}${getAttr(listType)}>`);\n } else if (listStack.length === level + 1) {\n const currentList = listStack[listStack.length - 1];\n if (currentList.type !== listType) {\n result.push(`</${currentList.type}>`);\n listStack.pop();\n listStack.push({ type: listType, level });\n result.push(`<${listType}${getAttr(listType)}>`);\n }\n }\n\n const liAttr = taskListClass || getAttr('li');\n result.push(`<li${liAttr}${dataQd(marker)}>${listItemContent}</li>`);\n } else {\n // Not a list item — close all open lists\n while (listStack.length > 0) {\n const list = listStack.pop();\n result.push(`</${list.type}>`);\n }\n result.push(line);\n }\n }\n\n // Close any remaining open lists\n while (listStack.length > 0) {\n const list = listStack.pop();\n result.push(`</${list.type}>`);\n }\n\n return result.join('\\n');\n}\n\n// ════════════════════════════════════════════════════════════════════\n// Static API\n// ════════════════════════════════════════════════════════════════════\n\n/**\n * Emit CSS rules for all quikdown elements.\n *\n * @param {string} prefix Class prefix (default: 'quikdown-')\n * @param {string} theme 'light' (default) or 'dark'\n * @returns {string} CSS text\n */\nquikdown.emitStyles = function(prefix = 'quikdown-', theme = 'light') {\n const styles = QUIKDOWN_STYLES;\n\n const themeOverrides = {\n dark: {\n '#f4f4f4': '#2a2a2a', // pre background\n '#f0f0f0': '#2a2a2a', // code background\n '#f2f2f2': '#2a2a2a', // th background\n '#ddd': '#3a3a3a', // borders\n '#06c': '#6db3f2', // links\n _textColor: '#e0e0e0'\n },\n light: {\n _textColor: '#333'\n }\n };\n\n let css = '';\n for (const [tag, style] of Object.entries(styles)) {\n let themedStyle = style;\n\n if (theme === 'dark' && themeOverrides.dark) {\n for (const [oldColor, newColor] of Object.entries(themeOverrides.dark)) {\n if (!oldColor.startsWith('_')) {\n themedStyle = themedStyle.replaceAll(oldColor, newColor);\n }\n }\n const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];\n if (needsTextColor.includes(tag)) {\n themedStyle += `;color:${themeOverrides.dark._textColor}`;\n }\n } else if (theme === 'light' && themeOverrides.light) {\n const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];\n if (needsTextColor.includes(tag)) {\n themedStyle += `;color:${themeOverrides.light._textColor}`;\n }\n }\n\n css += `.${prefix}${tag} { ${themedStyle} }\\n`;\n }\n\n return css;\n};\n\n/**\n * Create a pre-configured parser with baked-in options.\n *\n * @param {Object} options Options to bake in\n * @returns {Function} Configured quikdown(markdown) function\n */\nquikdown.configure = function(options) {\n return function(markdown) {\n return quikdown(markdown, options);\n };\n};\n\n/** Semantic version (injected at build time) */\nquikdown.version = quikdownVersion;\n\n// ════════════════════════════════════════════════════════════════════\n// Exports\n// ════════════════════════════════════════════════════════════════════\n\n/* istanbul ignore next */\nif (typeof module !== 'undefined' && module.exports) {\n module.exports = quikdown;\n}\n\n/* istanbul ignore next */\nif (typeof window !== 'undefined') {\n window.quikdown = quikdown;\n}\n\nexport default quikdown;\n"],"names":["isDashHRLine","trimmed","length","i","ch","j","CLASS_PREFIX","PLACEHOLDER_CB","ESC_MAP","QUIKDOWN_STYLES","h1","h2","h3","h4","h5","h6","pre","code","blockquote","table","th","td","hr","img","a","strong","em","del","ul","ol","li","quikdown","markdown","options","fence_plugin","inline_styles","bidirectional","lazy_linefeeds","allow_unsafe_html","getAttr","styles","tag","additionalStyle","style","includes","replace","trim","endsWith","classAttr","createGetAttr","escapeHtml","text","m","dataQd","marker","sanitizeUrl","url","allowUnsafe","trimmedUrl","lowerUrl","toLowerCase","dangerousProtocols","protocol","startsWith","html","codeBlocks","inlineCodes","match","fence","lang","placeholder","langTrimmed","render","push","trimEnd","custom","hasReverse","reverse","lines","split","result","inTable","tableLines","line","test","tableHtml","buildTable","join","processTable","hashCount","content","slice","repeat","joined","scanLineBlocks","listStack","indent","level","Math","floor","isOrdered","listType","listItemContent","taskListClass","taskMatch","checked","taskContent","isChecked","list","pop","type","currentList","liAttr","processLists","alt","src","sanitizedSrc","allow_unsafe_urls","altAttr","srcAttr","href","sanitizedHref","rel","textAttr","prefix","sanitizedUrl","savedTags","forEach","pattern","_","blocks","bi","b","offset","substring","RegExp","replacement","block","undefined","langClass","codeAttr","langAttr","fenceAttr","processInlineMarkdown","separatorIndex","headerLines","bodyLines","alignments","map","cell","alignStyle","processedCell","emitStyles","theme","themeOverrides","_textColor","css","Object","entries","themedStyle","oldColor","newColor","replaceAll","configure","version","module","exports","window"],"mappings":"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"}
@@ -5,7 +5,7 @@
5
5
  * Theme with container-based scoping.
6
6
  * Usage: <div class="quikdown-light">...content...</div>
7
7
  *
8
- * @version 1.2.9
8
+ * @version 1.2.10
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.10
4
4
  * @license BSD-2-Clause
5
5
  * @copyright DeftIO 2025
6
6
  */
@@ -118,7 +118,7 @@
118
118
  // ────────────────────────────────────────────────────────────────────
119
119
 
120
120
  /** Build-time version stamp (injected by tools/updateVersion) */
121
- const quikdownVersion = '1.2.9';
121
+ const quikdownVersion = '1.2.10';
122
122
 
123
123
  /** CSS class prefix used for all generated elements */
124
124
  const CLASS_PREFIX = 'quikdown-';
@@ -363,7 +363,6 @@
363
363
  // Images (must come before links — ![alt](src) vs [text](url))
364
364
  html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
365
365
  const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
366
- // Bidirectional attributes are only exercised via quikdown_bd bundle.
367
366
  /* istanbul ignore next - bd-only branch */
368
367
  const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
369
368
  /* istanbul ignore next - bd-only branch */
@@ -387,8 +386,12 @@
387
386
  return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
388
387
  });
389
388
 
389
+ // Protect rendered tags so emphasis regexes don't see attribute
390
+ // values — fixes #3 (underscores in URLs interpreted as emphasis).
391
+ const savedTags = [];
392
+ html = html.replace(/<[^>]+>/g, m => { savedTags.push(m); return `%%T${savedTags.length - 1}%%`; });
393
+
390
394
  // Bold, italic, strikethrough
391
- // Order matters: ** before * (so ** isn't consumed as two *s)
392
395
  const inlinePatterns = [
393
396
  [/\*\*(.+?)\*\*/g, 'strong', '**'],
394
397
  [/__(.+?)__/g, 'strong', '__'],
@@ -400,6 +403,9 @@
400
403
  html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
401
404
  });
402
405
 
406
+ // Restore protected tags
407
+ html = html.replace(/%%T(\d+)%%/g, (_, i) => savedTags[i]);
408
+
403
409
  // ── Step 5: Line breaks + paragraph wrapping ──
404
410
  if (lazy_linefeeds) {
405
411
  // Lazy linefeeds mode: every single \n becomes <br> EXCEPT: