function escapeHtml(value) { return String(value).replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); } function safeUrl(value) { const url = String(value).trim(); if (/^(https?:\/\/|mailto:|\/|\.\/|\.\.\/|#)/i.test(url)) return escapeHtml(url); return "#"; } const emoji = { smile:"😄", joy:"😂", heart:"❤️", thumbs_up:"👍", thumbsup:"👍", thumbs_down:"👎", fire:"🔥", rocket:"🚀", tada:"🎉", warning:"⚠️", white_check_mark:"✅", x:"❌", eyes:"👀", bulb:"💡", memo:"📝", pushpin:"📌", bug:"🐛", sparkles:"✨", thinking:"🤔", clap:"👏", ok_hand:"👌", pray:"🙏", muscle:"💪", coffee:"☕", tent:"⛺", star:"⭐", checkered_flag:"🏁" }; function inline(value) { const tokens = []; const stash = html => { const token = `\u0000T${tokens.length}\u0000`; tokens.push(html); return token; }; let html = escapeHtml(value); html = html.replace(/`([^`]+)`/g, (_, code) => stash(`${code}`)); html = html.replace(/!\[([^\]]*)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, alt, url, title) => { const titleAttr = title ? ` title="${escapeHtml(title)}"` : ""; return stash(`${alt}`); }); html = html.replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, label, url, title) => { const titleAttr = title ? ` title="${escapeHtml(title)}"` : ""; return stash(`${label}`); }); html = html.replace(/\[\^([^\]\s]+)\]/g, (_, id) => stash(`?`)); html = html .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/~~([^~]+)~~/g, "$1") .replace(/==([^=]+)==/g, "$1") .replace(/\*([^*]+)\*/g, "$1") .replace(/(?$1") .replace(/\^([^^\n]+)\^/g, "$1") .replace(/:([a-z0-9_+-]+):/gi, (match, name) => emoji[name] || match); html = html.replace(/(^|[\s(])((?:https?:\/\/|mailto:)[^\s<]+)/gi, (match, prefix, url) => { const clean = url.replace(/[.,!?;:]+$/, ""); const suffix = url.slice(clean.length); return `${prefix}${stash(`${clean}`)}${suffix}`; }); return html.replace(/\u0000T(\d+)\u0000/g, (_, index) => tokens[Number(index)] || ""); } const attrs = (line, editable = false, prefix = "", suffix = "", lineOffset = 0) => ` class="preview-source-line${editable ? " preview-editable" : ""}" data-source-line="${line + lineOffset + 1}"${editable ? ` contenteditable="true" spellcheck="true" data-source-prefix="${escapeHtml(prefix)}" data-source-suffix="${escapeHtml(suffix)}"` : ""}`; const isPlainText = line => !/[`*_~^=\[\]<>|:#]/.test(line) && !/^\s*(?:[-+*>]|\d+\.)\s/.test(line); function splitTableRow(line) { let value = line.trim(); if (value.startsWith("|")) value = value.slice(1); if (value.endsWith("|")) value = value.slice(0, -1); return value.split("|").map(cell => cell.trim().replace(/|/g, "|")); } function tableDelimiter(line) { const cells = splitTableRow(line); if (!cells.length || !cells.every(cell => /^:?-{3,}:?$/.test(cell))) return null; return cells.map(cell => cell.startsWith(":") && cell.endsWith(":") ? "center" : cell.endsWith(":") ? "right" : "left"); } export function renderMarkdown(source, lineOffset = 0) { let html = "", inCode = false, fence = "", language = "", code = [], codeStart = 0, list = null; const lines = String(source).split("\n"); const footnotes = new Map(); for (let i = 0; i < lines.length; i++) { const match = lines[i].match(/^\[\^([^\]\s]+)\]:\s*(.*)$/); if (!match) continue; const body = [match[2]]; let j = i + 1; while (j < lines.length && /^(?: {4}|\t)/.test(lines[j])) { body.push(lines[j].replace(/^(?: {4}|\t)/, "")); lines[j] = ""; j++; } footnotes.set(match[1], body.join("\n")); lines[i] = ""; } const closeList = () => { if (list) { html += ``; list = null; } }; const closeCode = () => { const body = escapeHtml(code.join("\n")); html += language.toLowerCase() === "mermaid" ? `
${body}
` : `${body}`; code = []; language = ""; fence = ""; }; for (let index = 0; index < lines.length; index++) { const line = lines[index]; const fenceMatch = line.match(/^\s*(```+|~~~+)\s*([^\s]*)\s*$/); if (fenceMatch) { closeList(); if (inCode && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) closeCode(); else if (!inCode) { fence = fenceMatch[1]; language = fenceMatch[2] || ""; codeStart = index; } inCode = !inCode; continue; } if (inCode) { code.push(line); continue; } const delimiter = index + 1 < lines.length ? tableDelimiter(lines[index + 1]) : null; if (line.includes("|") && delimiter) { closeList(); const headers = splitTableRow(line); html += `
`; headers.forEach((cell, i) => html += ``); html += ``; index += 2; while (index < lines.length && lines[index].includes("|") && lines[index].trim()) { const cells = splitTableRow(lines[index]); html += ``; headers.forEach((_, i) => html += ``); html += ``; index++; } html += `
${inline(cell)}
${inline(cells[i] || "")}
`; index--; continue; } if (/^
\s*$/i.test(line.trim())) { closeList(); let end = index + 1; while (end < lines.length && !/^<\/details>\s*$/i.test(lines[end].trim())) end++; if (end < lines.length) { const block = lines.slice(index + 1, end); let summary = "Details"; while (block.length && !block[0].trim()) block.shift(); if (block.length) { const summaryMatch = block[0].trim().match(/^([\s\S]*?)<\/summary>$/i); if (summaryMatch) { summary = summaryMatch[1].trim() || "Details"; block.shift(); } } while (block.length && !block[0].trim()) block.shift(); html += `
${inline(summary)}
${renderMarkdown(block.join("\n"), lineOffset + index + 1)}
`; index = end; continue; } } const heading = line.match(/^\s{0,4}(#{1,6})\s+(.+?)(?:\s+\{#([A-Za-z][\w:.-]*)\})?\s*$/); const task = line.match(/^(\s*)[-*+]\s+\[([ xX])\]\s+(.+)$/); const ul = line.match(/^(\s*)[-*+]\s+(.+)$/); const ol = line.match(/^(\s*)\d+\.\s+(.+)$/); if (heading) { closeList(); const n = heading[1].length; const id = heading[3] ? ` id="${escapeHtml(heading[3])}"` : ""; const suffix = heading[3] ? ` {#${heading[3]}}` : ""; html += `${inline(heading[2])}`; } else if (task) { if (list !== "ul") { closeList(); html += `
    `; list = "ul"; } const checked = task[2].toLowerCase() === "x"; html += `
  • ${inline(task[3])}
  • `; } else if (ul || ol) { const type = ul ? "ul" : "ol"; if (list !== type) { closeList(); html += `<${type}>`; list = type; } const match = ul || ol; const indentWidth = match[1].replace(/\t/g, " ").length; const prefix = ul ? `${match[1]}- ` : `${match[1]}${(line.match(/^\s*(\d+)\./)||[])[1] || 1}. `; const indentStyle = indentWidth ? ` style="margin-left:${Math.min(indentWidth, 24) * 0.45}em"` : ""; html += `${inline(match[2])}`; } else { closeList(); const definition = index + 1 < lines.length && /^:\s+/.test(lines[index + 1]); if (line.trim() && definition) { html += `
    ${inline(line)}
    `; while (index + 1 < lines.length && /^:\s+/.test(lines[index + 1])) { index++; html += `
    ${inline(lines[index].replace(/^:\s+/, ""))}
    `; } html += ``; } else if (/^---+$/.test(line.trim())) html += ``; else if (line.startsWith("> ")) html += ` ", "", lineOffset)}>${inline(line.slice(2))}`; else if (line.trim()) html += `${inline(line)}

    `; else html += `
    `; } } closeList(); if (inCode) closeCode(); if (footnotes.size) { html = html.replace(/\?<\/a><\/sup>/g, (_, id) => { const order = [...footnotes.keys()].indexOf(id) + 1; return `${order || "?"}`; }); html += `

      `; for (const [id, body] of footnotes) { html += `
    1. ${body.split("\n").map(part => inline(part)).join("
      ")}
    2. `; } html += `
    `; } return html; }