quikdown 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * quikdown-lex - Lightweight Markdown Parser (Lexer Implementation)
3
+ * @version 1.0.3dev4
4
+ * @license BSD-2-Clause
5
+ * @copyright DeftIO 2025
6
+ */
7
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).quikdown=t()}(this,function(){"use strict";const e={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"},t={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function n(s,r={}){if(!s||"string"!=typeof s)return"";const o={inline_styles:r.inline_styles||!1,class_prefix:r.class_prefix||"quikdown-",allow_unsafe_urls:r.allow_unsafe_urls||!1,fence_plugin:r.fence_plugin||null},i=s.split("\n"),l=[];let a=0,c=null,f=[],u=[];const h=(t,n="")=>{if(o.inline_styles){const s=e[t]||"",r=n?s?`${s};${n}`:n:s;return r?` style="${r}"`:""}return` class="${o.class_prefix}${t}"`},p=e=>e.replace(/[&<>"']/g,e=>t[e]),d=e=>{if(!e)return"";if(o.allow_unsafe_urls)return e;const t=e.trim(),n=t.toLowerCase();return/^(javascript|vbscript|data):/i.test(n)?/^data:image\//i.test(n)?t:"#":t},g=e=>{if(!e)return"";const t=[];return e=e.replace(/`([^`]+)`/g,(e,n)=>(t.push(p(n)),`${t.length-1}`)),e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=p(e)).replace(/!\[([^\]]*)\]\(([^)]+)\)/g,(e,t,n)=>`<img${h("img")} src="${d(n)}" alt="${t}">`)).replace(/\[([^\]]+)\]\(([^)]+)\)/g,(e,t,n)=>{const s=d(n),r=/^https?:\/\//i.test(s)?' rel="noopener noreferrer"':"";return`<a${h("a")} href="${s}"${r}>${t}</a>`})).replace(/(^|\s)(https?:\/\/[^\s<]+)/g,(e,t,n)=>`${t}<a${h("a")} href="${d(n)}" rel="noopener noreferrer">${n}</a>`)).replace(/\*\*(.+?)\*\*/g,`<strong${h("strong")}>$1</strong>`)).replace(/__(.+?)__/g,`<strong${h("strong")}>$1</strong>`)).replace(/(?<!\*)\*(?!\*)([^*]+)\*(?!\*)/g,`<em${h("em")}>$1</em>`)).replace(/(?<!_)_(?!_)([^_]+)_(?!_)/g,`<em${h("em")}>$1</em>`)).replace(/~~(.+?)~~/g,`<del${h("del")}>$1</del>`)).replace(/ $/gm,`<br${h("br")}>`)).replace(/\x01(\d+)\x02/g,(e,n)=>`<code${h("code")}>${t[n]}</code>`)},m=e=>{const t=e.trim();if(!t)return 0;switch(t[0]){case"#":if(/^#{1,6}\s+/.test(t))return 1;break;case"-":case"*":case"_":if(/^[-*_](\s*[-*_]){2,}$/.test(t))return 2;if(/^[*+-]\s+/.test(t))return 5;break;case"+":if(/^\+\s+/.test(t))return 5;break;case"`":case"~":if(/^[`~]{3,}/.test(t))return 3;break;case">":return 4;case"|":return/^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?$/.test(t)?8:7;default:if(/^\d+\.\s+/.test(t))return 6;if(t.includes("|"))return/^\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\s*$/.test(t)?8:7}return/^\s+[*+-]\s+/.test(e)?5:/^\s+\d+\.\s+/.test(e)?6:9},$=()=>{if(f.length>0){const e=f.join("\n");l.push(`<p>${g(e)}</p>`),f=[]}},b=()=>{if(u.length>0){const e=u.join("\n").trim();if(1!==u.length||e.includes("\n"))if(2!==u.length||""!==u[1]||u[0].includes("\n")){const t=u.filter(e=>""!==e);if(0===t.length);else if(2===t.length&&t.every(e=>!e.includes("\n")&&e.trim().length>0))l.push(`<blockquote${h("blockquote")}>${g(t[0])}</blockquote>`),l.push(`<blockquote${h("blockquote")}>${g(t[1])}</blockquote>`);else{const t=n(e,o);l.push(`<blockquote${h("blockquote")}>${t}</blockquote>`)}}else l.push(`<blockquote${h("blockquote")}>${g(u[0])}</blockquote>`);else l.push(`<blockquote${h("blockquote")}>${g(e)}</blockquote>`);u=[]}},k=e=>{const t=[];let n=e;for(;n<i.length;){const e=i[n].match(/^(\s*)([*+-]|\d+\.)\s+(.+)$/);if(!e)break;const[,s,r,a]=e,c=Math.floor(s.length/2),f=/^\d+\./.test(r),u=f?"ol":"ul";let p=a,d=h("li");if(!f){const e=a.match(/^\[([x ])\]\s+(.*)$/i);if(e){const t="x"===e[1].toLowerCase(),n=o.inline_styles?' style="margin-right:.5em"':` class="${o.class_prefix}task-checkbox"`;d=o.inline_styles?' style="list-style:none"':` class="${o.class_prefix}task-item"`,p=`<input type="checkbox"${n}${t?" checked":""} disabled> ${e[2]}`}}for(;t.length>0&&c<t[t.length-1].indent;){const e=t.pop();e.items.push(`\n</${e.type}>`)}if(0===t.length||c>t[t.length-1].indent){const e={type:u,indent:c,items:c>0?[`\n<${u}${h(u)}>`]:[`<${u}${h(u)}>`]};c>0&&t.length>0&&(t[t.length-1].items.push(e.items[0]),e.items=[]),t.push(e)}else if(t[t.length-1].type!==u){const e=t.pop();e.items.push(`\n</${e.type}>`),t.length>0?t[t.length-1].items.push(e.items.join("")):l.push(e.items.join(""));const n={type:u,indent:c,items:[`<${u}${h(u)}>`]};t.push(n)}t[t.length-1].items.push(`\n<li${d}>${g(p)}</li>`),n++}for(;t.length>1;){const e=t.pop();e.items.push(`\n</${e.type}>`),t[t.length-1].items.push(e.items.join(""))}if(t.length>0){const e=t[0];e.items.push(`\n</${e.type}>`),l.push(e.items.join(""))}return n},x=e=>{let t=e;const n=[];let s=null;const r=[],o=i[t].trim().replace(/^\|/,"").replace(/\|$/,"").split("|").map(e=>e.trim());if(n.push(...o),t++,!(t<i.length))return e;if(8!==m(i[t]))return e;{const e=i[t].trim().replace(/^\|/,"").replace(/\|$/,"").split("|");s=e.map(e=>{const t=e.trim();return t.startsWith(":")&&t.endsWith(":")?"center":t.endsWith(":")?"right":"left"}),t++}for(;t<i.length;){const e=m(i[t]);if(7!==e&&8!==e)break;const n=i[t].trim().replace(/^\|/,"").replace(/\|$/,"").split("|").map(e=>e.trim());r.push(n),t++}let a=`<table${h("table")}>`;return n.length>0&&(a+=`\n<thead${h("thead")}>\n<tr${h("tr")}>\n`,n.forEach((e,t)=>{const n=s&&"left"!==s[t]?`text-align:${s[t]}`:"";a+=`<th${h("th",n)}>${g(e)}</th>\n`}),a+="</tr>\n</thead>"),r.length>0&&(a+=`\n<tbody${h("tbody")}>\n`,r.forEach(e=>{a+=`<tr${h("tr")}>\n`,e.forEach((e,t)=>{const n=s&&"left"!==s[t]?`text-align:${s[t]}`:"";a+=`<td${h("td",n)}>${g(e)}</td>\n`}),a+="</tr>\n"}),a+="</tbody>"),a+="\n</table>",l.push(a),t};let _=0;for(;_<i.length;){const e=i[_],t=m(e);switch(a){case 0:switch(t){case 0:_++;break;case 1:const t=e.trim().match(/^(#{1,6})\s+(.+?)(?:\s*#*)?$/);if(t){const e=t[1].length,n=t[2];l.push(`<h${e}${h("h"+e)}>${g(n)}</h${e}>`)}_++;break;case 2:l.push(`<hr${h("hr")}>`),_++;break;case 3:const n=e.trim().match(/^([`~]{3,})(.*)$/);n&&(a=1,c={marker:n[1][0],count:n[1].length,lang:(n[2]||"").trim(),lines:[]}),_++;break;case 4:a=4,u=[e.replace(/^\s*>\s?/,"")],_++;break;case 5:case 6:_=k(_);break;case 7:case 8:const s=x(_);s===_?(a=5,f=[e],_++):_=s;break;default:a=5,f=[e],_++}break;case 1:const n=e.trim();if(new RegExp(`^${c.marker}{${c.count},}\\s*$`).test(n)){const e=c.lines.join("\n");let t="";if(o.fence_plugin&&(t=o.fence_plugin(e,c.lang)),!t||void 0===t){const n=!o.inline_styles&&c.lang?` class="language-${c.lang}"`:"",s=o.inline_styles?h("code"):n;t=`<pre${h("pre")}><code${s}>${p(e)}</code></pre>`}l.push(t),a=0,c=null}else c.lines.push(e);_++;break;case 5:switch(t){case 0:$(),a=0,_++;break;case 1:case 2:case 3:case 4:case 5:case 6:case 7:case 8:$(),a=0;break;default:f.push(e),_++}break;case 4:4===t?(u.push(e.replace(/^\s*>\s?/,"")),_++):0===t&&_+1<i.length&&4===m(i[_+1])?(u.push(""),_++):(b(),a=0)}}return $(),b(),l.join("").trim()}return n.emitStyles=function(t="quikdown-",n="light"){const s=e,r={"#f4f4f4":"#2a2a2a","#f0f0f0":"#2a2a2a","#f2f2f2":"#2a2a2a","#ddd":"#3a3a3a","#06c":"#6db3f2",_textColor:"#e0e0e0"},o={_textColor:"#333"};let i="";for(const[e,l]of Object.entries(s))if(l){let s=l;if("dark"===n&&r){for(const[e,t]of Object.entries(r))e.startsWith("_")||(s=s.replace(new RegExp(e,"g"),t));["h1","h2","h3","h4","h5","h6","td","li","blockquote"].includes(e)&&(s+=`;color:${r._textColor}`)}else if("light"===n&&o){["h1","h2","h3","h4","h5","h6","td","li","blockquote"].includes(e)&&(s+=`;color:${o._textColor}`)}i+=`.${t}${e} { ${s} }\n`}return i},n.configure=function(e){return function(t){return n(t,e)}},n.version="1.0.3dev4","undefined"!=typeof module&&module.exports&&(module.exports=n),"undefined"!=typeof window&&(window.quikdown=n),n});
8
+ //# sourceMappingURL=quikdown-lex.umd.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quikdown-lex.umd.min.js","sources":["../src/quikdown-lex.js"],"sourcesContent":["/**\n * quikdown-lex - Hand-coded lexer/parser implementation\n * \n * This is a state-machine based markdown parser that processes input\n * line-by-line with explicit state tracking. The approach trades regex\n * complexity for hand-coded state transitions, resulting in smaller\n * minified size and more predictable performance.\n * \n * Architecture:\n * 1. Line-by-line processing with lookahead\n * 2. Explicit state tracking (NORMAL, FENCE, TABLE, LIST, BLOCKQUOTE)\n * 3. Single-pass inline processing\n * 4. Direct HTML generation (no intermediate AST)\n * \n * @version __QUIKDOWN_VERSION__\n */\n\n// ===========================================================================\n// CONSTANTS & CONFIGURATION\n// ===========================================================================\n\n// Compact style map - keys match HTML tags, values are CSS strings\n// Optimized: no spaces after colons, decimal values shortened\nconst 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// HTML escape map for XSS prevention\nconst ESC_MAP = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;'\n};\n\n// Line type constants for state machine\nconst LINE_BLANK = 0;\nconst LINE_HEADING = 1;\nconst LINE_HR = 2;\nconst LINE_FENCE = 3;\nconst LINE_BLOCKQUOTE = 4;\nconst LINE_LIST_UNORDERED = 5;\nconst LINE_LIST_ORDERED = 6;\nconst LINE_TABLE = 7;\nconst LINE_TABLE_SEP = 8;\nconst LINE_TEXT = 9;\n\n// Parser states\nconst STATE_NORMAL = 0;\nconst STATE_FENCE = 1;\nconst STATE_LIST = 2;\nconst STATE_TABLE = 3;\nconst STATE_BLOCKQUOTE = 4;\nconst STATE_PARAGRAPH = 5;\n\n// ===========================================================================\n// MAIN PARSER FUNCTION\n// ===========================================================================\n\nfunction quikdown(markdown, options = {}) {\n // Early return for invalid input\n if (!markdown || typeof markdown !== 'string') return '';\n \n // Parse options with defaults\n const opts = {\n inline_styles: options.inline_styles || false,\n class_prefix: options.class_prefix || 'quikdown-',\n allow_unsafe_urls: options.allow_unsafe_urls || false,\n fence_plugin: options.fence_plugin || null\n };\n \n // Split into lines for processing\n const lines = markdown.split('\\n');\n const output = [];\n \n // Parser state\n let state = STATE_NORMAL;\n let stateData = null; // Holds state-specific data\n \n // Buffers for accumulating content\n let paragraphBuffer = [];\n let blockquoteBuffer = [];\n \n // ===========================================================================\n // HELPER FUNCTIONS\n // ===========================================================================\n \n /**\n * Generate HTML attribute (class or inline style)\n * @param {string} tag - HTML tag name\n * @param {string} extraStyle - Additional inline styles\n * @returns {string} HTML attribute string\n */\n const getAttr = (tag, extraStyle = '') => {\n if (opts.inline_styles) {\n const baseStyle = STYLES[tag] || '';\n const combined = extraStyle \n ? (baseStyle ? `${baseStyle};${extraStyle}` : extraStyle)\n : baseStyle;\n return combined ? ` style=\"${combined}\"` : '';\n }\n return ` class=\"${opts.class_prefix}${tag}\"`;\n };\n \n /**\n * Escape HTML entities to prevent XSS\n * @param {string} str - Input string\n * @returns {string} Escaped string\n */\n const escapeHtml = (str) => {\n return str.replace(/[&<>\"']/g, m => ESC_MAP[m]);\n };\n \n /**\n * Sanitize URLs to prevent XSS attacks\n * @param {string} url - Input URL\n * @returns {string} Sanitized URL or '#' if dangerous\n */\n const sanitizeUrl = (url) => {\n if (!url) return '';\n if (opts.allow_unsafe_urls) return url;\n \n const trimmed = url.trim();\n const lower = trimmed.toLowerCase();\n \n // Block dangerous protocols except data:image\n if (/^(javascript|vbscript|data):/i.test(lower)) {\n if (/^data:image\\//i.test(lower)) return trimmed;\n return '#';\n }\n \n return trimmed;\n };\n \n /**\n * Process inline markdown elements (bold, italic, links, etc.)\n * Single-pass processing with minimal allocations\n * @param {string} text - Input text\n * @returns {string} HTML with inline formatting\n */\n const processInline = (text) => {\n if (!text) return '';\n \n // Step 1: Protect inline code by extracting it\n const codes = [];\n text = text.replace(/`([^`]+)`/g, (_, code) => {\n codes.push(escapeHtml(code));\n return `\\x01${codes.length - 1}\\x02`; // Use control chars as markers\n });\n \n // Step 2: Escape HTML entities\n text = escapeHtml(text);\n \n // Step 3: Process images (must come before links)\n text = text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, (_, alt, src) => {\n return `<img${getAttr('img')} src=\"${sanitizeUrl(src)}\" alt=\"${alt}\">`;\n });\n \n // Step 4: Process links\n text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (_, label, href) => {\n const url = sanitizeUrl(href);\n const isExternal = /^https?:\\/\\//i.test(url);\n const rel = isExternal ? ' rel=\"noopener noreferrer\"' : '';\n return `<a${getAttr('a')} href=\"${url}\"${rel}>${label}</a>`;\n });\n \n // Step 5: Process autolinks\n text = text.replace(/(^|\\s)(https?:\\/\\/[^\\s<]+)/g, (_, prefix, url) => {\n return `${prefix}<a${getAttr('a')} href=\"${sanitizeUrl(url)}\" rel=\"noopener noreferrer\">${url}</a>`;\n });\n \n // Step 6: Process bold (** and __)\n text = text.replace(/\\*\\*(.+?)\\*\\*/g, `<strong${getAttr('strong')}>$1</strong>`);\n text = text.replace(/__(.+?)__/g, `<strong${getAttr('strong')}>$1</strong>`);\n \n // Step 7: Process italic (* and _) - using lookahead/behind\n text = text.replace(/(?<!\\*)\\*(?!\\*)([^*]+)\\*(?!\\*)/g, `<em${getAttr('em')}>$1</em>`);\n text = text.replace(/(?<!_)_(?!_)([^_]+)_(?!_)/g, `<em${getAttr('em')}>$1</em>`);\n \n // Step 8: Process strikethrough\n text = text.replace(/~~(.+?)~~/g, `<del${getAttr('del')}>$1</del>`);\n \n // Step 9: Process line breaks (two spaces at end of line)\n text = text.replace(/ $/gm, `<br${getAttr('br')}>`);\n \n // Step 10: Restore inline code\n text = text.replace(/\\x01(\\d+)\\x02/g, (_, idx) => {\n return `<code${getAttr('code')}>${codes[idx]}</code>`;\n });\n \n return text;\n };\n \n /**\n * Identify line type using optimized checks\n * @param {string} line - Input line\n * @returns {number} Line type constant\n */\n const getLineType = (line) => {\n const trimmed = line.trim();\n \n // Empty line\n if (!trimmed) return LINE_BLANK;\n \n // Use first character for quick discrimination\n const firstChar = trimmed[0];\n \n switch (firstChar) {\n case '#':\n // Heading: # through ######\n if (/^#{1,6}\\s+/.test(trimmed)) return LINE_HEADING;\n break;\n \n case '-':\n case '*':\n case '_':\n // Could be HR or list\n if (/^[-*_](\\s*[-*_]){2,}$/.test(trimmed)) return LINE_HR;\n if (/^[*+-]\\s+/.test(trimmed)) return LINE_LIST_UNORDERED;\n break;\n \n case '+':\n // Unordered list with +\n if (/^\\+\\s+/.test(trimmed)) return LINE_LIST_UNORDERED;\n break;\n \n case '`':\n case '~':\n // Fence marker (3+ backticks or tildes)\n if (/^[`~]{3,}/.test(trimmed)) return LINE_FENCE;\n break;\n \n case '>':\n // Blockquote\n return LINE_BLOCKQUOTE;\n \n case '|':\n // Table (starts with pipe)\n if (/^\\|?\\s*:?-+:?\\s*(\\|\\s*:?-+:?\\s*)*\\|?$/.test(trimmed)) {\n return LINE_TABLE_SEP;\n }\n return LINE_TABLE;\n \n default:\n // Check for ordered list (digit)\n if (/^\\d+\\.\\s+/.test(trimmed)) return LINE_LIST_ORDERED;\n \n // Check for table without leading pipe\n if (trimmed.includes('|')) {\n if (/^\\s*:?-+:?\\s*(\\|\\s*:?-+:?\\s*)+\\s*$/.test(trimmed)) {\n return LINE_TABLE_SEP;\n }\n return LINE_TABLE;\n }\n }\n \n // Check indented list items\n if (/^\\s+[*+-]\\s+/.test(line)) return LINE_LIST_UNORDERED;\n if (/^\\s+\\d+\\.\\s+/.test(line)) return LINE_LIST_ORDERED;\n \n return LINE_TEXT;\n };\n \n /**\n * Flush accumulated paragraph buffer to output\n */\n const flushParagraph = () => {\n if (paragraphBuffer.length > 0) {\n const content = paragraphBuffer.join('\\n');\n output.push(`<p>${processInline(content)}</p>`);\n paragraphBuffer = [];\n }\n };\n \n /**\n * Flush accumulated blockquote buffer to output\n */\n const flushBlockquote = () => {\n if (blockquoteBuffer.length > 0) {\n const innerContent = blockquoteBuffer.join('\\n').trim();\n \n // Check if it's a simple single-line blockquote without block elements\n if (blockquoteBuffer.length === 1 && !innerContent.includes('\\n')) {\n // Simple blockquote - just process inline\n output.push(`<blockquote${getAttr('blockquote')}>${processInline(innerContent)}</blockquote>`);\n } else if (blockquoteBuffer.length === 2 && blockquoteBuffer[1] === '' && !blockquoteBuffer[0].includes('\\n')) {\n // Two lines but second is empty - treat as single line\n output.push(`<blockquote${getAttr('blockquote')}>${processInline(blockquoteBuffer[0])}</blockquote>`);\n } else {\n // Multi-line blockquote - treat all lines as single block\n const lines = blockquoteBuffer.filter(line => line !== '');\n if (lines.length === 0) {\n // All empty lines, skip\n } else if (lines.length === 2 && lines.every(line => !line.includes('\\n') && line.trim().length > 0)) {\n // Two consecutive lines - keep them separate as two blockquotes\n output.push(`<blockquote${getAttr('blockquote')}>${processInline(lines[0])}</blockquote>`);\n output.push(`<blockquote${getAttr('blockquote')}>${processInline(lines[1])}</blockquote>`);\n } else {\n // Complex content - recursively parse\n const innerHtml = quikdown(innerContent, opts);\n output.push(`<blockquote${getAttr('blockquote')}>${innerHtml}</blockquote>`);\n }\n }\n blockquoteBuffer = [];\n }\n };\n \n /**\n * Process a list starting at current position\n * @param {number} startIdx - Starting line index\n * @returns {number} Next line index to process\n */\n const processList = (startIdx) => {\n const listStack = []; // Stack of { type, indent, items }\n let i = startIdx;\n \n while (i < lines.length) {\n const line = lines[i];\n const match = line.match(/^(\\s*)([*+-]|\\d+\\.)\\s+(.+)$/);\n \n if (!match) {\n // Not a list item, end list processing\n break;\n }\n \n const [, spaces, marker, content] = match;\n const indent = Math.floor(spaces.length / 2);\n const isOrdered = /^\\d+\\./.test(marker);\n const listType = isOrdered ? 'ol' : 'ul';\n \n // Process task list syntax\n let itemContent = content;\n let itemAttr = getAttr('li');\n \n if (!isOrdered) {\n const taskMatch = content.match(/^\\[([x ])\\]\\s+(.*)$/i);\n if (taskMatch) {\n const checked = taskMatch[1].toLowerCase() === 'x';\n const checkboxAttr = opts.inline_styles \n ? ' style=\"margin-right:.5em\"' \n : ` class=\"${opts.class_prefix}task-checkbox\"`;\n itemAttr = opts.inline_styles \n ? ' style=\"list-style:none\"' \n : ` class=\"${opts.class_prefix}task-item\"`;\n itemContent = `<input type=\"checkbox\"${checkboxAttr}${checked ? ' checked' : ''} disabled> ${taskMatch[2]}`;\n }\n }\n \n // Manage list stack based on indentation\n while (listStack.length > 0 && indent < listStack[listStack.length - 1].indent) {\n // Close deeper lists\n const closed = listStack.pop();\n closed.items.push(`\\n</${closed.type}>`);\n }\n \n if (listStack.length === 0 || indent > listStack[listStack.length - 1].indent) {\n // Start new list level\n const newList = {\n type: listType,\n indent: indent,\n items: indent > 0 ? [`\\n<${listType}${getAttr(listType)}>`] : [`<${listType}${getAttr(listType)}>`]\n };\n if (indent > 0 && listStack.length > 0) {\n // Nested list - add to parent's items\n listStack[listStack.length - 1].items.push(newList.items[0]);\n newList.items = [];\n }\n listStack.push(newList);\n } else if (listStack[listStack.length - 1].type !== listType) {\n // Different list type at same level - close and open new\n const closed = listStack.pop();\n closed.items.push(`\\n</${closed.type}>`);\n if (listStack.length > 0) {\n listStack[listStack.length - 1].items.push(closed.items.join(''));\n } else {\n output.push(closed.items.join(''));\n }\n const newList = {\n type: listType,\n indent: indent,\n items: [`<${listType}${getAttr(listType)}>`]\n };\n listStack.push(newList);\n }\n \n // Add list item to current list\n listStack[listStack.length - 1].items.push(\n `\\n<li${itemAttr}>${processInline(itemContent)}</li>`\n );\n \n i++;\n }\n \n // Close all open lists\n while (listStack.length > 1) {\n const closed = listStack.pop();\n closed.items.push(`\\n</${closed.type}>`);\n listStack[listStack.length - 1].items.push(closed.items.join(''));\n }\n \n if (listStack.length > 0) {\n const finalList = listStack[0];\n finalList.items.push(`\\n</${finalList.type}>`);\n output.push(finalList.items.join(''));\n }\n \n return i;\n };\n \n /**\n * Process a table starting at current position\n * @param {number} startIdx - Starting line index\n * @returns {number} Next line index to process\n */\n const processTable = (startIdx) => {\n let i = startIdx;\n const headerCells = [];\n let alignments = null;\n const bodyRows = [];\n \n // Parse first row as potential header\n const firstLine = lines[i].trim();\n const firstCells = firstLine.replace(/^\\|/, '').replace(/\\|$/, '').split('|').map(c => c.trim());\n headerCells.push(...firstCells);\n i++;\n \n // Check for separator line - REQUIRED for valid table\n if (i < lines.length) {\n const lineType = getLineType(lines[i]);\n if (lineType === LINE_TABLE_SEP) {\n // Parse alignments from separator\n const sepLine = lines[i].trim();\n const sepCells = sepLine.replace(/^\\|/, '').replace(/\\|$/, '').split('|');\n alignments = sepCells.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 i++;\n } else {\n // No separator, not a valid table - return without processing\n return startIdx;\n }\n } else {\n // End of input without separator - not a valid table\n return startIdx;\n }\n \n // Parse body rows\n while (i < lines.length) {\n const lineType = getLineType(lines[i]);\n if (lineType !== LINE_TABLE && lineType !== LINE_TABLE_SEP) break;\n \n const line = lines[i].trim();\n const cells = line.replace(/^\\|/, '').replace(/\\|$/, '').split('|').map(c => c.trim());\n bodyRows.push(cells);\n i++;\n }\n \n // Generate table HTML\n let html = `<table${getAttr('table')}>`;\n \n // Add header if present\n if (headerCells.length > 0) {\n html += `\\n<thead${getAttr('thead')}>\\n<tr${getAttr('tr')}>\\n`;\n headerCells.forEach((cell, idx) => {\n const align = alignments && alignments[idx] !== 'left' \n ? `text-align:${alignments[idx]}` \n : '';\n html += `<th${getAttr('th', align)}>${processInline(cell)}</th>\\n`;\n });\n html += `</tr>\\n</thead>`;\n }\n \n // Add body rows\n if (bodyRows.length > 0) {\n html += `\\n<tbody${getAttr('tbody')}>\\n`;\n bodyRows.forEach(row => {\n html += `<tr${getAttr('tr')}>\\n`;\n row.forEach((cell, idx) => {\n const align = alignments && alignments[idx] !== 'left' \n ? `text-align:${alignments[idx]}` \n : '';\n html += `<td${getAttr('td', align)}>${processInline(cell)}</td>\\n`;\n });\n html += `</tr>\\n`;\n });\n html += `</tbody>`;\n }\n \n html += `\\n</table>`;\n output.push(html);\n \n return i;\n };\n \n // ===========================================================================\n // MAIN PARSING LOOP - STATE MACHINE\n // ===========================================================================\n \n let i = 0;\n while (i < lines.length) {\n const line = lines[i];\n const lineType = getLineType(line);\n \n // STATE MACHINE - Handle current state and line type\n switch (state) {\n \n // -----------------------------------------------------------------------\n // NORMAL STATE - Can transition to any other state\n // -----------------------------------------------------------------------\n case STATE_NORMAL:\n switch (lineType) {\n case LINE_BLANK:\n // Blank line - just skip\n i++;\n break;\n \n case LINE_HEADING:\n // Parse heading\n const headingMatch = line.trim().match(/^(#{1,6})\\s+(.+?)(?:\\s*#*)?$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n const text = headingMatch[2];\n output.push(`<h${level}${getAttr('h' + level)}>${processInline(text)}</h${level}>`);\n }\n i++;\n break;\n \n case LINE_HR:\n // Horizontal rule\n output.push(`<hr${getAttr('hr')}>`);\n i++;\n break;\n \n case LINE_FENCE:\n // Start fence block\n const fenceMatch = line.trim().match(/^([`~]{3,})(.*)$/);\n if (fenceMatch) {\n state = STATE_FENCE;\n stateData = {\n marker: fenceMatch[1][0],\n count: fenceMatch[1].length,\n lang: (fenceMatch[2] || '').trim(),\n lines: []\n };\n }\n i++;\n break;\n \n case LINE_BLOCKQUOTE:\n // Start blockquote\n state = STATE_BLOCKQUOTE;\n blockquoteBuffer = [line.replace(/^\\s*>\\s?/, '')];\n i++;\n break;\n \n case LINE_LIST_UNORDERED:\n case LINE_LIST_ORDERED:\n // Process entire list\n i = processList(i);\n break;\n \n case LINE_TABLE:\n case LINE_TABLE_SEP:\n // Process entire table\n const newIdx = processTable(i);\n if (newIdx === i) {\n // Not a valid table, treat as text\n state = STATE_PARAGRAPH;\n paragraphBuffer = [line];\n i++;\n } else {\n i = newIdx;\n }\n break;\n \n case LINE_TEXT:\n default:\n // Start paragraph\n state = STATE_PARAGRAPH;\n paragraphBuffer = [line];\n i++;\n break;\n }\n break;\n \n // -----------------------------------------------------------------------\n // FENCE STATE - Inside a code fence\n // -----------------------------------------------------------------------\n case STATE_FENCE:\n // Check for closing fence\n const trimmed = line.trim();\n const closePattern = new RegExp(`^${stateData.marker}{${stateData.count},}\\\\s*$`);\n \n if (closePattern.test(trimmed)) {\n // End fence - output code block\n const code = stateData.lines.join('\\n');\n let output_html = '';\n \n // Try fence plugin first\n if (opts.fence_plugin) {\n output_html = opts.fence_plugin(code, stateData.lang);\n }\n \n // Fall back to default rendering\n if (!output_html || output_html === undefined) {\n const langAttr = !opts.inline_styles && stateData.lang \n ? ` class=\"language-${stateData.lang}\"` \n : '';\n const codeAttr = opts.inline_styles ? getAttr('code') : langAttr;\n output_html = `<pre${getAttr('pre')}><code${codeAttr}>${escapeHtml(code)}</code></pre>`;\n }\n \n output.push(output_html);\n state = STATE_NORMAL;\n stateData = null;\n } else {\n // Continue accumulating fence content\n stateData.lines.push(line);\n }\n i++;\n break;\n \n // -----------------------------------------------------------------------\n // PARAGRAPH STATE - Accumulating paragraph lines\n // -----------------------------------------------------------------------\n case STATE_PARAGRAPH:\n switch (lineType) {\n case LINE_BLANK:\n // End paragraph\n flushParagraph();\n state = STATE_NORMAL;\n i++;\n break;\n \n case LINE_HEADING:\n case LINE_HR:\n case LINE_FENCE:\n case LINE_BLOCKQUOTE:\n case LINE_LIST_UNORDERED:\n case LINE_LIST_ORDERED:\n case LINE_TABLE:\n case LINE_TABLE_SEP:\n // End paragraph and process new block\n flushParagraph();\n state = STATE_NORMAL;\n // Don't increment i - reprocess this line in NORMAL state\n break;\n \n case LINE_TEXT:\n default:\n // Continue paragraph\n paragraphBuffer.push(line);\n i++;\n break;\n }\n break;\n \n // -----------------------------------------------------------------------\n // BLOCKQUOTE STATE - Accumulating blockquote lines\n // -----------------------------------------------------------------------\n case STATE_BLOCKQUOTE:\n if (lineType === LINE_BLOCKQUOTE) {\n // Continue blockquote\n blockquoteBuffer.push(line.replace(/^\\s*>\\s?/, ''));\n i++;\n } else if (lineType === LINE_BLANK && i + 1 < lines.length && \n getLineType(lines[i + 1]) === LINE_BLOCKQUOTE) {\n // Blank line within blockquote\n blockquoteBuffer.push('');\n i++;\n } else {\n // End blockquote\n flushBlockquote();\n state = STATE_NORMAL;\n // Don't increment i - reprocess this line\n }\n break;\n }\n }\n \n // Flush any remaining content\n flushParagraph();\n flushBlockquote();\n \n return output.join('').trim();\n}\n\n// ===========================================================================\n// STATIC METHODS\n// ===========================================================================\n\n/**\n * Emit CSS styles for all quikdown elements\n * @param {string} prefix - Class prefix (default: 'quikdown-')\n * @param {string} theme - Optional theme: 'light' (default) or 'dark'\n * @returns {string} CSS stylesheet\n */\nquikdown.emitStyles = function(prefix = 'quikdown-', theme = 'light') {\n const styles = STYLES;\n \n // Define theme color overrides\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' // Explicit text color for light theme\n }\n };\n \n let css = '';\n for (const [tag, style] of Object.entries(styles)) {\n if (style) {\n let themedStyle = style;\n \n // Apply theme overrides if dark theme\n if (theme === 'dark' && themeOverrides.dark) {\n // Replace colors\n for (const [oldColor, newColor] of Object.entries(themeOverrides.dark)) {\n if (!oldColor.startsWith('_')) {\n themedStyle = themedStyle.replace(new RegExp(oldColor, 'g'), newColor);\n }\n }\n \n // Add text color for certain elements in dark theme\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 // Add explicit text color for light theme elements too\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 \n return css;\n};\n\n/**\n * Create a configured parser function\n * @param {Object} options - Parser options\n * @returns {Function} Configured parser\n */\nquikdown.configure = function(options) {\n return function(markdown) {\n return quikdown(markdown, options);\n };\n};\n\n/**\n * Version string\n */\nquikdown.version = '__QUIKDOWN_VERSION__';\n\n// ===========================================================================\n// EXPORTS\n// ===========================================================================\n\n// CommonJS\nif (typeof module !== 'undefined' && module.exports) {\n module.exports = quikdown;\n}\n\n// Browser global\nif (typeof window !== 'undefined') {\n window.quikdown = quikdown;\n}\n\n// ES6 default export\nexport default quikdown;"],"names":["STYLES","h1","h2","h3","h4","h5","h6","pre","code","blockquote","table","th","td","hr","img","a","strong","em","del","ul","ol","li","ESC_MAP","quikdown","markdown","options","opts","inline_styles","class_prefix","allow_unsafe_urls","fence_plugin","lines","split","output","state","stateData","paragraphBuffer","blockquoteBuffer","getAttr","tag","extraStyle","baseStyle","combined","escapeHtml","str","replace","m","sanitizeUrl","url","trimmed","trim","lower","toLowerCase","test","processInline","text","codes","_","push","length","alt","src","label","href","rel","prefix","idx","getLineType","line","includes","flushParagraph","content","join","flushBlockquote","innerContent","filter","every","innerHtml","processList","startIdx","listStack","i","match","spaces","marker","indent","Math","floor","isOrdered","listType","itemContent","itemAttr","taskMatch","checked","checkboxAttr","closed","pop","items","type","newList","finalList","processTable","headerCells","alignments","bodyRows","firstCells","map","c","sepCells","cell","startsWith","endsWith","lineType","cells","html","forEach","align","row","headingMatch","level","fenceMatch","count","lang","newIdx","RegExp","output_html","undefined","langAttr","codeAttr","emitStyles","theme","styles","themeOverrides","_textColor","css","style","Object","entries","themedStyle","oldColor","newColor","configure","version","module","exports","window"],"mappings":"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"}
package/dist/quikdown.cjs CHANGED
@@ -1,16 +1,11 @@
1
1
  /**
2
2
  * quikdown - Lightweight Markdown Parser
3
- * @version 1.0.2
3
+ * @version 1.0.3
4
4
  * @license BSD-2-Clause
5
5
  * @copyright DeftIO 2025
6
6
  */
7
7
  'use strict';
8
8
 
9
- // Auto-generated version file - DO NOT EDIT MANUALLY
10
- // This file is automatically updated by tools/updateVersion.js
11
-
12
- const quikdownVersion = "1.0.2";
13
-
14
9
  /**
15
10
  * quikdown - A minimal markdown parser optimized for chat/LLM output
16
11
  * Supports tables, code blocks, lists, and common formatting
@@ -22,64 +17,71 @@ const quikdownVersion = "1.0.2";
22
17
  * @returns {string} - The rendered HTML
23
18
  */
24
19
 
20
+ // Version will be injected at build time
21
+ const quikdownVersion = '1.0.3';
25
22
 
26
- function quikdown(markdown, options = {}) {
27
- if (!markdown || typeof markdown !== 'string') {
28
- return '';
29
- }
30
-
31
- const { fence_plugin, inline_styles = false } = options;
23
+ // Constants for reuse
24
+ const CLASS_PREFIX = 'quikdown-';
25
+ const PLACEHOLDER_CB = '§CB';
26
+ const PLACEHOLDER_IC = '§IC';
32
27
 
33
- // Style definitions - minimal, matching emitStyles
34
- const styles = {
35
- h1: 'font-size: 2em; font-weight: 600; margin: 0.67em 0; text-align: left',
36
- h2: 'font-size: 1.5em; font-weight: 600; margin: 0.83em 0',
37
- h3: 'font-size: 1.25em; font-weight: 600; margin: 1em 0',
38
- h4: 'font-size: 1em; font-weight: 600; margin: 1.33em 0',
39
- h5: 'font-size: 0.875em; font-weight: 600; margin: 1.67em 0',
40
- h6: 'font-size: 0.85em; font-weight: 600; margin: 2em 0',
41
- pre: 'background: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; margin: 1em 0',
42
- code: 'background: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-family: monospace',
43
- blockquote: 'border-left: 4px solid #ddd; margin-left: 0; padding-left: 1em',
44
- table: 'border-collapse: collapse; width: 100%; margin: 1em 0',
45
- thead: '',
46
- tbody: '',
47
- tr: '',
48
- th: 'border: 1px solid #ddd; padding: 8px; background-color: #f2f2f2; font-weight: bold; text-align: left',
49
- td: 'border: 1px solid #ddd; padding: 8px; text-align: left',
50
- hr: 'border: none; border-top: 1px solid #ddd; margin: 1em 0',
51
- img: 'max-width: 100%; height: auto',
52
- a: 'color: #0066cc; text-decoration: underline',
53
- strong: 'font-weight: bold',
54
- em: 'font-style: italic',
55
- del: 'text-decoration: line-through',
56
- ul: 'margin: 0.5em 0; padding-left: 2em',
57
- ol: 'margin: 0.5em 0; padding-left: 2em',
58
- li: 'margin: 0.25em 0',
59
- br: ''
60
- };
28
+ // Escape map at module level
29
+ const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
61
30
 
62
- // Helper to get class or style attribute
63
- function getAttr(tag, additionalStyle = '') {
31
+ // Single source of truth for all style definitions - optimized
32
+ const QUIKDOWN_STYLES = {
33
+ h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',
34
+ h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',
35
+ h3: 'font-size:1.25em;font-weight:600;margin:1em 0',
36
+ h4: 'font-size:1em;font-weight:600;margin:1.33em 0',
37
+ h5: 'font-size:.875em;font-weight:600;margin:1.67em 0',
38
+ h6: 'font-size:.85em;font-weight:600;margin:2em 0',
39
+ pre: 'background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0',
40
+ code: 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace',
41
+ blockquote: 'border-left:4px solid #ddd;margin-left:0;padding-left:1em',
42
+ table: 'border-collapse:collapse;width:100%;margin:1em 0',
43
+ th: 'border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left',
44
+ td: 'border:1px solid #ddd;padding:8px;text-align:left',
45
+ hr: 'border:none;border-top:1px solid #ddd;margin:1em 0',
46
+ img: 'max-width:100%;height:auto',
47
+ a: 'color:#06c;text-decoration:underline',
48
+ strong: 'font-weight:bold',
49
+ em: 'font-style:italic',
50
+ del: 'text-decoration:line-through',
51
+ ul: 'margin:.5em 0;padding-left:2em',
52
+ ol: 'margin:.5em 0;padding-left:2em',
53
+ li: 'margin:.25em 0',
54
+ // Task list specific styles
55
+ 'task-item': 'list-style:none',
56
+ 'task-checkbox': 'margin-right:.5em'
57
+ };
58
+
59
+ // Factory function to create getAttr for a given context
60
+ function createGetAttr(inline_styles, styles) {
61
+ return function(tag, additionalStyle = '') {
64
62
  if (inline_styles) {
65
- const style = styles[tag] || '';
66
- const fullStyle = additionalStyle ? `${style}; ${additionalStyle}` : style;
67
- return fullStyle ? ` style="${fullStyle}"` : '';
63
+ const style = styles[tag];
64
+ if (!style && !additionalStyle) return '';
65
+ const fullStyle = additionalStyle ? (style ? `${style};${additionalStyle}` : additionalStyle) : style;
66
+ return ` style="${fullStyle}"`;
68
67
  } else {
69
- return ` class="quikdown-${tag}"`;
68
+ return ` class="${CLASS_PREFIX}${tag}"`;
70
69
  }
70
+ };
71
+ }
72
+
73
+ function quikdown(markdown, options = {}) {
74
+ if (!markdown || typeof markdown !== 'string') {
75
+ return '';
71
76
  }
77
+
78
+ const { fence_plugin, inline_styles = false } = options;
79
+ const styles = QUIKDOWN_STYLES; // Use module-level styles
80
+ const getAttr = createGetAttr(inline_styles, styles); // Create getAttr once
72
81
 
73
82
  // Escape HTML entities to prevent XSS
74
83
  function escapeHtml(text) {
75
- const map = {
76
- '&': '&amp;',
77
- '<': '&lt;',
78
- '>': '&gt;',
79
- '"': '&quot;',
80
- "'": '&#39;'
81
- };
82
- return text.replace(/[&<>"']/g, m => map[m]);
84
+ return text.replace(/[&<>"']/g, m => ESC_MAP[m]);
83
85
  }
84
86
 
85
87
  // Sanitize URLs to prevent XSS attacks
@@ -89,7 +91,6 @@ function quikdown(markdown, options = {}) {
89
91
  // If unsafe URLs are explicitly allowed, return as-is
90
92
  if (allowUnsafe) return url;
91
93
 
92
- // Trim and lowercase for checking
93
94
  const trimmedUrl = url.trim();
94
95
  const lowerUrl = trimmedUrl.toLowerCase();
95
96
 
@@ -121,7 +122,7 @@ function quikdown(markdown, options = {}) {
121
122
  // Match paired fences - ``` with ``` and ~~~ with ~~~
122
123
  // Fence must be at start of line
123
124
  html = html.replace(/^(```|~~~)([^\n]*)\n([\s\S]*?)^\1$/gm, (match, fence, lang, code) => {
124
- const placeholder = `%%%CODEBLOCK${codeBlocks.length}%%%`;
125
+ const placeholder = `${PLACEHOLDER_CB}${codeBlocks.length}§`;
125
126
 
126
127
  // Trim the language specification
127
128
  const langTrimmed = lang ? lang.trim() : '';
@@ -145,7 +146,7 @@ function quikdown(markdown, options = {}) {
145
146
 
146
147
  // Extract inline code
147
148
  html = html.replace(/`([^`]+)`/g, (match, code) => {
148
- const placeholder = `%%%INLINECODE${inlineCodes.length}%%%`;
149
+ const placeholder = `${PLACEHOLDER_IC}${inlineCodes.length}§`;
149
150
  inlineCodes.push(escapeHtml(code));
150
151
  return placeholder;
151
152
  });
@@ -156,7 +157,7 @@ function quikdown(markdown, options = {}) {
156
157
  // Phase 2: Process block elements
157
158
 
158
159
  // Process tables
159
- html = processTable(html, inline_styles, styles);
160
+ html = processTable(html, getAttr);
160
161
 
161
162
  // Process headings (supports optional trailing #'s)
162
163
  html = html.replace(/^(#{1,6})\s+(.+?)\s*#*$/gm, (match, hashes, content) => {
@@ -173,7 +174,7 @@ function quikdown(markdown, options = {}) {
173
174
  html = html.replace(/^---+$/gm, `<hr${getAttr('hr')}>`);
174
175
 
175
176
  // Process lists
176
- html = processLists(html, inline_styles, styles);
177
+ html = processLists(html, getAttr, inline_styles);
177
178
 
178
179
  // Phase 3: Process inline elements
179
180
 
@@ -198,16 +199,18 @@ function quikdown(markdown, options = {}) {
198
199
  return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
199
200
  });
200
201
 
201
- // Bold (must use non-greedy matching)
202
- html = html.replace(/\*\*(.+?)\*\*/g, `<strong${getAttr('strong')}>$1</strong>`);
203
- html = html.replace(/__(.+?)__/g, `<strong${getAttr('strong')}>$1</strong>`);
204
-
205
- // Italic (must not match bold markers)
206
- html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, `<em${getAttr('em')}>$1</em>`);
207
- html = html.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, `<em${getAttr('em')}>$1</em>`);
208
-
209
- // Strikethrough
210
- html = html.replace(/~~(.+?)~~/g, `<del${getAttr('del')}>$1</del>`);
202
+ // Process inline formatting (bold, italic, strikethrough)
203
+ const inlinePatterns = [
204
+ [/\*\*(.+?)\*\*/g, 'strong'],
205
+ [/__(.+?)__/g, 'strong'],
206
+ [/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, 'em'],
207
+ [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em'],
208
+ [/~~(.+?)~~/g, 'del']
209
+ ];
210
+
211
+ inlinePatterns.forEach(([pattern, tag]) => {
212
+ html = html.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);
213
+ });
211
214
 
212
215
  // Line breaks (two spaces at end of line)
213
216
  html = html.replace(/ $/gm, `<br${getAttr('br')}>`);
@@ -216,21 +219,26 @@ function quikdown(markdown, options = {}) {
216
219
  html = html.replace(/\n\n+/g, '</p><p>');
217
220
  html = '<p>' + html + '</p>';
218
221
 
219
- // Clean up empty paragraphs and unwrap block elements (account for attributes)
220
- html = html.replace(/<p><\/p>/g, '');
221
- html = html.replace(/<p>(<h[1-6][^>]*>)/g, '$1');
222
- html = html.replace(/(<\/h[1-6]>)<\/p>/g, '$1');
223
- html = html.replace(/<p>(<blockquote[^>]*>)/g, '$1');
224
- html = html.replace(/(<\/blockquote>)<\/p>/g, '$1');
225
- html = html.replace(/<p>(<ul[^>]*>|<ol[^>]*>)/g, '$1');
226
- html = html.replace(/(<\/ul>|<\/ol>)<\/p>/g, '$1');
227
- html = html.replace(/<p>(<hr[^>]*>)<\/p>/g, '$1');
228
- html = html.replace(/<p>(<table[^>]*>)/g, '$1');
229
- html = html.replace(/(<\/table>)<\/p>/g, '$1');
230
- html = html.replace(/<p>(<pre[^>]*>)/g, '$1');
231
- html = html.replace(/(<\/pre>)<\/p>/g, '$1');
232
- // Also unwrap code block placeholders
233
- html = html.replace(/<p>(%%%CODEBLOCK\d+%%%)<\/p>/g, '$1');
222
+ // Clean up empty paragraphs and unwrap block elements
223
+ const cleanupPatterns = [
224
+ [/<p><\/p>/g, ''],
225
+ [/<p>(<h[1-6][^>]*>)/g, '$1'],
226
+ [/(<\/h[1-6]>)<\/p>/g, '$1'],
227
+ [/<p>(<blockquote[^>]*>)/g, '$1'],
228
+ [/(<\/blockquote>)<\/p>/g, '$1'],
229
+ [/<p>(<ul[^>]*>|<ol[^>]*>)/g, '$1'],
230
+ [/(<\/ul>|<\/ol>)<\/p>/g, '$1'],
231
+ [/<p>(<hr[^>]*>)<\/p>/g, '$1'],
232
+ [/<p>(<table[^>]*>)/g, '$1'],
233
+ [/(<\/table>)<\/p>/g, '$1'],
234
+ [/<p>(<pre[^>]*>)/g, '$1'],
235
+ [/(<\/pre>)<\/p>/g, '$1'],
236
+ [new RegExp(`<p>(${PLACEHOLDER_CB}\\d)<\/p>`, 'g'), '$1']
237
+ ];
238
+
239
+ cleanupPatterns.forEach(([pattern, replacement]) => {
240
+ html = html.replace(pattern, replacement);
241
+ });
234
242
 
235
243
  // Phase 4: Restore code blocks and inline code
236
244
 
@@ -254,13 +262,13 @@ function quikdown(markdown, options = {}) {
254
262
  replacement = `<pre${getAttr('pre')}><code${codeAttr}>${block.code}</code></pre>`;
255
263
  }
256
264
 
257
- const placeholder = `%%%CODEBLOCK${i}%%%`;
265
+ const placeholder = `${PLACEHOLDER_CB}${i}§`;
258
266
  html = html.replace(placeholder, replacement);
259
267
  });
260
268
 
261
269
  // Restore inline code
262
270
  inlineCodes.forEach((code, i) => {
263
- const placeholder = `%%%INLINECODE${i}%%%`;
271
+ const placeholder = `${PLACEHOLDER_IC}${i}§`;
264
272
  html = html.replace(placeholder, `<code${getAttr('code')}>${code}</code>`);
265
273
  });
266
274
 
@@ -270,31 +278,21 @@ function quikdown(markdown, options = {}) {
270
278
  /**
271
279
  * Process inline markdown formatting
272
280
  */
273
- function processInlineMarkdown(text, inline_styles, styles) {
274
- // Helper to get attributes
275
- function getAttr(tag, additionalStyle = '') {
276
- if (inline_styles) {
277
- const style = styles[tag] || '';
278
- const fullStyle = additionalStyle ? `${style}; ${additionalStyle}` : style;
279
- return fullStyle ? ` style="${fullStyle}"` : '';
280
- } else {
281
- return ` class="quikdown-${tag}"`;
282
- }
283
- }
284
-
285
- // Process bold
286
- text = text.replace(/\*\*(.+?)\*\*/g, `<strong${getAttr('strong')}>$1</strong>`);
287
- text = text.replace(/__(.+?)__/g, `<strong${getAttr('strong')}>$1</strong>`);
288
-
289
- // Process italic (must not match bold markers)
290
- text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, `<em${getAttr('em')}>$1</em>`);
291
- text = text.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, `<em${getAttr('em')}>$1</em>`);
292
-
293
- // Process strikethrough
294
- text = text.replace(/~~(.+?)~~/g, `<del${getAttr('del')}>$1</del>`);
295
-
296
- // Process inline code
297
- text = text.replace(/`([^`]+)`/g, `<code${getAttr('code')}>$1</code>`);
281
+ function processInlineMarkdown(text, getAttr) {
282
+
283
+ // Process inline formatting patterns
284
+ const patterns = [
285
+ [/\*\*(.+?)\*\*/g, 'strong'],
286
+ [/__(.+?)__/g, 'strong'],
287
+ [/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, 'em'],
288
+ [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em'],
289
+ [/~~(.+?)~~/g, 'del'],
290
+ [/`([^`]+)`/g, 'code']
291
+ ];
292
+
293
+ patterns.forEach(([pattern, tag]) => {
294
+ text = text.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);
295
+ });
298
296
 
299
297
  return text;
300
298
  }
@@ -302,7 +300,7 @@ function processInlineMarkdown(text, inline_styles, styles) {
302
300
  /**
303
301
  * Process markdown tables
304
302
  */
305
- function processTable(text, inline_styles, styles) {
303
+ function processTable(text, getAttr) {
306
304
  const lines = text.split('\n');
307
305
  const result = [];
308
306
  let inTable = false;
@@ -322,7 +320,7 @@ function processTable(text, inline_styles, styles) {
322
320
  // Not a table line
323
321
  if (inTable) {
324
322
  // Process the accumulated table
325
- const tableHtml = buildTable(tableLines, inline_styles, styles);
323
+ const tableHtml = buildTable(tableLines, getAttr);
326
324
  if (tableHtml) {
327
325
  result.push(tableHtml);
328
326
  } else {
@@ -338,7 +336,7 @@ function processTable(text, inline_styles, styles) {
338
336
 
339
337
  // Handle table at end of text
340
338
  if (inTable && tableLines.length > 0) {
341
- const tableHtml = buildTable(tableLines, inline_styles, styles);
339
+ const tableHtml = buildTable(tableLines, getAttr);
342
340
  if (tableHtml) {
343
341
  result.push(tableHtml);
344
342
  } else {
@@ -352,17 +350,7 @@ function processTable(text, inline_styles, styles) {
352
350
  /**
353
351
  * Build an HTML table from markdown table lines
354
352
  */
355
- function buildTable(lines, inline_styles, styles) {
356
- // Helper to get attributes
357
- function getAttr(tag, additionalStyle = '') {
358
- if (inline_styles) {
359
- const style = styles[tag] || '';
360
- const fullStyle = additionalStyle ? `${style}; ${additionalStyle}` : style;
361
- return fullStyle ? ` style="${fullStyle}"` : '';
362
- } else {
363
- return ` class="quikdown-${tag}"`;
364
- }
365
- }
353
+ function buildTable(lines, getAttr) {
366
354
 
367
355
  if (lines.length < 2) return null;
368
356
 
@@ -402,8 +390,8 @@ function buildTable(lines, inline_styles, styles) {
402
390
  // Handle pipes at start/end or not
403
391
  const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
404
392
  cells.forEach((cell, i) => {
405
- const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align: ${alignments[i]}` : '';
406
- const processedCell = processInlineMarkdown(cell.trim(), inline_styles, styles);
393
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
394
+ const processedCell = processInlineMarkdown(cell.trim(), getAttr);
407
395
  html += `<th${getAttr('th', alignStyle)}>${processedCell}</th>\n`;
408
396
  });
409
397
  html += '</tr>\n';
@@ -419,8 +407,8 @@ function buildTable(lines, inline_styles, styles) {
419
407
  // Handle pipes at start/end or not
420
408
  const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
421
409
  cells.forEach((cell, i) => {
422
- const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align: ${alignments[i]}` : '';
423
- const processedCell = processInlineMarkdown(cell.trim(), inline_styles, styles);
410
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
411
+ const processedCell = processInlineMarkdown(cell.trim(), getAttr);
424
412
  html += `<td${getAttr('td', alignStyle)}>${processedCell}</td>\n`;
425
413
  });
426
414
  html += '</tr>\n';
@@ -435,17 +423,7 @@ function buildTable(lines, inline_styles, styles) {
435
423
  /**
436
424
  * Process markdown lists (ordered and unordered)
437
425
  */
438
- function processLists(text, inline_styles, styles) {
439
- // Helper to get attributes
440
- function getAttr(tag, additionalStyle = '') {
441
- if (inline_styles) {
442
- const style = styles[tag] || '';
443
- const fullStyle = additionalStyle ? `${style}; ${additionalStyle}` : style;
444
- return fullStyle ? ` style="${fullStyle}"` : '';
445
- } else {
446
- return ` class="quikdown-${tag}"`;
447
- }
448
- }
426
+ function processLists(text, getAttr, inline_styles) {
449
427
 
450
428
  const lines = text.split('\n');
451
429
  const result = [];
@@ -469,10 +447,10 @@ function processLists(text, inline_styles, styles) {
469
447
  const [, checked, taskContent] = taskMatch;
470
448
  const isChecked = checked.toLowerCase() === 'x';
471
449
  const checkboxAttr = inline_styles
472
- ? ' style="margin-right: 0.5em"'
473
- : ' class="quikdown-task-checkbox"';
450
+ ? ' style="margin-right:.5em"'
451
+ : ` class="${CLASS_PREFIX}task-checkbox"`;
474
452
  listItemContent = `<input type="checkbox"${checkboxAttr}${isChecked ? ' checked' : ''} disabled> ${taskContent}`;
475
- taskListClass = inline_styles ? ' style="list-style: none"' : ' class="quikdown-task-item"';
453
+ taskListClass = inline_styles ? ' style="list-style:none"' : ` class="${CLASS_PREFIX}task-item"`;
476
454
  }
477
455
 
478
456
  // Close deeper levels
@@ -520,39 +498,56 @@ function processLists(text, inline_styles, styles) {
520
498
 
521
499
  /**
522
500
  * Emit CSS styles for quikdown elements
501
+ * @param {string} prefix - Optional class prefix (default: 'quikdown-')
502
+ * @param {string} theme - Optional theme: 'light' (default) or 'dark'
523
503
  * @returns {string} CSS string with quikdown styles
524
504
  */
525
- quikdown.emitStyles = function() {
526
- const styles = {
527
- h1: 'font-size: 2em; font-weight: 600; margin: 0.67em 0; text-align: left',
528
- h2: 'font-size: 1.5em; font-weight: 600; margin: 0.83em 0',
529
- h3: 'font-size: 1.25em; font-weight: 600; margin: 1em 0',
530
- h4: 'font-size: 1em; font-weight: 600; margin: 1.33em 0',
531
- h5: 'font-size: 0.875em; font-weight: 600; margin: 1.67em 0',
532
- h6: 'font-size: 0.85em; font-weight: 600; margin: 2em 0',
533
- pre: 'background: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; margin: 1em 0',
534
- code: 'background: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-family: monospace',
535
- blockquote: 'border-left: 4px solid #ddd; margin-left: 0; padding-left: 1em',
536
- table: 'border-collapse: collapse; width: 100%; margin: 1em 0',
537
- th: 'border: 1px solid #ddd; padding: 8px; background-color: #f2f2f2; font-weight: bold; text-align: left',
538
- td: 'border: 1px solid #ddd; padding: 8px; text-align: left',
539
- hr: 'border: none; border-top: 1px solid #ddd; margin: 1em 0',
540
- img: 'max-width: 100%; height: auto',
541
- a: 'color: #0066cc; text-decoration: underline',
542
- strong: 'font-weight: bold',
543
- em: 'font-style: italic',
544
- del: 'text-decoration: line-through',
545
- ul: 'margin: 0.5em 0; padding-left: 2em',
546
- ol: 'margin: 0.5em 0; padding-left: 2em',
547
- li: 'margin: 0.25em 0',
548
- 'task-item': 'list-style: none',
549
- 'task-checkbox': 'margin-right: 0.5em'
505
+ quikdown.emitStyles = function(prefix = 'quikdown-', theme = 'light') {
506
+ const styles = QUIKDOWN_STYLES;
507
+
508
+ // Define theme color overrides
509
+ const themeOverrides = {
510
+ dark: {
511
+ '#f4f4f4': '#2a2a2a', // pre background
512
+ '#f0f0f0': '#2a2a2a', // code background
513
+ '#f2f2f2': '#2a2a2a', // th background
514
+ '#ddd': '#3a3a3a', // borders
515
+ '#06c': '#6db3f2', // links
516
+ _textColor: '#e0e0e0'
517
+ },
518
+ light: {
519
+ _textColor: '#333' // Explicit text color for light theme
520
+ }
550
521
  };
551
522
 
552
523
  let css = '';
553
524
  for (const [tag, style] of Object.entries(styles)) {
554
525
  if (style) {
555
- css += `.quikdown-${tag} { ${style} }\n`;
526
+ let themedStyle = style;
527
+
528
+ // Apply theme overrides if dark theme
529
+ if (theme === 'dark' && themeOverrides.dark) {
530
+ // Replace colors
531
+ for (const [oldColor, newColor] of Object.entries(themeOverrides.dark)) {
532
+ if (!oldColor.startsWith('_')) {
533
+ themedStyle = themedStyle.replace(new RegExp(oldColor, 'g'), newColor);
534
+ }
535
+ }
536
+
537
+ // Add text color for certain elements in dark theme
538
+ const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
539
+ if (needsTextColor.includes(tag)) {
540
+ themedStyle += `;color:${themeOverrides.dark._textColor}`;
541
+ }
542
+ } else if (theme === 'light' && themeOverrides.light) {
543
+ // Add explicit text color for light theme elements too
544
+ const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
545
+ if (needsTextColor.includes(tag)) {
546
+ themedStyle += `;color:${themeOverrides.light._textColor}`;
547
+ }
548
+ }
549
+
550
+ css += `.${prefix}${tag} { ${themedStyle} }\n`;
556
551
  }
557
552
  }
558
553