52 lines
2.0 KiB
JavaScript
52 lines
2.0 KiB
JavaScript
/* Brain of Reese — shared markdown renderer (no external libs, no CDN).
|
|
*
|
|
* Extracted from app.js (phase 10) so the chat page and the document
|
|
* viewer share the exact same escape-first renderer: every character is
|
|
* HTML-escaped before any markup transform runs, so document (or user)
|
|
* content can never inject live HTML/XSS. Classic script on purpose:
|
|
* index.html and document.html load it via a plain relative <script src>
|
|
* and both module scripts (app.js / document.js) call the globals it
|
|
* defines. Rendering behavior is unchanged from the original app.js copy.
|
|
*/
|
|
|
|
function escapeHtml(s) {
|
|
return s.replace(/[&<>"']/g, (c) => ({
|
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
}[c]));
|
|
}
|
|
|
|
function renderMarkdown(md) {
|
|
// 1. Protect fenced code blocks.
|
|
const codeBlocks = [];
|
|
let text = md.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, _lang, code) => {
|
|
codeBlocks.push(`<pre><code>${escapeHtml(code.replace(/\n$/, ""))}</code></pre>`);
|
|
return `\u0000CODE${codeBlocks.length - 1}\u0000`;
|
|
});
|
|
|
|
// 2. Escape everything else, then apply inline + block transforms.
|
|
text = escapeHtml(text)
|
|
.replace(/`([^`\n]+)`/g, "<code>$1</code>")
|
|
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
.replace(/(^|[\s(])\*([^*\n]+)\*/g, "$1<em>$2</em>")
|
|
.replace(/^### (.*)$/gm, "<h4>$1</h4>")
|
|
.replace(/^## (.*)$/gm, "<h3>$1</h3>")
|
|
.replace(/^# (.*)$/gm, "<h3>$1</h3>")
|
|
.replace(/^\s*[-*] (.*)$/gm, "<li>$1</li>")
|
|
.replace(/(<li>[\s\S]*?<\/li>)(?!\s*<li>)/g, "<ul>$1</ul>")
|
|
.replace(/^\d+\. (.*)$/gm, "<li>$1</li>");
|
|
|
|
// 3. Paragraphs (double newline separated).
|
|
text = text
|
|
.split(/\n{2,}/)
|
|
.map((block) => {
|
|
const b = block.trim();
|
|
if (!b) return "";
|
|
if (/^<(h\d|ul|ol|pre|li)/.test(b)) return b;
|
|
return `<p>${b.replace(/\n/g, "<br>")}</p>`;
|
|
})
|
|
.join("");
|
|
|
|
// 4. Restore code blocks.
|
|
return text.replace(/\u0000CODE(\d+)\u0000/g, (_m, i) => codeBlocks[Number(i)]);
|
|
}
|