ChatGPT-ComposerFormatGuard

Keep pasted and typed Markdown literal in the ChatGPT composer until it is sent.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name			ChatGPT-ComposerFormatGuard
// @name:zh-CN		ChatGPT-输入格式保护
// @namespace		https://github.com/Mehver
// @version		1.0
// @description		Keep pasted and typed Markdown literal in the ChatGPT composer until it is sent.
// @description:zh-CN	让粘贴和键入的 Markdown 在 ChatGPT 输入框中保持原样,发送后再由 ChatGPT 渲染。
// @author		https://github.com/Mehver
// @icon			data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IB2cksfwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAA1RpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiI+CjxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CjxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOklwdGM0eG1wQ29yZT0iaHR0cDovL2lwdGMub3JnL3N0ZC9JcHRjNHhtcENvcmUvMS4wL3htbG5zLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSI0MDAwIiBleGlmOlBpeGVsWURpbWVuc2lvbj0iNDAwMCI+CjwvcmRmOkRlc2NyaXB0aW9uPgo8L3JkZjpSREY+CjwveDp4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
// @match		https://chatgpt.com/*
// @match		https://chat.openai.com/*
// @license		BSD-3-Clause
// @charset		UTF-8
// @homepageURL		https://github.com/MehverSynRGB/ChatGPT-ComposerFormatGuard
// @contributionURL	https://github.com/MehverSynRGB/ChatGPT-ComposerFormatGuard
// @copyright		Copyright (c) 2026 Mehver (https://github.com/Mehver). All rights reserved.
// @grant			GM_getValue
// @grant			GM_setValue
// @run-at		document-start
// @noframes
// ==/UserScript==

