Modubox HTML Renderer(2)

Renders HTML code blocks on modubox.ai chat pages.

Versione datata 05/07/2025. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Modubox HTML Renderer(2)
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Renders HTML code blocks on modubox.ai chat pages.
// @author       You
// @match        https://modubox.ai/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const APP_READY_SELECTOR = 'uni-app'; // A selector for an element that exists when the app is ready
    const RENDER_TARGET_SELECTOR = 'pre:not([data-rendered]) code';

    console.log('Modubox HTML Renderer: Script started.');

    // --- Styles ---
    GM_addStyle(`
        .rendered-html-container {
            border: 1px solid #ddd;
            padding: 10px;
            margin: 10px 0;
            background-color: #fff;
        }
        .render-toggle-button {
            display: block;
            margin-top: 10px;
            font-size: 12px;
            cursor: pointer;
            padding: 2px 8px;
            border: 1px solid #ccc;
            background-color: #f0f0f0;
            border-radius: 4px;
        }

        .rendered-html-iframe {
            width: 100%;
            border: 1px solid #ddd;
            background-color: #fff;
            margin: 10px 0;
        }
        #modubox-renderer-settings-btn {
            position: fixed;
            top: 150px;
            right: 20px;
            z-index: 10001;
            padding: 8px 12px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        #modubox-renderer-settings-panel {
            position: fixed;
            top: 200px;
            right: 20px;
            background: white;
            border: 1px solid #ccc;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            padding: 15px;
            z-index: 10000;
            display: none;
            border-radius: 5px;
        }
    `);

    let displayMode = GM_getValue('displayMode', 'hide'); // 'hide' or 'show'

    // --- Utility: Debounce ---
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    function applyDisplayModeForElement(preElement) {
        if (displayMode === 'hide') {
            preElement.hide();
        } else {
            preElement.show();
        }
    }

    function applyDisplayModeToAll() {
        $('.rendered-html-container').each(function() {
            applyDisplayModeForElement($(this).find('pre'));
        });
    }

    function createSettingsPanel() {
        if ($('#modubox-renderer-settings-btn').length) return;

        const panel = $(`
            <div id="modubox-renderer-settings-panel">
                <h4>HTML渲染器设置</h4>
                <div>
                    <label><input type="radio" name="displayMode" value="hide"> 隐藏原始代码</label><br>
                    <label><input type="radio" name="displayMode" value="show"> 显示原始代码</label>
                </div>
                <button id="close-settings" style="margin-top: 10px;">关闭</button>
            </div>
        `);

        const settingsButton = $('<button id="modubox-renderer-settings-btn">HTML渲染器</button>');

        $('body').append(settingsButton).append(panel);

        panel.find(`input[name="displayMode"][value="${displayMode}"]`).prop('checked', true);

        settingsButton.on('click', () => panel.toggle());
        panel.find('#close-settings').on('click', () => panel.hide());
        panel.find('input[name="displayMode"]').on('change', function() {
            displayMode = $(this).val();
            GM_setValue('displayMode', displayMode);
            console.log(`Modubox HTML Renderer: 显示模式已设置为: ${displayMode}`);
            applyDisplayModeToAll();
        });
        console.log('Modubox HTML Renderer: 设置面板已创建。');
    }

    // This function processes a collection of code elements to render them.
    function processElements(elements) {
        elements.each(function() {
            try {
                const codeElement = $(this);
                const codeText = codeElement.text();
                const codeHtml = codeElement.html();

                // We check the raw HTML for an escaped version of <!DOCTYPE html> which highlight.js might create,
                // or the plain text version.
                const isHtmlBlock = /&lt;!DOCTYPE html&gt;/i.test(codeHtml) || /<!DOCTYPE html>/i.test(codeText);

                if (isHtmlBlock) {
                    const preElement = codeElement.closest('pre');
                    if (preElement.attr('data-rendered')) return;

                    console.log('Modubox HTML Renderer: Found HTML block to render.');
                    let htmlContent = codeElement.text();
                    preElement.attr('data-rendered', 'true');

                    // --- SMARTER FIX for escaped HTML by highlight.js ---
                    if (htmlContent.includes('&lt;') && htmlContent.includes('&gt;')) {
                        console.log('Modubox HTML Renderer: Detected escaped HTML, decoding entities...');
                        const tempDiv = document.createElement('div');
                        tempDiv.innerHTML = htmlContent;
                        htmlContent = tempDiv.textContent || tempDiv.innerText || "";
                    } else {
                        console.log('Modubox HTML Renderer: Content appears to be raw HTML, skipping decoding.');
                    }
                    // --- END FIX ---

                    // --- FIX for malformed HTML: Use a TreeWalker to find all CSS rules in text nodes and move them to a <style> tag in the <head> ---
                    try {
                        const docParser = new DOMParser();
                        const tempDoc = docParser.parseFromString(htmlContent, 'text/html');
                        const bodyNode = tempDoc.body;
                        let cssContent = '';
                        const nodesToRemove = [];

                        const filter = {
                            acceptNode: function(node) {
                                if (node.parentNode && (node.parentNode.nodeName.toUpperCase() === 'SCRIPT' || node.parentNode.nodeName.toUpperCase() === 'STYLE')) {
                                    return NodeFilter.FILTER_REJECT;
                                }
                                return NodeFilter.FILTER_ACCEPT;
                            }
                        };

                        const walker = tempDoc.createTreeWalker(bodyNode, NodeFilter.SHOW_TEXT, filter, false);
                        let node;
                        while(node = walker.nextNode()) {
                            const trimmedValue = node.nodeValue.trim();
                            if (trimmedValue.length > 5 && trimmedValue.includes('{') && trimmedValue.includes('}') && (trimmedValue.includes(':') || trimmedValue.startsWith('@'))) {
                                cssContent += trimmedValue + '\n';
                                nodesToRemove.push(node);
                            }
                        }

                        if (cssContent) {
                            console.log('Modubox HTML Renderer: Found and extracted potential CSS from text nodes.');
                            nodesToRemove.forEach(node => {
                                if (node.parentNode) {
                                    node.parentNode.removeChild(node);
                                }
                            });

                            const styleTag = tempDoc.createElement('style');
                            styleTag.textContent = cssContent;
                            tempDoc.head.appendChild(styleTag);

                            htmlContent = tempDoc.documentElement.outerHTML;
                            console.log('Modubox HTML Renderer: Injected corrected CSS into head.');
                        }
                    } catch(e) {
                        console.error('Modubox HTML Renderer: Failed during HTML correction.', e);
                    }
                    // --- END FIX ---

                    // --- CSP WORKAROUND: Use a Blob URL instead of srcdoc ---
                    const blob = new Blob([htmlContent], { type: 'text/html' });
                    const url = URL.createObjectURL(blob);

                    const iframe = $(`<iframe class="rendered-html-iframe"></iframe>`);
                    iframe.attr('src', url);

                    // Clean up the object URL when the iframe is removed to prevent memory leaks.
                    iframe.on('remove', function() {
                        URL.revokeObjectURL(url);
                    });

                    iframe.on('load', function() {
                        const iframeEl = this;
                        console.log('Modubox HTML Renderer: Iframe loaded with srcdoc (unsandboxed).');

                        const adjustHeight = () => {
                            try {
                                const doc = iframeEl.contentWindow.document;
                                if (!doc) return false;
                                const body = doc.body;
                                const html = doc.documentElement;
                                const height = Math.max(
                                    body.scrollHeight, body.offsetHeight,
                                    html.clientHeight, html.scrollHeight, html.offsetHeight
                                );
                                const newHeight = height + 20; // Add a small buffer

                                // Only update if the height is significantly different to prevent infinite loops.
                                if (Math.abs(parseInt(iframeEl.style.height || '0') - newHeight) > 1) {
                                     iframeEl.style.height = newHeight + 'px';
                                     return true; // Indicate that a change was made
                                }
                            } catch (e) {
                                console.error("Modubox HTML Renderer: Failed to calculate iframe height.", e);
                            }
                            return false; // No change made
                        };

                        // --- DYNAMIC HEIGHT ADJUSTMENT WITH DEBOUNCED RESIZEOBSERVER ---
                        const debouncedAdjust = debounce(() => {
                            if (adjustHeight()) {
                                console.log('Modubox HTML Renderer: Iframe height readjusted by ResizeObserver.');
                            }
                        }, 100); // 100ms debounce delay

                        try {
                            const doc = iframeEl.contentWindow.document;
                            if (doc && doc.body) {
                                const resizeObserver = new iframeEl.contentWindow.ResizeObserver(debouncedAdjust);
                                resizeObserver.observe(doc.body);
                            }
                        } catch (e) {
                            console.error("Modubox HTML Renderer: Failed to set up ResizeObserver for iframe.", e);
                        }

                        // Also run an initial adjustment after a short delay as a fallback.
                        setTimeout(() => {
                            if(adjustHeight()) {
                                console.log(`Modubox HTML Renderer: Initial iframe height adjusted.`);
                            }
                        }, 300); // A shorter delay is fine as ResizeObserver will take over.
                    });

                    const container = $('<div class="rendered-html-container" data-renderer-managed></div>');
                    const toggleButton = $('<button class="render-toggle-button">显示/隐藏原始代码</button>');

                    // Append the iframe and button to the new container
                    container.append(iframe).append(toggleButton);

                    // Insert the container *after* the original <pre> element instead of replacing it.
                    preElement.after(container);

                    // The button now toggles the original pre element.
                    toggleButton.on('click', () => preElement.toggle());

                    // Apply the initial display mode to the original pre element.
                    applyDisplayModeForElement(preElement);
                }
            } catch (e) {
                console.error('Modubox HTML Renderer: Error processing a code block.', e);
            }
        });
    }

    function startObserver() {
        console.log('Modubox HTML Renderer: Starting MutationObserver.');
        const observer = new MutationObserver((mutationsList) => {
            for(const mutation of mutationsList) {
                // --- Defensive check: Ignore mutations within our own rendered containers ---
                if (mutation.target && $(mutation.target).closest('[data-renderer-managed]').length) {
                    continue;
                }

                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1 && !$(node).closest('[data-renderer-managed]').length) { // ELEMENT_NODE and not inside our container
                            const newCodeElements = $(node).find(RENDER_TARGET_SELECTOR).addBack(RENDER_TARGET_SELECTOR);
                            if (newCodeElements.length > 0) {
                                console.log('Modubox HTML Renderer: Detected new code blocks via MutationObserver.');
                                processElements(newCodeElements);
                            }
                        }
                    });
                }
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
        console.log('Modubox HTML Renderer: MutationObserver is now observing the document body.');
    }

    function initialize() {
        console.log('Modubox HTML Renderer: 初始化...');
        try {
            createSettingsPanel();
            // Process any elements that are already on the page
            processElements($(RENDER_TARGET_SELECTOR));
            // Start observing for future changes
            startObserver();
            console.log('Modubox HTML Renderer: 初始化完成,观察者已启动。');
        } catch (e) {
            console.error('Modubox HTML Renderer: 初始化失败。', e);
        }
    }

    // --- Robust Initialization for SPA ---
    console.log(`Modubox HTML Renderer: Waiting for app to be ready ('${APP_READY_SELECTOR}')...`);
    const initInterval = setInterval(() => {
        if ($(APP_READY_SELECTOR).length) {
            console.log('Modubox HTML Renderer: App is ready!');
            clearInterval(initInterval);
            // A small delay can still be helpful for everything to settle.
            setTimeout(initialize, 500);
        }
    }, 500);

})();