Modubox HTML JavaScript注入器

Renders HTML code blocks on modubox.ai chat pages.

As of 06.07.2025. See апошняя версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Modubox HTML JavaScript注入器
// @namespace    http://tampermonkey.net/
// @version      0.12
// @description  Renders HTML code blocks on modubox.ai chat pages.
// @author       You
// @match        https://modubox.ai/*
// @match        https://www.sexyai.top/*
// @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'

  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).');

            let isAdjusting = false;

            const adjustHeight = () => {
                if (isAdjusting) return;
                isAdjusting = true;

                try {
                    const doc = iframeEl.contentWindow.document;
                    if (!doc || !doc.documentElement) return;

                    const newHeight = Math.ceil(doc.documentElement.scrollHeight);
                    const currentHeight = Math.ceil(parseFloat(iframeEl.style.height || '0'));

                    if (currentHeight !== newHeight) {
                        iframeEl.style.height = newHeight + 'px';
                    }
                } catch (e) {
                    console.error('Modubox HTML Renderer: Failed to calculate iframe height.', e);
                }

                // Use requestAnimationFrame to wait for the next frame before allowing another adjustment.
                requestAnimationFrame(() => {
                    isAdjusting = false;
                });
            };

            // --- ROBUST DYNAMIC HEIGHT ADJUSTMENT (REPLACES RESIZEOBSERVER) ---
            const setupHeightAdjustment = () => {
                const win = iframeEl.contentWindow;
                const doc = win.document;

                if (!win || !doc || !doc.body) {
                    console.error('Modubox HTML Renderer: Iframe content window or document not ready for height adjustment setup.');
                    return;
                }

                // 1. Initial adjustment
                adjustHeight();

                // 2. Adjust after all initial resources (like images) are loaded.
                win.addEventListener('load', adjustHeight);

                // 3. Use MutationObserver for dynamic content changes (e.g., from scripts).
                const mutationObserver = new MutationObserver(adjustHeight);
                mutationObserver.observe(doc.body, { childList: true, subtree: true, attributes: true });

                // 4. Fallback for images that might load later or be added dynamically.
                const images = doc.getElementsByTagName('img');
                for (const img of images) {
                    img.addEventListener('load', adjustHeight);
                    img.addEventListener('error', adjustHeight); // Also adjust if an image fails to load
                }
            };

            // The 'load' event on the iframe itself is the entry point.
            setupHeightAdjustment();
          });

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