(function () {
    'use strict';

    const COMPOSER_SELECTOR = '#thread-bottom form[data-type="unified-composer"] #prompt-textarea.ProseMirror[contenteditable="true"], form[data-type="unified-composer"] #prompt-textarea.ProseMirror[contenteditable="true"], #prompt-textarea.ProseMirror[contenteditable="true"]';
    const STORAGE_KEY = 'chatgptComposerFormatGuardEnabled';
    const TOGGLE_ID = 'chatgpt-composer-format-guard-toggle';
    const FORMAT_SHORTCUTS = new Set(['b', 'i', 'u']);
    const LIST_INPUT_TYPES = new Set(['insertOrderedList', 'insertUnorderedList']);
    const HTML_BLOCK_TAGS = new Set(['ADDRESS', 'ARTICLE', 'ASIDE', 'BLOCKQUOTE', 'DD', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE', 'FOOTER', 'FORM', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', 'HR', 'LI', 'MAIN', 'NAV', 'OL', 'P', 'PRE', 'SECTION', 'TABLE', 'TD', 'TH', 'TR', 'UL']);
    const HTML_IGNORED_TAGS = new Set(['EMBED', 'IFRAME', 'NOSCRIPT', 'OBJECT', 'SCRIPT', 'STYLE', 'TEMPLATE', 'TITLE']);

    let enabled = getEnabledPreference();
    let composing = false;

    function getComposer(target) {
        return target instanceof Element ? target.closest(COMPOSER_SELECTOR) : null;
    }

    function containsFile(clipboardData) {
        return Array.from(clipboardData.items || []).some(item => item.kind === 'file');
    }

    function getClipboardText(clipboardData) {
        const plainText = clipboardData.getData('text/plain');
        if (plainText) return plainText;

        const html = clipboardData.getData('text/html');
        if (!html) return '';

        const parsed = new DOMParser().parseFromString(html, 'text/html');
        const text = [];

        function appendBreak() {
            if (text.length && !text[text.length - 1].endsWith('\n')) text.push('\n');
        }

        function isBlockBoundary(node) {
            return node && node.nodeType === 1 && (node.tagName === 'BR' || HTML_BLOCK_TAGS.has(node.tagName));
        }

        function readNode(node) {
            if (node.nodeType === 3) {
                const value = node.nodeValue || '';
                const previous = node.previousSibling;
                const next = node.nextSibling;
                const isPreformatted = node.parentElement && node.parentElement.closest('pre, textarea');
                if (!value.trim() && !isPreformatted) {
                    if (isBlockBoundary(previous) || isBlockBoundary(next)) return;
                    text.push(/[\r\n\t]/.test(value) ? ' ' : value);
                    return;
                }

                text.push(value);
                return;
            }
            if (node.nodeType !== 1) return;
            if (HTML_IGNORED_TAGS.has(node.tagName)) return;

            if (node.tagName === 'BR') {
                text.push('\n');
                return;
            }

            const isBlock = HTML_BLOCK_TAGS.has(node.tagName);
            if (isBlock) appendBreak();
            for (const child of node.childNodes) readNode(child);
            if (isBlock) appendBreak();
        }

        readNode(parsed.body);
        return text.join('').replace(/^\n+|\n+$/g, '');
    }

    function getEnabledPreference() {
        try {
            if (typeof GM_getValue === 'function') return GM_getValue(STORAGE_KEY, true) !== false;
        } catch (error) {
            console.warn('[ChatGPT-ComposerFormatGuard] Unable to read the saved setting.', error);
        }

        return localStorage.getItem(STORAGE_KEY) !== 'false';
    }

    function setEnabledPreference(value) {
        enabled = value;

        try {
            if (typeof GM_setValue === 'function') {
                GM_setValue(STORAGE_KEY, value);
            } else {
                localStorage.setItem(STORAGE_KEY, String(value));
            }
        } catch (error) {
            console.warn('[ChatGPT-ComposerFormatGuard] Unable to save the setting.', error);
        }

        updateToggle();
    }

    function createPlainTextPasteEvent(text) {
        try {
            const rawClipboard = new DataTransfer();
            rawClipboard.setData('text/plain', text.replace(/\r\n?/g, '\n'));
            return new ClipboardEvent('paste', {
                bubbles: true,
                cancelable: true,
                composed: true,
                clipboardData: rawClipboard
            });
        } catch (error) {
            console.warn('[ChatGPT-ComposerFormatGuard] Unable to create a plain-text paste event.', error);
            return null;
        }
    }

    document.addEventListener('paste', event => {
        // Ignore our replacement event and leave attachments for ChatGPT to handle.
        if (!enabled || composing || !event.isTrusted || event.defaultPrevented) return;

        const composer = getComposer(event.target);
        const clipboardData = event.clipboardData;
        if (!composer || !clipboardData || containsFile(clipboardData)) return;

        const plainText = getClipboardText(clipboardData);
        if (!plainText) return;

        const rawPasteEvent = createPlainTextPasteEvent(plainText);
        if (!rawPasteEvent) return;

        event.preventDefault();
        event.stopImmediatePropagation();
        composer.dispatchEvent(rawPasteEvent);
    }, true);

    document.addEventListener('beforeinput', event => {
        if (!enabled || composing || !event.isTrusted || event.defaultPrevented || event.isComposing) return;

        const composer = getComposer(event.target);
        if (!composer) return;

        const inputType = event.inputType || '';
        if (inputType === 'insertText' && event.data) {
            const rawPasteEvent = createPlainTextPasteEvent(event.data);
            if (!rawPasteEvent) return;

            event.preventDefault();
            event.stopImmediatePropagation();
            composer.dispatchEvent(rawPasteEvent);
            return;
        }

        if ((inputType.startsWith('format') && inputType !== 'formatSetBlockTextDirection') || LIST_INPUT_TYPES.has(inputType)) {
            event.preventDefault();
            event.stopImmediatePropagation();
        }
    }, true);

    document.addEventListener('keydown', event => {
        if (!enabled || composing || !event.isTrusted || event.defaultPrevented || event.isComposing) return;
        if ((!event.ctrlKey && !event.metaKey) || event.altKey || !FORMAT_SHORTCUTS.has(event.key.toLowerCase())) return;
        if (!getComposer(event.target)) return;

        event.preventDefault();
        event.stopImmediatePropagation();
    }, true);

    document.addEventListener('compositionstart', event => {
        if (getComposer(event.target)) composing = true;
    }, true);

    document.addEventListener('compositionend', event => {
        if (!getComposer(event.target)) return;

        // Keep the final IME commit on the native path even when the browser clears isComposing early.
        window.setTimeout(() => {
            composing = false;
        }, 0);
    }, true);

    function updateToggle() {
        const toggle = document.getElementById(TOGGLE_ID);
        if (!toggle) return;

        toggle.style.background = enabled ? '#10a37f' : '#6e6e80';
        toggle.style.boxShadow = enabled ? '0 2px 10px rgba(16, 163, 127, 0.42)' : '0 2px 10px rgba(0, 0, 0, 0.24)';
        toggle.style.opacity = enabled ? '1' : '0.72';
        toggle.setAttribute('aria-pressed', String(enabled));
        toggle.setAttribute('aria-label', `输入格式保护:${enabled ? '已启用,点击关闭' : '已关闭,点击启用'}`);
        toggle.title = `输入格式保护:${enabled ? '已启用' : '已关闭'}`;
    }

    function mountToggle() {
        if (document.getElementById(TOGGLE_ID)) return true;
        if (!document.querySelector(COMPOSER_SELECTOR) || !document.body) return false;

        const toggle = document.createElement('button');
        toggle.id = TOGGLE_ID;
        toggle.type = 'button';
        toggle.textContent = 'MD';
        toggle.style.cssText = 'all:unset;box-sizing:border-box;position:fixed;right:20px;bottom:96px;z-index:1000;display:flex;align-items:center;justify-content:center;width:42px;height:42px;border:1px solid rgba(255,255,255,0.28);border-radius:50%;color:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;font-weight:700;letter-spacing:-0.5px;cursor:pointer;user-select:none;transition:transform 0.15s ease,opacity 0.15s ease,background 0.15s ease,box-shadow 0.15s ease;';
        toggle.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();
            setEnabledPreference(!enabled);
        });
        toggle.addEventListener('mouseenter', () => {
            toggle.style.transform = 'scale(1.08)';
        });
        toggle.addEventListener('mouseleave', () => {
            toggle.style.transform = '';
        });

        document.body.appendChild(toggle);
        updateToggle();
        return true;
    }

    function installToggle() {
        mountToggle();

        let scheduled = false;
        const observer = new MutationObserver(() => {
            if (document.getElementById(TOGGLE_ID) || scheduled) return;

            scheduled = true;
            window.requestAnimationFrame(() => {
                scheduled = false;
                mountToggle();
            });
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', installToggle, { once: true });
    } else {
        installToggle();
    }

    console.log('Script ChatGPT-ComposerFormatGuard (ChatGPT-输入格式保护) loaded successfully. https://github.com/MehverSynRGB/ChatGPT-ComposerFormatGuard');
})();