您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
// ==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); })();