import { copyable, h } from "./dom.js"; import { diagram } from "./mermaid.js"; import { taskStyle } from "./taskcolor.js"; import { todoLink } from "./todolink.js"; const choiceMarker = /^(\([ *]\)|\[[ xX]\])\s+(\S.*?)\s*$/; const detailsOpen = /^\s*
\s*(?:(.*?)<\/summary>)?\s*$/i; const summaryLine = /^\s*(.*?)<\/summary>\s*$/i; const detailsClose = /^\s*<\/details>\s*$/i; const fence = /^\s*(```|~~~)\s*(\S*)/; const heading = /^(#{1,6})\s+(.*)$/; const bullet = /^(\s*)[-*+]\s+(.*)$/; const ordered = /^(\s*)(\d+)[.)]\s+(.*)$/; const quote = /^\s*>\s?(.*)$/; const rule = /^\s*([-*_])(\s*\1){2,}\s*$/; const imageOnly = /^\s*!\[[^\]]*\]\((<[^>]*>|[^)]*)\)\s*$/; const tableSep = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/; export function markdown(src, className = "md", opts = {}) { return h("div", { class: className }, ...blocks(src.replace(/\r\n?/g, "\n").split("\n"), opts)); } export function inlineMarkdown(src, opts = {}) { return h("span", { class: "md-inline" }, ...inline(src, opts)); } function blocks(lines, opts) { const out = []; let i = 0; while (i < lines.length) { const line = lines[i]; if (line.trim() === "") { i++; continue; } const f = fence.exec(line); if (f) { const marker = f[1]; const body = []; i++; while (i < lines.length && !lines[i].trimStart().startsWith(marker)) { body.push(lines[i]); i++; } i++; const lang = f[2].toLowerCase(); if (lang === "mermaid") { out.push(diagram(body.join("\n"))); continue; } if (lang === "diff" || lang === "patch") { out.push(copyable(h("pre", { class: "md-code diff" }, h("code", {}, ...body.map(diffLine))))); continue; } out.push(copyable(h("pre", { class: "md-code" }, h("code", {}, body.join("\n"))))); continue; } if (detailsClose.test(line)) { i++; continue; } const d = detailsOpen.exec(line); if (d) { let title = d[1]; i++; if (title === undefined && i < lines.length && summaryLine.test(lines[i])) { title = summaryLine.exec(lines[i])[1]; i++; } const body = []; let depth = 1; while (i < lines.length) { const l = lines[i]; i++; if (detailsOpen.test(l)) { depth++; } else if (detailsClose.test(l) && --depth === 0) { break; } body.push(l); } out.push(h("details", { class: "md-details" }, h("summary", {}, ...inline(title?.trim() || "Details", opts)), h("div", { class: "md-details-body" }, ...blocks(body, opts)))); continue; } const hd = heading.exec(line); if (hd) { const level = Math.min(hd[1].length + 2, 6); out.push(h(`h${level}`, {}, ...inline(hd[2], opts))); i++; continue; } if (rule.test(line)) { out.push(h("hr")); i++; continue; } if (quote.test(line)) { const body = []; while (i < lines.length && quote.test(lines[i])) { body.push(quote.exec(lines[i])[1]); i++; } out.push(h("blockquote", {}, ...blocks(body, { taskId: opts.taskId }))); continue; } if (bullet.test(line) || ordered.test(line)) { const [list, next] = parseList(lines, i, opts); out.push(list); i = next; continue; } if (line.includes("|") && i + 1 < lines.length && tableSep.test(lines[i + 1])) { const rows = [cells(line)]; i += 2; while (i < lines.length && lines[i].includes("|") && lines[i].trim() !== "") { rows.push(cells(lines[i])); i++; } out.push(table(rows, opts)); continue; } const para = []; while (i < lines.length && lines[i].trim() !== "" && !startsBlock(lines, i)) { para.push(lines[i].trim()); i++; } out.push(h("p", {}, ...joinLines(para, opts))); } return out; } function diffLine(l) { const cls = l.startsWith("@@") ? "hunk" : l.startsWith("+++") || l.startsWith("---") ? "meta" : l.startsWith("+") ? "add" : l.startsWith("-") ? "del" : "ctx"; return h("span", { class: `line ${cls}` }, l + "\n"); } function startsBlock(lines, i) { const line = lines[i]; return (fence.test(line) || detailsOpen.test(line) || detailsClose.test(line) || heading.test(line) || rule.test(line) || quote.test(line) || bullet.test(line) || ordered.test(line) || (line.includes("|") && i + 1 < lines.length && tableSep.test(lines[i + 1]))); } function joinLines(lines, opts) { const out = []; lines.forEach((l, n) => { if (n > 0) { out.push(h("br")); } out.push(...inline(l, opts)); }); return out; } function indentOf(line) { return line.length - line.trimStart().length; } function parseList(lines, start, opts) { const first = lines[start]; const isOrdered = !bullet.test(first); const base = indentOf(first); const list = isOrdered ? h("ol") : h("ul"); if (isOrdered) { const n = Number(ordered.exec(first)[2]); if (n !== 1) { list.start = n; } } let i = start; let imageChoices = !isOrdered && !!opts.choice; while (i < lines.length) { const line = lines[i]; const m = isOrdered ? ordered.exec(line) : bullet.exec(line); if (!m || indentOf(line) !== base) { break; } const text = isOrdered ? m[3] : m[2]; const body = [text]; i++; while (i < lines.length) { const next = lines[i]; if (next.trim() === "") { const after = lines[i + 1]; if (after !== undefined && indentOf(after) > base && after.trim() !== "") { body.push(""); i++; continue; } break; } if (indentOf(next) <= base && (bullet.test(next) || ordered.test(next) || startsBlock(lines, i))) { break; } body.push(indentOf(next) > base ? next.slice(Math.min(indentOf(next), base + 2)) : next.trim()); i++; } const detail = body.slice(1).filter((l) => l.trim() !== ""); imageChoices &&= choiceMarker.test(text) && detail.length === 1 && imageOnly.test(detail[0]); list.append(item(body, isOrdered ? { taskId: opts.taskId } : opts)); while (i < lines.length && lines[i].trim() === "" && lines[i + 1] !== undefined && indentOf(lines[i + 1]) === base && (isOrdered ? ordered : bullet).test(lines[i + 1])) { i++; } } if (imageChoices) { list.className = "choice-grid"; } return [list, i]; } function item(body, opts) { const li = h("li"); const m = opts.choice && choiceMarker.exec(body[0]); const picker = m ? opts.choice(m[2]) : null; const detail = h("div", { class: "choice-detail" }); if (picker) { li.className = "choice"; li.append(picker); if (body.slice(1).some((l) => l.trim() !== "")) { picker.append(detail); } } const target = picker ? detail : li; const first = picker ? [] : [body[0]]; const rest = body.slice(1); if (rest.every((_, n) => !startsBlock(body, n + 1)) && !rest.includes("")) { target.append(...joinLines([...first, ...rest].map((l) => l.trim()), opts)); return li; } const head = first; let k = 1; while (k < body.length && body[k] !== "" && !startsBlock(body, k)) { head.push(body[k].trim()); k++; } target.append(...joinLines(head, opts), ...blocks(body.slice(k), opts)); return li; } function cells(line) { let s = line.trim(); if (s.startsWith("|")) { s = s.slice(1); } if (s.endsWith("|")) { s = s.slice(0, -1); } return s.split("|").map((c) => c.trim()); } function table(rows, opts) { const [head, ...body] = rows; return h("div", { class: "md-table" }, h("table", {}, h("thead", {}, h("tr", {}, ...head.map((c) => h("th", {}, ...inline(c, opts))))), h("tbody", {}, ...body.map((r) => h("tr", {}, ...r.map((c) => h("td", {}, ...inline(c, opts)))))))); } const inlineToken = /`([^`]+)`|\*\*(.+?)\*\*|__(.+?)__|\*([^*\s][^*]*?)\*|(?]+)>|([^)]+))\)|\[([^\]]+)\]\(([^)\s]+)\)|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/; function inline(src, opts) { const out = []; let rest = src; while (rest.length > 0) { const m = inlineToken.exec(rest); if (!m) { out.push(document.createTextNode(rest)); break; } if (m.index > 0) { out.push(document.createTextNode(rest.slice(0, m.index))); } const [whole, code, strong1, strong2, em1, em2, imgAlt, imgAngled, imgSrc, linkText, linkHref, bare] = m; if (code !== undefined) { out.push(h("code", {}, code)); } else if (strong1 !== undefined || strong2 !== undefined) { out.push(h("strong", {}, ...inline(strong1 ?? strong2, opts))); } else if (em1 !== undefined || em2 !== undefined) { out.push(h("em", {}, ...inline(em1 ?? em2, opts))); } else if (imgAlt !== undefined) { out.push(image((imgAngled ?? imgSrc).trim(), imgAlt, opts.taskId)); } else if (linkText !== undefined) { out.push(link(linkHref, inline(linkText, opts))); } else if (bare !== undefined) { out.push(link(bare, [document.createTextNode(bare)])); } rest = rest.slice(m.index + whole.length); } return out; } function link(href, children) { if (/^https?:\/\//i.test(href)) { return h("a", { href, target: "_blank", rel: "noopener noreferrer" }, ...children); } const task = /^#\/task\/(\w+)(\/\w+)?$/.exec(href); if (task) { return h("a", { href, class: "task-link", style: taskStyle(task[1]) }, ...children); } const todo = /^#\/todo\/(\w+)\/(.+)$/.exec(href); if (todo) { try { return todoLink(todo[1], decodeURIComponent(todo[2]), children); } catch { return h("span", {}, ...children); } } if (href.startsWith("#/")) { return h("a", { href }, ...children); } return h("span", {}, ...children); } function worktreeImageURL(path, taskId) { if (/^data:image\/(png|jpeg|gif|webp);/i.test(path)) { return path; } let file = path; if (/^file:\/\//i.test(file)) { file = decodeURI(file.slice("file://".length)); } else if (/^[a-z][a-z0-9+.-]*:/i.test(file)) { return null; } if (!taskId) { return null; } return `/api/tasks/${encodeURIComponent(taskId)}/image?path=${encodeURIComponent(file)}`; } export function image(src, alt, taskId) { const url = worktreeImageURL(src, taskId); if (!url) { return link(src, [document.createTextNode(alt || src)]); } const img = h("img", { src: url, alt, loading: "lazy" }); const el = h("a", { class: "md-img", href: url, target: "_blank", rel: "noopener" }, img); img.onerror = () => el.replaceWith(h("span", { class: "md-img-missing" }, `image not available: ${src}`)); return el; } //# sourceMappingURL=markdown.js.map