Modubox HTML Renderer(2)

Renders HTML code blocks on modubox.ai chat pages.

ของเมื่อวันที่ 05-07-2025 ดู เวอร์ชันล่าสุด

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

})();