Greasy Fork 支持简体中文。

ChatGPT 渲染HTML代码

chatgpt渲染html代码块,像Claude一样实时预览html ( chatgpt renders HTML code blocks, previewing HTML in real-time like Claude )

// ==UserScript==
// @name         ChatGPT 渲染HTML代码
// @namespace    https://github.com/21888/chatgpt-render-html
// @version      1.0.3
// @description  chatgpt渲染html代码块,像Claude一样实时预览html ( chatgpt renders HTML code blocks, previewing HTML in real-time like Claude )
// @match        https://chatgpt.com/c/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com
// @grant        none
// @license      GPL-2.0-only
// ==/UserScript==
(function () {
    "use strict";
    // 使用 MutationObserver 监听 DOM 变化
    const observer = new MutationObserver(debounce(xuanranHTML, 500));
    const createIframeObserver = new MutationObserver(
        debounce(createIframe, 500)
    );

    // 观察目标节点的变化
    observer.observe(document.body, { childList: true, subtree: true });
    createIframeObserver.observe(document.body, {
        childList: true,
        subtree: true,
    });

    // 首次调用渲染
    window.addEventListener("load", () => {
        setTimeout(xuanranHTML, 1000);
        // 创建一个iframe
        setTimeout(createIframe, 1000);
    });
})();

function createIframe() {
    // 判断是否已经创建 dynamicContentIframe
    if (document.getElementById("dynamicContentIframe")) {
        console.log("已经创建 dynamicContentIframe");
        return;
    }

    // 创建一个基于main标签的兄弟iframe元素
    const mainElement = document.querySelector("main");
    // mainElement.style.display = "flex";
    mainElement.style.overflow = "hidden";
    if (mainElement) {
        const iframe = document.createElement("iframe");
        iframe.id = "dynamicContentIframe"; // 添加id以便动态修改内容
        iframe.style.display = "relative";
        iframe.style.width = "100%";
        iframe.style.height = "100%";
        iframe.style.backgroundColor = "#FFFDF6"; // 添加奶白色背景
        // sandbox="allow-scripts"
        iframe.sandbox = "allow-scripts";
        iframe.srcdoc = "<html><body></body></html>";
        // 创建切换显示代码块的div
        const toggleCodeButton = document.createElement("div");
        toggleCodeButton.id = "toggleCodeButton";
        toggleCodeButton.style.position = "absolute";
        toggleCodeButton.style.top = "10px";
        toggleCodeButton.style.right = "40px";
        toggleCodeButton.style.cursor = "pointer";
        toggleCodeButton.innerHTML = `
            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M4 6H16M4 10H16M4 14H16" stroke="black" stroke-width="2" stroke-linecap="round"/>
            </svg>
        `;
        toggleCodeButton.onclick = () => {
            // document.getElementById("dynamicContentIframe").contentWindow.document.body.innerHTML = "123";
            document.getElementById("codeContainer").style.display =
                document.getElementById("codeContainer").style.display ===
                "none"
                    ? "block"
                    : "none";
        };

        // 创建缩小按钮
        const minimizeButton = document.createElement("div");
        minimizeButton.id = "minimizeButton";
        minimizeButton.style.position = "absolute";
        minimizeButton.style.top = "10px";
        minimizeButton.style.right = "10px";
        minimizeButton.style.cursor = "pointer";
        minimizeButton.innerHTML = `
            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M4 10H16" stroke="black" stroke-width="2" stroke-linecap="round"/>
            </svg>
        `;
        minimizeButton.onclick = () => {
            iframe.style.display =
                iframe.style.display === "none" ? "block" : "none";
            document.querySelector("main").style.display =
                document.querySelector("main").style.display === "block"
                    ? "flex"
                    : "block";
            document
                .querySelector(
                    "body > div.relative.flex.h-full.w-full.overflow-hidden.transition-colors.z-0 > div.flex-shrink-0.overflow-x-hidden.bg-token-sidebar-surface-primary.max-md\\:\\!w-0 > div > div > div > nav > div.flex.justify-between.flex.h-\\[60px\\].items-center.md\\:h-header-height > span > button"
                )
                .click();
        };
        //  toggleCodeButton 和 minimizeButton 添加到一个div下
        const buttonContainer = document.createElement("div");
        buttonContainer.id = "buttonContainer";
        buttonContainer.style.position = "relative";
        buttonContainer.style.top = "10px";
        buttonContainer.style.right = "10px";
        buttonContainer.appendChild(minimizeButton);
        buttonContainer.appendChild(toggleCodeButton);

        // 直接将 iframe 和按钮添加到 main 元素下
        mainElement.appendChild(iframe);
        mainElement.appendChild(buttonContainer);

        // 设置 main 元素为相对定位,以便正确定位最小化按钮
        mainElement.style.position = "relative";

        // mainElement.insertBefore(container, mainElement.nextSibling);
        // 创建一个用户存放显示代码的div
        const codeContainer = document.createElement("div");
        codeContainer.id = "codeContainer";
        codeContainer.style.width = "100%";
        codeContainer.style.height = "100%";
        codeContainer.style.zIndex = "1000";
        // codeContainer.style.backgroundColor = "#FFFDF6"; // 添加奶白色背景
        codeContainer.style.display = "block";
        codeContainer.style.overflow = "hidden";
        codeContainer.style.overflowY = "scroll";
        codeContainer.style.display = "none";
        mainElement.appendChild(codeContainer);
    } else {
        console.error("Main element not found");
    }
    // 显示
    document.getElementById("dynamicContentIframe").style.display = "block";
}
function xuanranHTML() {
    // 获取所有的 code 元素
    const codes = document.querySelectorAll(".overflow-y-auto.p-4 code");

    // 遍历所有的 code 元素
    codes.forEach((codeElement) => {
        // 检查是否已经处理过,避免重复插入
        if (codeElement.classList.contains("processed")) {
            return;
        }

        // 标记该元素已经处理过
        codeElement.classList.add("processed");

        // 判断 codeElement.textContent 是否是 HTML 内容
        if (codeElement.parentNode.parentNode.children[0].innerText != "html") {
            console.log(
                "不渲染: " +
                    codeElement.parentNode.parentNode.children[0].innerText
            );
            return;
        }
        // 隐藏codeElement
        codeElement.parentNode.parentNode.style.display = "none";
        let componentContainer = renderSmallWindow(codeElement);
    });
}

