ChatGPT-ComposerFormatGuard

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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