Render Mermaid diagrams and worktree images in agent Markdown
This commit is contained in:
Vendored
+60
-30
@@ -1,22 +1,23 @@
|
||||
import { h } from "./dom.js";
|
||||
import { diagram } from "./mermaid.js";
|
||||
const choiceMarker = /^(\([ *]\)|\[[ xX]\])\s+(\S.*?)\s*$/;
|
||||
const detailsOpen = /^\s*<details>\s*(?:<summary>(.*?)<\/summary>)?\s*$/i;
|
||||
const summaryLine = /^\s*<summary>(.*?)<\/summary>\s*$/i;
|
||||
const detailsClose = /^\s*<\/details>\s*$/i;
|
||||
const fence = /^\s*(```|~~~)/;
|
||||
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 tableSep = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
|
||||
export function markdown(src, className = "md", choice) {
|
||||
return h("div", { class: className }, ...blocks(src.replace(/\r\n?/g, "\n").split("\n"), choice));
|
||||
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) {
|
||||
return h("span", { class: "md-inline" }, ...inline(src));
|
||||
export function inlineMarkdown(src, opts = {}) {
|
||||
return h("span", { class: "md-inline" }, ...inline(src, opts));
|
||||
}
|
||||
function blocks(lines, choice) {
|
||||
function blocks(lines, opts) {
|
||||
const out = [];
|
||||
let i = 0;
|
||||
while (i < lines.length) {
|
||||
@@ -35,7 +36,7 @@ function blocks(lines, choice) {
|
||||
i++;
|
||||
}
|
||||
i++;
|
||||
out.push(h("pre", { class: "md-code" }, h("code", {}, body.join("\n"))));
|
||||
out.push(f[2].toLowerCase() === "mermaid" ? diagram(body.join("\n")) : h("pre", { class: "md-code" }, h("code", {}, body.join("\n"))));
|
||||
continue;
|
||||
}
|
||||
if (detailsClose.test(line)) {
|
||||
@@ -63,13 +64,13 @@ function blocks(lines, choice) {
|
||||
}
|
||||
body.push(l);
|
||||
}
|
||||
out.push(h("details", { class: "md-details" }, h("summary", {}, ...inline(title?.trim() || "Details")), h("div", { class: "md-details-body" }, ...blocks(body, choice))));
|
||||
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])));
|
||||
out.push(h(`h${level}`, {}, ...inline(hd[2], opts)));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
@@ -84,11 +85,11 @@ function blocks(lines, choice) {
|
||||
body.push(quote.exec(lines[i])[1]);
|
||||
i++;
|
||||
}
|
||||
out.push(h("blockquote", {}, ...blocks(body)));
|
||||
out.push(h("blockquote", {}, ...blocks(body, { taskId: opts.taskId })));
|
||||
continue;
|
||||
}
|
||||
if (bullet.test(line) || ordered.test(line)) {
|
||||
const [list, next] = parseList(lines, i, choice);
|
||||
const [list, next] = parseList(lines, i, opts);
|
||||
out.push(list);
|
||||
i = next;
|
||||
continue;
|
||||
@@ -100,7 +101,7 @@ function blocks(lines, choice) {
|
||||
rows.push(cells(lines[i]));
|
||||
i++;
|
||||
}
|
||||
out.push(table(rows));
|
||||
out.push(table(rows, opts));
|
||||
continue;
|
||||
}
|
||||
const para = [];
|
||||
@@ -108,7 +109,7 @@ function blocks(lines, choice) {
|
||||
para.push(lines[i].trim());
|
||||
i++;
|
||||
}
|
||||
out.push(h("p", {}, ...joinLines(para)));
|
||||
out.push(h("p", {}, ...joinLines(para, opts)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -124,20 +125,20 @@ function startsBlock(lines, i) {
|
||||
ordered.test(line) ||
|
||||
(line.includes("|") && i + 1 < lines.length && tableSep.test(lines[i + 1])));
|
||||
}
|
||||
function joinLines(lines) {
|
||||
function joinLines(lines, opts) {
|
||||
const out = [];
|
||||
lines.forEach((l, n) => {
|
||||
if (n > 0) {
|
||||
out.push(h("br"));
|
||||
}
|
||||
out.push(...inline(l));
|
||||
out.push(...inline(l, opts));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
function indentOf(line) {
|
||||
return line.length - line.trimStart().length;
|
||||
}
|
||||
function parseList(lines, start, choice) {
|
||||
function parseList(lines, start, opts) {
|
||||
const first = lines[start];
|
||||
const isOrdered = !bullet.test(first);
|
||||
const base = indentOf(first);
|
||||
@@ -175,17 +176,17 @@ function parseList(lines, start, choice) {
|
||||
body.push(indentOf(next) > base ? next.slice(Math.min(indentOf(next), base + 2)) : next.trim());
|
||||
i++;
|
||||
}
|
||||
list.append(item(body, isOrdered ? undefined : choice));
|
||||
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++;
|
||||
}
|
||||
}
|
||||
return [list, i];
|
||||
}
|
||||
function item(body, choice) {
|
||||
function item(body, opts) {
|
||||
const li = h("li");
|
||||
const m = choice && choiceMarker.exec(body[0]);
|
||||
const picker = m ? choice(m[2]) : null;
|
||||
const m = opts.choice && choiceMarker.exec(body[0]);
|
||||
const picker = m ? opts.choice(m[2]) : null;
|
||||
if (picker) {
|
||||
li.className = "choice";
|
||||
li.append(picker);
|
||||
@@ -193,7 +194,7 @@ function item(body, choice) {
|
||||
const first = picker ? [] : [body[0]];
|
||||
const rest = body.slice(1);
|
||||
if (rest.every((l) => !startsBlockLine(l)) && !rest.includes("")) {
|
||||
li.append(...joinLines([...first, ...rest].map((l) => l.trim())));
|
||||
li.append(...joinLines([...first, ...rest].map((l) => l.trim()), opts));
|
||||
return li;
|
||||
}
|
||||
const head = first;
|
||||
@@ -202,7 +203,7 @@ function item(body, choice) {
|
||||
head.push(body[k].trim());
|
||||
k++;
|
||||
}
|
||||
li.append(...joinLines(head), ...blocks(body.slice(k), choice));
|
||||
li.append(...joinLines(head, opts), ...blocks(body.slice(k), opts));
|
||||
return li;
|
||||
}
|
||||
function startsBlockLine(line) {
|
||||
@@ -218,12 +219,12 @@ function cells(line) {
|
||||
}
|
||||
return s.split("|").map((c) => c.trim());
|
||||
}
|
||||
function table(rows) {
|
||||
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))))), h("tbody", {}, ...body.map((r) => h("tr", {}, ...r.map((c) => h("td", {}, ...inline(c))))))));
|
||||
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][^*]*?)\*|(?<![\w])_([^_\s][^_]*?)_(?![\w])|\[([^\]]+)\]\(([^)\s]+)\)|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/;
|
||||
function inline(src) {
|
||||
const inlineToken = /`([^`]+)`|\*\*(.+?)\*\*|__(.+?)__|\*([^*\s][^*]*?)\*|(?<![\w])_([^_\s][^_]*?)_(?![\w])|!\[([^\]]*)\]\((?:<([^>]+)>|([^)]+))\)|\[([^\]]+)\]\(([^)\s]+)\)|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/;
|
||||
function inline(src, opts) {
|
||||
const out = [];
|
||||
let rest = src;
|
||||
while (rest.length > 0) {
|
||||
@@ -235,18 +236,21 @@ function inline(src) {
|
||||
if (m.index > 0) {
|
||||
out.push(document.createTextNode(rest.slice(0, m.index)));
|
||||
}
|
||||
const [whole, code, strong1, strong2, em1, em2, linkText, linkHref, bare] = m;
|
||||
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)));
|
||||
out.push(h("strong", {}, ...inline(strong1 ?? strong2, opts)));
|
||||
}
|
||||
else if (em1 !== undefined || em2 !== undefined) {
|
||||
out.push(h("em", {}, ...inline(em1 ?? em2)));
|
||||
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)));
|
||||
out.push(link(linkHref, inline(linkText, opts)));
|
||||
}
|
||||
else if (bare !== undefined) {
|
||||
out.push(link(bare, [document.createTextNode(bare)]));
|
||||
@@ -264,4 +268,30 @@ function link(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
|
||||
Reference in New Issue
Block a user