File size: 11,205 Bytes
2b770e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// ================================================================================
// 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();

})();