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.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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);

})();