const ESC = { "&": "&", "<": "<", ">": ">", '"': """ }; const SEP = /^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/; const BLOCK = /^(#{1,6} |```|\$\$|- |\d+\. |> |\|)/; export const escape = (s) => String(s).replace(/[&<>"]/g, (c) => ESC[c]); export function slug(text) { return text.toLowerCase().replace(/[^\p{L}\p{N} _-]/gu, "").replace(/ /g, "-"); } const TEX = { lceil: "\u2308", rceil: "\u2309", lfloor: "\u230a", rfloor: "\u230b", ne: "\u2260", neq: "\u2260", le: "\u2264", leq: "\u2264", ge: "\u2265", geq: "\u2265", times: "\u00d7", infty: "\u221e", pi: "\u03c0", cdot: "\u00b7", to: "\u2192", left: "", right: "" }; export function plain(s) { return s .replace(/\\([a-zA-Z]+)\s?/g, (m, name) => (name in TEX ? TEX[name] : m)) .replace(/\s+([\u2309\u230b])/g, "$1") .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1") .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") .replace(/`([^`]*)`/g, "$1") .replace(/\*\*([^*]*)\*\*/g, "$1") .replace(/\*([^*]*)\*/g, "$1") .replace(/\$\$?([^$]*)\$\$?/g, "$1") .replace(/\s+/g, " ") .trim(); } export function title(md) { const m = md.match(/^# (.+)$/m); return m ? plain(m[1]) : ""; } export function summary(md, max = 200) { const skip = /^(#{1,6} |```|\$\$|\||!\[)/; const line = md.split("\n").find((l) => l.trim() && !skip.test(l)) ?? ""; const text = plain(line.replace(/^(- |\d+\. |> )/, "")); if (text.length <= max) return text; const cut = text.lastIndexOf(" ", max); return text.slice(0, cut > 0 ? cut : max); } function bracket(src, i) { let depth = 0; let j = i; for (; j < src.length; j++) { if (src[j] === "[") depth++; else if (src[j] === "]" && --depth === 0) break; } if (j >= src.length || src[j + 1] !== "(") return null; let k = j + 2; for (depth = 1; k < src.length; k++) { if (src[k] === "(") depth++; else if (src[k] === ")" && --depth === 0) break; } if (k >= src.length) return null; return { text: src.slice(i + 1, j), url: src.slice(j + 2, k).trim(), end: k + 1 }; } const WORD = /[\p{L}\p{N}]/u; const blank = (c) => c === undefined || c === " " || c === "\n"; function closing(src, from, mark) { if (blank(src[from]) || WORD.test(src[from - mark.length - 1] ?? " ")) return -1; let j = src.indexOf(mark, from + 1); while (j > 0 && (blank(src[j - 1]) || WORD.test(src[j + mark.length] ?? " "))) j = src.indexOf(mark, j + 1); return j; } const unescape = (tex) => tex.replace(/\\\\([!-\/:-@[-`{-~])/g, "\\$1"); export function inline(src, ctx) { let out = ""; let i = 0; while (i < src.length) { const c = src[i]; if (c === "`") { const j = src.indexOf("`", i + 1); if (j > i + 1) { out += "" + escape(src.slice(i + 1, j)) + ""; i = j + 1; continue; } } else if (c === "$") { const display = src[i + 1] === "$"; const mark = display ? "$$" : "$"; const j = src.indexOf(mark, i + mark.length); if (j > i + mark.length - 1 && src.slice(i + mark.length, j).trim()) { out += ctx.math(unescape(src.slice(i + mark.length, j)), display); i = j + mark.length; continue; } } else if (c === "!" && src[i + 1] === "[") { const m = bracket(src, i + 1); if (m) { out += `${escape(m.text)}`; i = m.end; continue; } } else if (c === "[") { const m = bracket(src, i); if (m) { out += `${inline(m.text, ctx)}`; i = m.end; continue; } } else if (c === "*") { const strong = src[i + 1] === "*"; const mark = strong ? "**" : "*"; const j = closing(src, i + mark.length, mark); if (j > 0) { const tag = strong ? "strong" : "em"; out += `<${tag}>${inline(src.slice(i + mark.length, j), ctx)}`; i = j + mark.length; continue; } } out += ESC[c] ?? c; i++; } return out; } function cells(row) { const trimmed = row.trim().replace(/^\|/, "").replace(/\|$/, ""); return trimmed.split(/(? c.trim().replace(/\\\|/g, "|")); } function table(rows, ctx) { const align = cells(rows[1]).map((c) => { const l = c.startsWith(":"); const r = c.endsWith(":"); return l && r ? "center" : r ? "right" : l ? "left" : ""; }); const cell = (tag, text, k) => { const cls = align[k] && align[k] !== "left" ? ` class="${align[k]}"` : ""; return `<${tag}${cls}>${inline(text, ctx)}`; }; const head = cells(rows[0]).map((c, k) => cell("th", c, k)).join(""); const body = rows .slice(2) .map((r) => "" + cells(r).map((c, k) => cell("td", c, k)).join("") + "") .join("\n"); return `
${head}\n${body}\n
`; } function heading(line, ctx) { const level = line.indexOf(" "); const text = line.slice(level + 1).trim(); const base = slug(plain(text)); const seen = ctx.ids.get(base) ?? 0; ctx.ids.set(base, seen + 1); const id = seen ? `${base}-${seen}` : base; return `${inline(text, ctx)}`; } const WIDGET = /^!\[([^\]]*)\]\(demos\/([a-z0-9-]+)\/([a-z0-9-]+)\)$/; function paragraph(text, ctx) { const widget = ctx.widget && text.match(WIDGET); if (widget) return ctx.widget(widget[2], widget[3], inline(widget[1], ctx)); const image = text.match(/^!\[([^\]]*)\]\(([^)]*)\)$/); if (image) { const src = escape(ctx.link(image[2].trim())); return `
${escape(image[1])}
${inline(image[1], ctx)}
`; } return `

${inline(text, ctx)}

`; } export function render(md, opts = {}) { const ctx = { link: opts.link ?? ((u) => u), math: opts.math ?? ((t) => `${escape(t)}`), widget: opts.widget, ids: new Map(), }; const lines = md.replace(/\r\n?/g, "\n").split("\n"); const out = []; let i = 0; const run = (test) => { const start = i; while (i < lines.length && test(lines[i])) i++; return lines.slice(start, i); }; while (i < lines.length) { const line = lines[i]; if (line.startsWith("```")) { i++; const code = run((l) => !l.startsWith("```")); i++; out.push(`
${escape(code.join("\n"))}
`); } else if (line.startsWith("$$")) { const single = line.trim().length > 4 && line.trim().endsWith("$$"); let tex; if (single) { tex = line.trim().slice(2, -2); i++; } else { i++; const body = run((l) => !l.trim().endsWith("$$")); const last = i < lines.length ? lines[i].trim().slice(0, -2) : ""; i++; tex = line.slice(2) + "\n" + body.join("\n") + "\n" + last; } out.push(ctx.math(unescape(tex.trim()), true)); } else if (/^#{1,6} /.test(line)) { out.push(heading(line, ctx)); i++; } else if (line.startsWith("|") && i + 1 < lines.length && SEP.test(lines[i + 1])) { out.push(table(run((l) => l.startsWith("|")), ctx)); } else if (line.startsWith("- ")) { const items = run((l) => l.startsWith("- ")); out.push(""); } else if (/^\d+\. /.test(line)) { const items = run((l) => /^\d+\. /.test(l)); out.push("
    \n" + items.map((l) => `
  1. ${inline(l.replace(/^\d+\. /, ""), ctx)}
  2. `).join("\n") + "\n
"); } else if (line.startsWith("> ")) { const quote = run((l) => l.startsWith("> ")); out.push(`

${inline(quote.map((l) => l.slice(2)).join("\n"), ctx)}

`); } else if (!line.trim()) { i++; } else { const text = [lines[i++], ...run((l) => l.trim() && !BLOCK.test(l))]; out.push(paragraph(text.join("\n"), ctx)); } } return out.join("\n"); } export function front(md) { const m = md.match(/^---\n([\s\S]*?)\n---\n?/); if (!m) return { data: {}, body: md }; const data = {}; for (const line of m[1].split("\n")) { const at = line.indexOf(":"); if (at > 0) data[line.slice(0, at).trim()] = line.slice(at + 1).trim(); } return { data, body: md.slice(m[0].length) }; }