feat(web): render Lyra's replies as Markdown (readable, not a wall of asterisks)
Her replies are full of **bold**, numbered lists and headings but rendered as raw monospace text, so the chat was a cluttered wall of literal markup. Add a small self-contained Markdown renderer (no deps): headings, ordered/unordered lists, bold/italic, inline + fenced code, links + autolinked URLs, with HTML escaping. Assistant messages now render to HTML; user/system stay literal text. Proportional font + spacing/list/code styling for assistant bubbles. (Renderer avoids literal backticks via String.fromCharCode(96) — a triple-tick regex literal had been corrupting the file with NUL bytes.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -298,12 +298,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
var bt = String.fromCharCode(96);
|
||||
var esc = function (s) { return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); };
|
||||
var src = String(text == null ? "" : text).replace(/\r\n/g, "\n");
|
||||
var blocks = [];
|
||||
var fenceRe = new RegExp(bt + bt + bt + "[^\\n]*\\n?([\\s\\S]*?)" + bt + bt + bt, "g");
|
||||
src = src.replace(fenceRe, function (_, code) { blocks.push(code.replace(/\n+$/, "")); return "@@CB" + (blocks.length - 1) + "@@"; });
|
||||
var codeRe = new RegExp(bt + "([^" + bt + "]+)" + bt, "g");
|
||||
var inline = function (s) {
|
||||
return esc(s)
|
||||
.replace(codeRe, "<code>$1</code>")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
||||
.replace(/\*([^*\n]+)\*/g, "<em>$1</em>")
|
||||
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
|
||||
.replace(/(^|[\s(])(https?:\/\/[^\s<)]+)/g, '$1<a href="$2" target="_blank" rel="noopener">$2</a>');
|
||||
};
|
||||
var lines = src.split("\n");
|
||||
var out = [], para = [], list = null;
|
||||
var flushPara = function () { if (para.length) { out.push("<p>" + para.map(inline).join("<br>") + "</p>"); para = []; } };
|
||||
var flushList = function () { if (list) { out.push("<" + list.t + ">" + list.items.map(function (it) { return "<li>" + inline(it) + "</li>"; }).join("") + "</" + list.t + ">"); list = null; } };
|
||||
var flushAll = function () { flushPara(); flushList(); };
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].replace(/\s+$/, ""); var t = line.trim(); var m;
|
||||
if ((m = t.match(/^@@CB(\d+)@@$/))) { flushAll(); out.push("<pre><code>" + esc(blocks[+m[1]]) + "</code></pre>"); continue; }
|
||||
if (!t) { flushAll(); continue; }
|
||||
if ((m = line.match(/^(#{1,4})\s+(.*)$/))) { flushAll(); out.push("<h" + m[1].length + ">" + inline(m[2]) + "</h" + m[1].length + ">"); continue; }
|
||||
if ((m = line.match(/^\s*\d+[.)]\s+(.*)$/))) { flushPara(); if (!list || list.t !== "ol") { flushList(); list = { t: "ol", items: [] }; } list.items.push(m[1]); continue; }
|
||||
if ((m = line.match(/^\s*[-*+]\s+(.*)$/))) { flushPara(); if (!list || list.t !== "ul") { flushList(); list = { t: "ul", items: [] }; } list.items.push(m[1]); continue; }
|
||||
flushList(); para.push(line);
|
||||
}
|
||||
flushAll();
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
function addMessage(role, text, autoScroll = true) {
|
||||
const messagesEl = document.getElementById("messages");
|
||||
|
||||
const msgDiv = document.createElement("div");
|
||||
msgDiv.className = `msg ${role}`;
|
||||
msgDiv.textContent = text;
|
||||
if (role === "assistant") { msgDiv.innerHTML = renderMarkdown(text); } else { msgDiv.textContent = text; }
|
||||
messagesEl.appendChild(msgDiv);
|
||||
|
||||
// Auto-scroll to bottom if enabled
|
||||
|
||||
Reference in New Issue
Block a user