CodeVed / js /markdownRenderer.js
Divy Patel
Clean deployment with comprehensive LFS tracking
2b770e0
Raw
History Blame Contribute Delete
11.2 kB
// ================================================================================
// CODE VED - ADVANCED MARKDOWN & MATH RENDERER (markdownRenderer.js)
// Engineered by Divy Patel | Modular Architecture
//
// This module is the "Visual Engine" of the AI. It handles:
// 1. Markdown parsing (via marked.js)
// 2. Syntax Highlighting (via highlight.js)
// 3. Complex Math Formulas (via MathJax) with pre/post-processing to prevent breaking
// 4. Mermaid Diagrams rendering
// 5. AI "Thinking Process" collapsible UI
// 6. Code block copy buttons
// ================================================================================
(function() {
'use strict';
// ----------------------------------------------------------------------------
// 1. INITIALIZATION & CONFIGURATION
// ----------------------------------------------------------------------------
// Initialize Mermaid (Diagrams)
function initMermaid() {
if (typeof mermaid !== 'undefined') {
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose',
fontFamily: 'Inter, sans-serif'
});
}
}
// Configure Marked.js (Markdown Parser)
function initMarked() {
if (typeof marked === 'undefined') return;
const renderer = new marked.Renderer();
// Custom Code Block Renderer (Handles Syntax Highlighting & Mermaid)
renderer.code = function(code, language, isEscaped) {
// Handle Mermaid Diagrams
if (language === 'mermaid') {
return `<pre class="mermaid-diagram"><code class="mermaid">${code}</code></pre>`;
}
// Handle Syntax Highlighting
let highlightedCode = code;
const langClass = language ? `language-${language}` : '';
if (language && typeof hljs !== 'undefined' && hljs.getLanguage(language)) {
try {
highlightedCode = hljs.highlight(code, { language: language, ignoreIllegals: true }).value;
} catch (e) {
highlightedCode = hljs.highlightAuto(code).value;
}
} else if (typeof hljs !== 'undefined') {
highlightedCode = hljs.highlightAuto(code).value;
}
// SVG Icons for Copy Button
const copyIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
// Return formatted code block with a copy button
return `<pre class="code-block-wrapper"><button class="code-copy-btn" title="Copy code">${copyIcon}<span>Copy</span></button><code class="hljs ${langClass}">${highlightedCode}</code></pre>`;
};
// Apply the custom renderer to marked
marked.use({
renderer: renderer,
gfm: true,
breaks: true,
pedantic: false,
mangle: false,
headerIds: false
});
}
// ----------------------------------------------------------------------------
// 2. MATHJAX PRE-PROCESSING & POST-PROCESSING (The Secret Sauce)
// ----------------------------------------------------------------------------
/*
* WHY THIS IS NEEDED:
* Markdown parsers (like marked) often break LaTeX math syntax. For example,
* underscores (_) in math formulas get converted to italics (<em>), and
* asterisks (*) get converted to bold (<strong>).
* To prevent this, we temporarily hide math blocks using unique placeholders,
* parse the markdown, and then put the math blocks back!
*/
function preprocessMath(text) {
const mathBlocks = {};
let counter = 0;
function createPlaceholder(match) {
const id = `@@MATHBLOCK_${counter++}@@`;
mathBlocks[id] = match;
return id;
}
// 1. Display Math: $$ ... $$
text = text.replace(/\$\$([\s\S]+?)\$\$/g, createPlaceholder);
// 2. Display Math: \[ ... \]
text = text.replace(/\\\[([\s\S]+?)\\\]/g, createPlaceholder);
// 3. Inline Math: $ ... $ (Ensuring it doesn't match currency or empty spaces)
text = text.replace(/(?<!\$)\$(?!\$)((?:[^$\\]|\\.)+?)\$(?!\$)/g, createPlaceholder);
// 4. Inline Math: \( ... \)
text = text.replace(/\\\(([\s\S]+?)\\\)/g, createPlaceholder);
return { text, mathBlocks };
}
function postprocessMath(html, mathBlocks) {
// Replace all placeholders back with the original LaTeX strings
for (const [id, mathStr] of Object.entries(mathBlocks)) {
html = html.split(id).join(mathStr);
}
return html;
}
// ----------------------------------------------------------------------------
// 3. THINKING PROCESS EXTRACTION (AI Reasoning UI)
// ----------------------------------------------------------------------------
function processThinkingBlocks(text) {
// Normalize different AI thinking tags (Qwen, etc.)
let normalizedText = text
.replace(/<\|channel\|>thought\s*<\|channel\|>/gi, "<think>\n")
.replace(/<\|channel\|>answer\s*<\|channel\|>/gi, "\n</think>\n")
.replace(/<\|im_start\|>thought/gi, "<think>\n")
.replace(/<\|im_end\|>/gi, "\n</think>\n");
const thinkRegex = /<think>([\s\S]*?)(?:<\/think>|$)/i;
const thinkMatch = normalizedText.match(thinkRegex);
let thinkHtml = '';
let mainText = normalizedText;
if (thinkMatch) {
const thinkContent = thinkMatch[1].trim();
// Parse the thinking content as markdown too, for better readability
const parsedThinkContent = typeof marked !== 'undefined' ? marked.parse(thinkContent) : thinkContent;
thinkHtml = `
<details class="qwen-think-box" open>
<summary>
<svg class="arrow" style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
Thinking Process
</summary>
<div class="qwen-think-content">${parsedThinkContent}</div>
</details>`;
// Remove the think block from the main text
mainText = normalizedText.replace(thinkRegex, '').trim();
}
return { thinkHtml, mainText };
}
// ----------------------------------------------------------------------------
// 4. POST-RENDERING TRIGGERS (MathJax & Mermaid)
// ----------------------------------------------------------------------------
async function triggerMathJax(container) {
if (window.MathJax && MathJax.typesetPromise) {
try {
// Clear previous math rendering in this container to prevent errors
MathJax.typesetClear([container]);
// Render new math
await MathJax.typesetPromise([container]);
} catch (err) {
console.warn('MathJax Rendering Warning:', err);
}
}
}
async function triggerMermaid(container) {
if (typeof mermaid === 'undefined') return;
const diagrams = container.querySelectorAll('.mermaid');
if (diagrams.length === 0) return;
try {
// mermaid.run is the modern way to render specific nodes
await mermaid.run({ nodes: diagrams });
} catch (err) {
console.warn('Mermaid Rendering Warning:', err);
// If mermaid fails, it might leave broken SVGs. We clean them up.
diagrams.forEach(d => {
if (!d.querySelector('svg')) {
d.innerHTML = `<span style="color:var(--brand-danger);font-size:12px;">⚠️ Invalid Mermaid Syntax</span>`;
}
});
}
}
// ----------------------------------------------------------------------------
// 5. MAIN RENDERING ENGINE (Exposed to script.js)
// ----------------------------------------------------------------------------
/**
* The core function called by script.js to render AI responses.
* @param {string} fullText - The raw text from the AI.
* @param {boolean} isProcessing - True if the AI is still streaming.
* @param {HTMLElement} container - The DOM element to inject the HTML into.
*/
async function parseAndRender(fullText, isProcessing, container) {
if (!container) return;
// Step 1: Extract and format "Thinking" blocks
const { thinkHtml, mainText } = processThinkingBlocks(fullText);
let finalHtml = thinkHtml;
// Step 2: Process the main text
if (mainText) {
// 2a. Hide Math formulas so Markdown parser doesn't break them
const { text: safeText, mathBlocks } = preprocessMath(mainText);
// 2b. Parse Markdown to HTML
let parsedHtml = '';
if (typeof marked !== 'undefined') {
parsedHtml = marked.parse(safeText);
} else {
parsedHtml = safeText.replace(/\n/g, '<br>'); // Fallback
}
// 2c. Put Math formulas back
parsedHtml = postprocessMath(parsedHtml, mathBlocks);
// 2d. Wrap in a class for MathJax targeting
finalHtml += `<div class="tex2jax_process">${parsedHtml}</div>`;
}
// Step 3: Add blinking cursor if AI is still generating
if (isProcessing) {
finalHtml += `<span class="blinking-cursor"></span>`;
}
// Step 4: Inject into DOM
container.innerHTML = finalHtml;
// Step 5: Trigger external renderers (MathJax & Mermaid)
// We only trigger them fully when processing is done to save performance,
// but we do a lightweight pass during streaming.
if (!isProcessing) {
await triggerMathJax(container);
await triggerMermaid(container);
} else {
// Lightweight MathJax trigger during streaming (optional, prevents lag)
// triggerMathJax(container);
}
}
// ----------------------------------------------------------------------------
// 6. INITIALIZATION & EXPORT
// ----------------------------------------------------------------------------
function init() {
initMermaid();
initMarked();
console.log('[MarkdownRenderer] Engine initialized successfully.');
}
// Expose the API to the global window object so script.js can use it
window.MarkdownRenderer = {
parseAndRender: parseAndRender,
init: init
};
// Auto-initialize when this script loads
init();
})();