Gemini Advanced Renderer (HTML, Mermaid, Pollinations)

Merges two scripts: 1) Renders HTML/Mermaid/ECharts code blocks with advanced syntax correction. 2) Replaces pollinations.ai image links with the actual rendered images.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Gemini Advanced Renderer (HTML, Mermaid, Pollinations)
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Merges two scripts: 1) Renders HTML/Mermaid/ECharts code blocks with advanced syntax correction. 2) Replaces pollinations.ai image links with the actual rendered images.
// @author       YourName (Refactored by AI & Combined)
// @match        https://gemini.google.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_openInTab
// @connect      kroki.io
// @connect      cdn.jsdelivr.net
// @connect      image.pollinations.ai
// @connect      *
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    const DEBUG = true;

    // --- Styles ---
    const styles = `
        .render-preview-button{margin-left:10px;cursor:pointer;padding:4px 8px;background-color:#1a73e8;color:#fff;border:none;border-radius:4px;font-size:12px;font-weight:500;opacity:.9;transition:opacity .3s,background-color .3s}.render-preview-button:hover:not(:disabled){opacity:1;background-color:#185abc}.render-preview-button:disabled{background-color:#9e9e9e;cursor:not-allowed}.mermaid-button{background-color:#9c27b0}.mermaid-button:hover:not(:disabled){background-color:#7b1fa2}.echarts-button{background-color:#4caf50}.echarts-button:hover:not(:disabled){background-color:#45a049}.preview-container{width:100%;margin-top:10px;border:1px solid #dee2e6;border-radius:8px;overflow:hidden;box-shadow:0 2px 4px rgba(0,0,0,.05);background-color:#fff;position:relative}.preview-iframe{width:100%;height:600px;border:none;display:block}.preview-controls{padding:8px;background-color:#f5f5f5;border-bottom:1px solid #dee2e6;font-size:12px;display:flex;gap:10px;align-items:center;color:#333}.control-button{padding:4px 8px;background:#6c757d;color:#fff;border:none;border-radius:3px;cursor:pointer;font-size:11px}.control-button:hover{background:#5a6268}.mermaid-preview-container{width:100%;margin-top:10px;border:1px solid #dee2e6;border-radius:8px;padding:20px;background-color:#fff;box-shadow:0 2px 4px rgba(0,0,0,.05);min-height:200px;max-height:600px;overflow:auto;text-align:center;position:relative}.preview-overlay{flex-grow:1;text-align:left}.preview-error{padding:15px;color:#d32f2f;background:#ffeaea;border-radius:4px;font-family:monospace;white-space:pre-wrap;text-align:left;font-size:13px}.mermaid-success-badge{position:absolute;top:5px;right:5px;background:#4caf50;color:#fff;padding:2px 8px;border-radius:3px;font-size:12px}.mermaid-warning-badge{position:absolute;top:5px;left:5px;background:#ff9800;color:#fff;padding:2px 8px;border-radius:3px;font-size:12px;cursor:help}
    `;
    const styleSheet = document.createElement("style");
    styleSheet.innerText = styles;
    document.head.appendChild(styleSheet);


    // --- Trusted Types & Utilities ---
    let trustedPolicy;
    function getTrustedPolicy() {
        if (trustedPolicy) return trustedPolicy;
        if (window.trustedTypes && window.trustedTypes.createPolicy) {
            try {
                trustedPolicy = window.trustedTypes.createPolicy('gemini-advanced-renderer-policy-v1', {
                    createHTML: (string) => string, createScript: (string) => string,
                });
            } catch (e) { trustedPolicy = window.trustedTypes.getPolicy('gemini-advanced-renderer-policy-v1'); }
        }
        return trustedPolicy;
    }
    function safeSetHTML(element, html) {
        const policy = getTrustedPolicy();
        element.innerHTML = policy ? policy.createHTML(html) : html;
    }
    function fetchResource(url, method = 'GET', data = null, responseType = 'text') {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method, url, data, responseType,
                headers: data ? { 'Content-Type': 'text/plain', 'Accept': 'image/svg+xml' } : {},
                onload: (res) => (res.status >= 200 && res.status < 300) ? resolve(responseType === 'blob' ? res.response : res.responseText) : reject(new Error(`Request failed: ${res.status}\n${res.responseText}`)),
                onerror: (err) => reject(new Error(`Network error: ${err.toString()}`)),
                ontimeout: () => reject(new Error(`Request timed out`))
            });
        });
    }

    // --- HTML Rendering Engine ---
    async function createSelfContainedHTML(html, updateStatus) {
        updateStatus('Parsing HTML...');
        const parser = new DOMParser(); const policy = getTrustedPolicy();
        const trustedHtmlInput = policy ? policy.createHTML(html) : html;
        const doc = parser.parseFromString(trustedHtmlInput, 'text/html');
        doc.querySelectorAll('script[src], link[rel="stylesheet"][href]').forEach(el => { el.dataset.originalUrl = new URL(el.src || el.href, window.location.href).href; });
        const resources = Array.from(doc.querySelectorAll('script[src], link[rel="stylesheet"][href]'));
        updateStatus(`Found ${resources.length} external resource(s).`);
        for (const res of resources) {
            const url = res.dataset.originalUrl;
            try {
                updateStatus(`Downloading: ${url.split('/').pop()}`);
                const content = await fetchResource(url, 'GET', null, 'text');
                if (res.tagName === 'SCRIPT') {
                    const newScript = doc.createElement('script');
                    newScript.textContent = policy ? policy.createScript(content) : content;
                    res.parentNode.replaceChild(newScript, res);
                } else {
                    const newStyle = doc.createElement('style');
                    newStyle.textContent = content;
                    res.parentNode.replaceChild(newStyle, res);
                }
            } catch (error) {
                console.error(error);
                if (res.tagName === 'SCRIPT') {
                    const errorScript = doc.createElement('script');
                    const errorContent = `console.error("Failed to load script: ${url}. ${error.message.replace(/"/g, '\\"')}");`;
                    errorScript.textContent = policy ? policy.createScript(errorContent) : errorContent;
                    res.parentNode.replaceChild(errorScript, res);
                }
            }
        }
        updateStatus('All resources embedded.');
        return `<!DOCTYPE html><html><head>${doc.head.innerHTML}</head><body>${doc.body.innerHTML}</body></html>`;
    }
    async function renderHTML(content, codeBlockContainer) {
        const previewContainer = document.createElement('div'); previewContainer.className = 'preview-container';
        const controls = document.createElement('div'); controls.className = 'preview-controls';
        const openInTabBtn = document.createElement('button'); openInTabBtn.className = 'control-button'; openInTabBtn.textContent = 'Open in New Tab'; openInTabBtn.disabled = true;
        const statusContainer = document.createElement('div'); statusContainer.className = 'preview-overlay'; statusContainer.textContent = 'Initializing...';
        controls.appendChild(statusContainer); controls.appendChild(openInTabBtn);
        const iframe = document.createElement('iframe'); iframe.className = 'preview-iframe'; iframe.removeAttribute('sandbox');
        previewContainer.appendChild(controls); previewContainer.appendChild(iframe);
        codeBlockContainer.parentNode.insertBefore(previewContainer, codeBlockContainer.nextSibling);
        try {
            const selfContainedHTML = await createSelfContainedHTML(content, (msg) => { statusContainer.textContent = msg; });
            const blob = new Blob([selfContainedHTML], { type: 'text/html' });
            const url = URL.createObjectURL(blob);
            previewContainer.dataset.blobUrl = url;
            iframe.onload = () => { statusContainer.textContent = 'Render successful! 🎉'; };
            iframe.onerror = () => { statusContainer.textContent = 'Error loading content in iframe.'; statusContainer.style.color = '#d32f2f'; };
            iframe.src = url;
            openInTabBtn.onclick = () => GM_openInTab(url, { active: true });
            openInTabBtn.disabled = false;
        } catch (error) {
            console.error('HTML rendering failed:', error);
            const errorDiv = document.createElement('div'); errorDiv.className = 'preview-error'; errorDiv.textContent = `Fatal Error: ${error.message}`;
            statusContainer.replaceChildren(errorDiv);
        }
    }


    // --- Mermaid Diagram Rendering Engine ---
    function isMermaidCode(content) {
        const keywords = ['C4Context','C4Container','C4Component','C4Dynamic','classDiagram','erDiagram','flowchart','gantt','gitGraph','graph','journey','mindmap','pie','quadrantChart','requirementDiagram','sequenceDiagram','stateDiagram','timeline'];
        const trimmed = content.trim();
        return keywords.some(k => trimmed.startsWith(k)) || trimmed.startsWith('%%{init:');
    }
    function fixGanttSyntax(code) {
        const lines = code.split('\n'); let fixes = [];
        const fixedLines = lines.map((line, index) => {
            let processedLine = line;
            const afterMatches = line.match(/after\s+\w+/g);
            if (afterMatches && afterMatches.length > 1) {
                afterMatches.slice(1).forEach(extraAfter => { processedLine = processedLine.replace(`, ${extraAfter}`, ''); });
                fixes.push(`Line ${index + 1}: Removed extraneous 'after' clauses.`);
            }
            if (line.includes(':')) {
                processedLine = processedLine.replace(/:/g, ':');
                fixes.push(`Line ${index + 1}: Replaced full-width colon.`);
            }
            return processedLine;
        });
        return { code: fixedLines.join('\n'), fixes };
    }
    function fixQuadrantChartSyntax(code) {
        const lines = code.split('\n'); let fixes = [];
        const fixedLines = lines.map((line, index) => {
            const match = line.match(/^( *)(title|x-axis|y-axis|quadrant-\d+) (?!")(.*)$/);
            if (match) {
                const indent = match[1], keyword = match[2]; let text = match[3];
                if (keyword === 'x-axis' || keyword === 'y-axis') {
                    const parts = text.split('-->').map(p => p.trim());
                    if (parts.length === 2) {
                        fixes.push(`Line ${index + 1}: Added quotes to axis labels.`);
                        return `${indent}${keyword} "${parts[0]}" --> "${parts[1]}"`;
                    }
                } else {
                     fixes.push(`Line ${index + 1}: Added quotes to label.`);
                     return `${indent}${keyword} "${text}"`;
                }
            }
            return line;
        });
        return { code: fixedLines.join('\n'), fixes };
    }
    function fixRequirementDiagramSyntax(code) {
        const lines = code.split('\n'); let fixes = [];
        const fixedLines = lines.map((line, index) => {
            const match = line.match(/^( *)text: (?!")(.*)$/);
             if (match) {
                fixes.push(`Line ${index + 1}: Added quotes to 'text' property.`);
                return `${match[1]}text: "${match[2]}"`;
            }
            return line;
        });
        return { code: fixedLines.join('\n'), fixes };
    }
    function preprocessMermaidCode(code) {
        let allFixes = [];
        let processedCode = code.trim();
        if (processedCode.startsWith('gantt')) {
            const result = fixGanttSyntax(processedCode);
            processedCode = result.code;
            allFixes = allFixes.concat(result.fixes);
            if (!processedCode.includes('dateFormat')) {
                const lines = processedCode.split('\n');
                lines.splice(1, 0, '    dateFormat YYYY-MM-DD');
                processedCode = lines.join('\n');
                allFixes.push('Added default `dateFormat`.');
            }
        } else if (processedCode.startsWith('quadrantChart')) {
            const result = fixQuadrantChartSyntax(processedCode);
            processedCode = result.code;
            allFixes = allFixes.concat(result.fixes);
        } else if (processedCode.startsWith('requirementDiagram')) {
            const result = fixRequirementDiagramSyntax(processedCode);
            processedCode = result.code;
            allFixes = allFixes.concat(result.fixes);
        }
        return { code: processedCode, fixes: allFixes };
    }
    async function renderMermaid(content, codeBlockContainer) {
        const previewContainer = document.createElement('div');
        previewContainer.className = 'mermaid-preview-container';
        previewContainer.textContent = '🎨 Rendering diagram with Kroki.io...';
        codeBlockContainer.parentNode.insertBefore(previewContainer, codeBlockContainer.nextSibling);
        try {
            const { code, fixes } = preprocessMermaidCode(content);
            if (DEBUG) { console.log("Processed Mermaid Code:\n", code); console.log("Fixes applied:", fixes); }
            const svg = await fetchResource('https://kroki.io/mermaid/svg', 'POST', code, 'text');
            safeSetHTML(previewContainer, svg);

            const svgElement = previewContainer.querySelector('svg');
            if (svgElement && !svgElement.querySelector('text > tspan[x="10"]')) {
                const successBadge = document.createElement('div');
                successBadge.className = 'mermaid-success-badge';
                successBadge.textContent = '✓ Rendered';
                previewContainer.appendChild(successBadge);
                if (fixes.length > 0) {
                    const warningBadge = document.createElement('div');
                    warningBadge.className = 'mermaid-warning-badge';
                    warningBadge.textContent = '⚠️ Auto-fixed';
                    warningBadge.title = 'Applied fixes:\n' + fixes.join('\n');
                    previewContainer.appendChild(warningBadge);
                }
            }
        } catch (error) {
            console.error('Mermaid rendering error:', error);
            const errorContainer = document.createElement('div');
            errorContainer.className = 'preview-error';
            if (error.message.includes('</svg>')) {
                 safeSetHTML(errorContainer, error.message.substring(error.message.indexOf('<?xml')));
            } else { errorContainer.textContent = `Mermaid Render Failed:\n${error.message}`; }
            previewContainer.replaceChildren(errorContainer);
        }
    }


    // --- Pollinations.ai Image Rendering Engine ---
    async function renderPollinationsLink(node) {
        if (node.tagName !== 'A' || !node.href || node.dataset.rendered) return;

        let imageUrl = null;
        if (node.href.startsWith('https://image.pollinations.ai/prompt/')) {
            imageUrl = node.href;
        } else if (node.href.includes('google.com/search?q=https://image.pollinations.ai/prompt/')) {
            imageUrl = new URL(node.href).searchParams.get('q');
        }

        if (!imageUrl) return;

        node.dataset.rendered = 'true';
        const placeholder = document.createElement('div');
        placeholder.textContent = 'Loading Pollinations image...';
        placeholder.style.cssText = 'padding: 10px; border: 1px dashed #ccc; display: inline-block;';
        node.parentNode.replaceChild(placeholder, node);

        try {
            const imageBlob = await fetchResource(imageUrl, 'GET', null, 'blob');
            const imageUrlObject = URL.createObjectURL(imageBlob);
            const img = document.createElement('img');
            img.src = imageUrlObject;
            img.style.cssText = 'max-width: 100%; height: auto; display: block; border-radius: 8px;';
            img.onload = () => placeholder.parentNode.replaceChild(img, placeholder);
            img.onerror = () => { throw new Error("Image could not be loaded into element."); };
        } catch (error) {
            placeholder.textContent = `Image failed to load: ${error.message}`;
            placeholder.style.color = 'red';
            console.error(`Pollinations render error for ${imageUrl}:`, error);
        }
    }


    // --- Main Script Logic & Observer ---
    function addRenderButton(codeBlockContainer) {
        if (codeBlockContainer.querySelector('.render-preview-button')) return;
        const header = codeBlockContainer.querySelector('.code-block-decoration');
        const codeElement = codeBlockContainer.querySelector('pre > code');
        if (!header || !codeElement) return;

        const content = codeElement.textContent || '';
        const lang = header.querySelector('span')?.textContent.trim().toLowerCase() || '';
        const isHtml = lang === 'html';
        const isMermaid = lang === 'mermaid' || isMermaidCode(content);
        if (!isHtml && !isMermaid) return;

        const button = document.createElement('button');
        button.className = 'render-preview-button';
        let buttonText = '', renderFn = null;

        if (isMermaid) {
            buttonText = '📊 Render Diagram';
            button.classList.add('mermaid-button');
            renderFn = () => renderMermaid(content, codeBlockContainer);
        } else {
            buttonText = '▶️ Render HTML';
            if (content.toLowerCase().includes('echarts')) {
                buttonText = '📈 Render ECharts';
                button.classList.add('echarts-button');
            }
            renderFn = () => renderHTML(content, codeBlockContainer);
        }
        button.innerText = buttonText;

        button.onclick = async (e) => {
            e.stopPropagation();
            const existingPreview = codeBlockContainer.nextElementSibling;
            if (existingPreview?.matches('.preview-container, .mermaid-preview-container')) {
                if (existingPreview.dataset.blobUrl) URL.revokeObjectURL(existingPreview.dataset.blobUrl);
                existingPreview.remove();
                button.innerText = buttonText;
                button.disabled = false;
            } else {
                button.disabled = true;
                button.innerText = '⏳ Rendering...';
                await renderFn();
                button.innerText = '❌ Close Preview';
                button.disabled = false;
            }
        };

        const buttonsDiv = header.querySelector('.buttons');
        if (buttonsDiv) {
            buttonsDiv.prepend(button);
        } else {
            if (DEBUG) console.warn('Renderer script: ".buttons" div not found. Appending to header as fallback.');
            header.appendChild(button);
        }
    }

    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            for (const addedNode of mutation.addedNodes) {
                if (addedNode.nodeType !== 1) continue; // Ensure it's an element

                // --- Find and process new Code Blocks ---
                if (addedNode.matches('div.code-block')) {
                    addRenderButton(addedNode);
                }
                addedNode.querySelectorAll('div.code-block').forEach(addRenderButton);

                // --- Find and process new Pollinations Links ---
                const linkSelector = 'a[href*="image.pollinations.ai/prompt/"]';
                if (addedNode.matches(linkSelector)) {
                    renderPollinationsLink(addedNode);
                }
                addedNode.querySelectorAll(linkSelector).forEach(renderPollinationsLink);
            }
        }
    });

    if (DEBUG) console.log(`Gemini Advanced Renderer (v1.0) is active.`);

    // Start observing the document body for changes
    observer.observe(document.body, { childList: true, subtree: true });

    // Initial run for any content already on the page when the script loads
    document.querySelectorAll('div.code-block').forEach(addRenderButton);
    document.querySelectorAll('a[href*="image.pollinations.ai/prompt/"]').forEach(renderPollinationsLink);

})();