The end of open source? Just regurgitate some markdown parsing code.

This commit is contained in:
Thomas Krijnen
2026-04-03 11:03:44 +02:00
parent 24acfeaf45
commit c1f146966c
2 changed files with 241 additions and 1 deletions
+174 -1
View File
@@ -162,17 +162,190 @@ function setBusy(isBusy, reason = "") {
setStatus(isBusy ? (reason || "Working…") : "Ready");
}
function escapeHtml(text) {
return text
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function sanitizeUrl(url) {
try {
const parsed = new URL(url, window.location.href);
if (["http:", "https:", "mailto:"].includes(parsed.protocol)) {
return parsed.href;
}
} catch {
}
return null;
}
function renderInlineMarkdown(text) {
const placeholders = [];
const addPlaceholder = (html) => {
const token = `@@MD${placeholders.length}@@`;
placeholders.push({ token, html });
return token;
};
let rendered = text;
rendered = rendered.replace(/`([^`]+)`/g, (_, code) => addPlaceholder(`<code>${escapeHtml(code)}</code>`));
rendered = rendered.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => {
const href = sanitizeUrl(url);
if (!href) {
return `${label} (${url})`;
}
return addPlaceholder(
`<a href="${escapeHtml(href)}" target="_blank" rel="noreferrer noopener">${escapeHtml(label)}</a>`
);
});
rendered = escapeHtml(rendered);
rendered = rendered.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
rendered = rendered.replace(/\*([^*]+)\*/g, "<em>$1</em>");
rendered = rendered.replace(/_([^_]+)_/g, "<em>$1</em>");
for (const placeholder of placeholders) {
rendered = rendered.replaceAll(placeholder.token, placeholder.html);
}
return rendered;
}
function renderMarkdown(text) {
const lines = String(text).replace(/\r\n?/g, "\n").split("\n");
const html = [];
let paragraphLines = [];
let quoteLines = [];
let listType = null;
let listItems = [];
const flushParagraph = () => {
if (!paragraphLines.length) return;
html.push(`<p>${renderInlineMarkdown(paragraphLines.join(" "))}</p>`);
paragraphLines = [];
};
const flushQuote = () => {
if (!quoteLines.length) return;
const quoteBody = quoteLines.map((line) => renderInlineMarkdown(line)).join("<br />");
html.push(`<blockquote><p>${quoteBody}</p></blockquote>`);
quoteLines = [];
};
const flushList = () => {
if (!listItems.length || !listType) return;
const items = listItems.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("");
html.push(`<${listType}>${items}</${listType}>`);
listType = null;
listItems = [];
};
const flushAll = () => {
flushParagraph();
flushQuote();
flushList();
};
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
const trimmed = line.trim();
if (trimmed.startsWith("```")) {
flushAll();
const language = trimmed.slice(3).trim();
const codeLines = [];
index += 1;
while (index < lines.length && !lines[index].trim().startsWith("```")) {
codeLines.push(lines[index]);
index += 1;
}
const languageClass = language ? ` class="language-${escapeHtml(language)}"` : "";
html.push(`<pre><code${languageClass}>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
continue;
}
if (!trimmed) {
flushAll();
continue;
}
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
flushAll();
const level = headingMatch[1].length;
html.push(`<h${level}>${renderInlineMarkdown(headingMatch[2])}</h${level}>`);
continue;
}
const quoteMatch = trimmed.match(/^>\s?(.*)$/);
if (quoteMatch) {
flushParagraph();
flushList();
quoteLines.push(quoteMatch[1]);
continue;
}
if (quoteLines.length) {
flushQuote();
}
const unorderedListMatch = trimmed.match(/^[-*]\s+(.+)$/);
if (unorderedListMatch) {
flushParagraph();
if (listType && listType !== "ul") {
flushList();
}
listType = "ul";
listItems.push(unorderedListMatch[1]);
continue;
}
const orderedListMatch = trimmed.match(/^\d+\.\s+(.+)$/);
if (orderedListMatch) {
flushParagraph();
if (listType && listType !== "ol") {
flushList();
}
listType = "ol";
listItems.push(orderedListMatch[1]);
continue;
}
if (listItems.length) {
flushList();
}
paragraphLines.push(trimmed);
}
flushAll();
return html.join("");
}
function addMessage(role, text) {
if (text.ok) {
text = text.data;
}
if (typeof text !== "string") {
text = JSON.stringify(text, null, 2);
}
const wrap = document.createElement("div");
wrap.className = `msg ${role}`;
wrap.innerHTML = `
<div class="role ${role}">${role}${role === "tool" ? '<span class="chevron">▶</span>' : ''}</div>
<div class="bubble"></div>`;
const bubble = wrap.querySelector(".bubble");
bubble.textContent = text;
if (role === "assistant") {
bubble.classList.add("markdown-content");
bubble.innerHTML = renderMarkdown(text);
} else {
bubble.textContent = text;
}
bubble.onclick = function () {
if (bubble.scrollHeight > 100 && role === "tool") {
const expanded = bubble.style.maxHeight === 'none';
+67
View File
@@ -86,6 +86,73 @@ main {
white-space: pre-wrap;
}
.msg.assistant .bubble {
padding: 10px 14px;
line-height: 1.5;
}
.markdown-content > :first-child {
margin-top: 0;
}
.markdown-content > :last-child {
margin-bottom: 0;
}
.markdown-content p,
.markdown-content ul,
.markdown-content ol,
.markdown-content blockquote,
.markdown-content pre {
margin: 0 0 12px 0;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin: 0 0 12px 0;
line-height: 1.25;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 24px;
}
.markdown-content blockquote {
margin-left: 0;
padding-left: 12px;
border-left: 3px solid #ddd;
color: #555;
}
.markdown-content code {
padding: 1px 4px;
border-radius: 4px;
background: #f2f2f2;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 90%;
}
.markdown-content pre {
overflow-x: auto;
padding: 12px;
border-radius: 10px;
background: #f4f4f4;
}
.markdown-content pre code {
padding: 0;
background: transparent;
}
.markdown-content a {
color: inherit;
}
.msg.user .bubble {
padding: 10px 20px;
background: #eee;