mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 10:33:20 +00:00
The end of open source? Just regurgitate some markdown parsing code.
This commit is contained in:
+174
-1
@@ -162,17 +162,190 @@ function setBusy(isBusy, reason = "") {
|
||||
setStatus(isBusy ? (reason || "Working…") : "Ready");
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return text
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user