function renderSmallWindow(codeElement) {
    // 创建组件容器
    const componentContainer = document.createElement("div");
    componentContainer.style.display = "flex";
    componentContainer.style.alignItems = "center";
    componentContainer.style.border = "1px solid #e5e7eb";
    componentContainer.style.borderRadius = "8px";
    componentContainer.style.padding = "10px";
    componentContainer.style.backgroundColor = "#f9fafb";
    componentContainer.style.marginBottom = "20px";
    componentContainer.style.cursor = "pointer";

    // 创建图标容器
    const iconContainer = document.createElement("div");
    iconContainer.style.borderRight = "1px solid #e5e7eb";
    iconContainer.style.paddingRight = "10px";

    // 创建 SVG 图标
    const svgIcon = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "svg"
    );
    svgIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg");
    svgIcon.setAttribute("style", "height: 24px; width: 24px; color: #6b7280;");
    svgIcon.setAttribute("fill", "none");
    svgIcon.setAttribute("viewBox", "0 0 24 24");
    svgIcon.setAttribute("stroke", "currentColor");
    svgIcon.setAttribute("stroke-width", "2");

    const pathElement = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "path"
    );
    pathElement.setAttribute("stroke-linecap", "round");
    pathElement.setAttribute("stroke-linejoin", "round");
    pathElement.setAttribute(
        "d",
        "M16 8c0-1.104-.9-2-2-2H6c-1.1 0-2 .896-2 2v8c0 1.104.9 2 2 2h8c1.1 0 2-.896 2-2V8zm-4 4h.01m-3 0h.01M7 12h.01m0 0h.01M12 7v.01M8 7v.01M7 7v.01M12 8v.01m-5 0v.01m-1 0v.01"
    );

    svgIcon.appendChild(pathElement);
    iconContainer.appendChild(svgIcon);

    // 创建文本容器
    const textContainer = document.createElement("div");
    textContainer.style.marginLeft = "10px";

    // 创建标题
    const title = document.createElement("h3");
    title.textContent = codeElement.parentNode.parentNode.children[0].innerText;
    title.style.margin = "0";
    title.style.fontSize = "16px";
    title.style.fontWeight = "600";
    title.style.color = "#374151";

    // 创建描述
    const description = document.createElement("p");
    description.textContent = "预览页面 / 刷新渲染";
    description.style.margin = "0";
    description.style.fontSize = "14px";
    description.style.color = "#6b7280";

    textContainer.appendChild(title);
    textContainer.appendChild(description);

    // 组装组件
    componentContainer.appendChild(iconContainer);
    componentContainer.appendChild(textContainer);

    // 添加点击事件,切换代码显示状态
    componentContainer.addEventListener("click", function () {
        const codeParent = codeElement.parentNode.parentNode;
        const mainElement = document.querySelector("main");
        // codeParent.style.display = codeParent.style.display === 'none' ? 'block' : 'none';
        // 隐藏展开左侧历史记录
        if (mainElement.style.display != "flex") {
            mainElement.style.display = "flex";
        }
        if (
            document.querySelector(
                "body > div.relative.flex.h-full.w-full.overflow-hidden.transition-colors.z-0 > div.flex-shrink-0.overflow-x-hidden.bg-token-sidebar-surface-primary.max-md\\:\\!w-0"
            ).style.width != "0px"
        ) {
            document
                .querySelector(
                    "body > div.relative.flex.h-full.w-full.overflow-hidden.transition-colors.z-0 > div.flex-shrink-0.overflow-x-hidden.bg-token-sidebar-surface-primary.max-md\\:\\!w-0 > div > div > div > nav > div.flex.justify-between.flex.h-\\[60px\\].items-center.md\\:h-header-height > span > button"
                )
                .click();
            // dynamicContentIframe
            document.getElementById("dynamicContentIframe").style.display =
                "block";
        }

        // iframe 赋值
        renderIframeContent(
            document.getElementById("dynamicContentIframe"),
            codeElement.textContent
        );
        // 拦截script标签

        // 设置代码容器代码
        // document.getElementById("codeContainer").innerHTML = codeElement.parentNode.parentNode.innerHTML;
        const codeContainer = document.getElementById("codeContainer");
        codeContainer.innerHTML = "";
        const clonedContent = codeElement.parentNode.parentNode.cloneNode(true);

        codeContainer.appendChild(clonedContent);
        codeContainer.childNodes[0].style.display = "contents";

        // 查找并处理按钮
        const buttons = clonedContent.querySelectorAll("button");
        buttons.forEach((button, index) => {
            console.log(button);
            if (button.className == "flex gap-1 items-center py-1") {
                button.addEventListener("click", function (event) {
                    button.innerText = " success √ ";
                    setTimeout(() => {
                        button.innerText = "Copy code";
                    }, 1000);
                });
            }
            button.addEventListener("click", function (event) {
                event.stopPropagation(); // 阻止事件冒泡到父元素
                // 找到原始按钮并模拟点击
                const originalButtons =
                    codeElement.parentNode.parentNode.querySelectorAll(
                        "button"
                    );
                if (originalButtons[index]) {
                    originalButtons[index].click();
                }
            });
        });

        // 添加事件委托到 codeContainer(用于处理代码元素的点击)
        codeContainer.addEventListener("click", function (event) {
            const clickedElement = event.target.closest("code");
            if (clickedElement) {
                // 模拟原始代码元素的点击行为
                const originalCodeElement = codeElement.closest("code");
                if (originalCodeElement) {
                    originalCodeElement.click();
                }
            }
        });
    });

    // 将组件插入到代码元素之前
    codeElement.parentNode.parentNode.insertAdjacentElement(
        "beforebegin",
        componentContainer
    );
    return componentContainer;
}
function renderIframeContent(iframe, content) {
    // console.log(__remixContext.state.loaderData.root.cspScriptNonce);
    // Add nonce to script tags in content
    const nonce = __remixContext.state.loaderData.root.cspScriptNonce;
    content = content.replace(/<script/g, `<script nonce="${nonce}"`);
    iframe.srcdoc = content;
    return;
    // 获取 iframe 内部的 document 对象
    const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;

    // 将 code 元素的内容设置为 iframe 的内容
    iframeDoc.open();

    if (true) {
        // 刷新特效,true为有感刷新,!true为无痕刷新
        // iframeDoc.write(`loading`);
        // 生成一个随机的nonce值
        const nonce = generateNonce();

        // 在content中添加nonce
        // content = content.replace(/<script/g, `<script nonce="${nonce}"`);
        // 设置meta头替换
        // content = content.replace(/<meta/g, `<meta nonce="${nonce}"`);
        // let doms = new DOMParser().parseFromString(content, "text/html");

        // Remove script tags from content
        // content = content.replace(
        //     /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
        //     ""
        // );
        // 插入
        iframeDoc.write(content);
        doms.querySelectorAll("script").forEach((script) => {
            return;
            console.log("script");
            const newScript = iframeDoc.createElement("script");
            newScript.textContent = script.textContent;
            newScript.nonce = nonce;
            // iframeDoc.documentElement.appendChild(newScript);
            // 创建一个blob URL来加载脚本
            const blob = new Blob([script.textContent], {
                type: "application/javascript",
            });
            const scriptUrl = URL.createObjectURL(blob);
            newScript.src = scriptUrl;
            script.onload = () => {
                console.log("加载完成");
                URL.revokeObjectURL(scriptUrl);
            };
            iframeDoc.documentElement.appendChild(newScript);
        });

        // setTimeout(() => iframeDoc.write(content), 100);
    } else {
        iframeDoc.write(content);
    }
    iframeDoc.close();
}

function debounce(func, wait) {
    let timeout;
    return function () {
        const context = this,
            args = arguments;
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(context, args), wait);
    };
}
function generateNonce() {
    return (
        Math.random().toString(36).substring(2, 15) +
        Math.random().toString(36).substring(2, 15)
    );
}