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(``);
});
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}>`; list = null; } };
const closeCode = () => {
const body = escapeHtml(code.join("\n"));
html += language.toLowerCase() === "mermaid"
? `
${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 += `| ${inline(cell)} | `); html += `
|---|
| ${inline(cells[i] || "")} | `); html += `
", "", lineOffset)}>${inline(line.slice(2))}`; else if (line.trim()) html += `
${inline(line)}
`; else html += `