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