Copy HTML formatting into Unicode Supported Formatting

Convert HTML formatting to Unicode characters when copying text from AI chat websites

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         Copy HTML formatting into Unicode Supported Formatting
// @namespace    www.fiverr.com/web_coder_nsd
// @version      1.0.8
// @description  Convert HTML formatting to Unicode characters when copying text from AI chat websites
// @author       noushadBug
// @license      MIT
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @match        https://deepseek.com/*
// @match        https://chat.deepseek.com/*
// @match        https://gemini.google.com/*
// @match        https://z.ai/*
// @match        https://chat.z.ai/*
// @match        https://claude.ai/*
// @match        https://perplexity.ai/*
// @match        https://www.perplexity.ai/*
// @match        https://poe.com/*
// @match        https://copilot.microsoft.com/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    const IS_CHATGPT = /chatgpt\.com|chat\.openai\.com/.test(location.hostname);

    // ═══════════════════════════════════════════════════════════════
    // UNICODE CHARACTER MAPPINGS
    // ═══════════════════════════════════════════════════════════════

    const BOLD = {
        'A':'𝗔','B':'𝗕','C':'𝗖','D':'𝗗','E':'𝗘','F':'𝗙','G':'𝗚','H':'𝗛','I':'𝗜','J':'𝗝',
        'K':'𝗞','L':'𝗟','M':'𝗠','N':'𝗡','O':'𝗢','P':'𝗣','Q':'𝗤','R':'𝗥','S':'𝗦','T':'𝗧',
        'U':'𝗨','V':'𝗩','W':'𝗪','X':'𝗫','Y':'𝗬','Z':'𝗭',
        'a':'𝗮','b':'𝗯','c':'𝗰','d':'𝗱','e':'𝗲','f':'𝗳','g':'𝗴','h':'𝗵','i':'𝗶','j':'𝗷',
        'k':'𝗸','l':'𝗹','m':'𝗺','n':'𝗻','o':'𝗼','p':'𝗽','q':'𝗾','r':'𝗿','s':'𝘀','t':'𝘁',
        'u':'𝘂','v':'𝘃','w':'𝘄','x':'𝘅','y':'𝘆','z':'𝘇',
        '0':'𝟬','1':'𝟭','2':'𝟮','3':'𝟯','4':'𝟰','5':'𝟱','6':'𝟲','7':'𝟳','8':'𝟴','9':'𝟵'
    };
    const ITALIC = {
        'A':'𝘈','B':'𝘉','C':'𝘊','D':'𝘋','E':'𝘌','F':'𝘍','G':'𝘎','H':'𝘏','I':'𝘐','J':'𝘑',
        'K':'𝘒','L':'𝘓','M':'𝘔','N':'𝘕','O':'𝘖','P':'𝘗','Q':'𝘘','R':'𝘙','S':'𝘚','T':'𝘛',
        'U':'𝘜','V':'𝘝','W':'𝘞','X':'𝘟','Y':'𝘠','Z':'𝘡',
        'a':'𝘢','b':'𝘣','c':'𝘤','d':'𝘥','e':'𝘦','f':'𝘧','g':'𝘨','h':'𝗁','i':'𝘪','j':'𝘫',
        'k':'𝘬','l':'𝘭','m':'𝘮','n':'𝘯','o':'𝘰','p':'𝘱','q':'𝘲','r':'𝘳','s':'𝘴','t':'𝘵',
        'u':'𝘶','v':'𝘷','w':'𝘸','x':'𝘹','y':'𝘺','z':'𝘻'
    };
    const MONO = {
        'A':'𝙰','B':'𝙱','C':'𝙲','D':'𝙳','E':'𝙴','F':'𝙵','G':'𝙶','H':'𝙷','I':'𝙸','J':'𝙹',
        'K':'𝙺','L':'𝙻','M':'𝙼','N':'𝙽','O':'𝙾','P':'𝙿','Q':'𝚀','R':'𝚁','S':'𝚂','T':'𝚃',
        'U':'𝚄','V':'𝚅','W':'𝚆','X':'𝚇','Y':'𝚈','Z':'𝚉',
        'a':'𝚊','b':'𝚋','c':'𝚌','d':'𝚍','e':'𝚎','f':'𝚏','g':'𝚐','h':'𝚑','i':'𝚒','j':'𝚓',
        'k':'𝚔','l':'𝚕','m':'𝚖','n':'𝚗','o':'𝚘','p':'𝚙','q':'𝚚','r':'𝚛','s':'𝚜','t':'𝚝',
        'u':'𝚞','v':'𝚟','w':'𝚠','x':'𝚡','y':'𝚢','z':'𝚣',
        '0':'𝟶','1':'𝟷','2':'𝟸','3':'𝟹','4':'𝟺','5':'𝟻','6':'𝟼','7':'𝟽','8':'𝟾','9':'𝟿'
    };
    const UNDERLINE = '\u0332';

    // ═══════════════════════════════════════════════════════════════
    // CONVERTER FUNCTIONS
    // ═══════════════════════════════════════════════════════════════

    function toBold(str)   { let r=''; for (const c of str) r += BOLD[c]  ||c; return r; }
    function toItalic(str) { let r=''; for (const c of str) r += ITALIC[c]||c; return r; }
    function toMono(str)   { let r=''; for (const c of str) r += MONO[c]  ||c; return r; }

    // ═══════════════════════════════════════════════════════════════
    // HTML → UNICODE
    // ═══════════════════════════════════════════════════════════════

    function htmlToUnicode(root) {
        const result = [];
        let listDepth = 0, listType = [], listCounters = [];

        function process(node, fmt) {
            if (!node) return;

            if (node.nodeType === Node.TEXT_NODE) {
                let text = node.textContent;
                if (!text) return;
                if (fmt.mono) {
                    text = toMono(text);
                } else {
                    if (fmt.bold && fmt.italic) text = toBold(toItalic(text));
                    else if (fmt.bold)   text = toBold(text);
                    else if (fmt.italic) text = toItalic(text);
                    if (fmt.underline) {
                        let t = '';
                        for (const c of text) t += c + UNDERLINE;
                        text = t;
                    }
                }
                result.push(text);
                return;
            }

            if (node.nodeType !== Node.ELEMENT_NODE) return;

            const tag = node.tagName.toLowerCase();
            const nf  = { ...fmt };

            if (tag === 'strong' || tag === 'b') nf.bold = true;
            if (tag === 'em'     || tag === 'i') nf.italic = true;
            if (tag === 'code'   || tag === 'kbd' || tag === 'samp') nf.mono = true;
            if (tag === 'u'      || tag === 'ins') nf.underline = true;

            let prefix = '', suffix = '';

            switch (tag) {
                case 'h1': prefix='\n❒ '; nf.bold=true; suffix='\n'; break;
                case 'h2': prefix='\n➜ '; nf.bold=true; suffix='\n'; break;
                case 'h3': case 'h4': case 'h5': case 'h6':
                    prefix='\n▸ '; nf.bold=true; suffix='\n'; break;
                case 'li': {
                    const indent = '    '.repeat(Math.max(0, listDepth-1));
                    if (listType[listDepth-1]==='ol') {
                        listCounters[listDepth-1] = (listCounters[listDepth-1]||0)+1;
                        const n = listCounters[listDepth-1];
                        const pn = ['⑴','⑵','⑶','⑷','⑸','⑹','⑺','⑻','⑼','⑽','⑾','⑿','⒀','⒁','⒂','⒃','⒄','⒅','⒆','⒇'];
                        prefix = indent + (n<=20 ? pn[n-1] : n+'.') + ' ';
                    } else {
                        const bullets = ['◉','•','•','•','•'];
                        prefix = indent + bullets[Math.min(listDepth-1, bullets.length-1)] + ' ';
                    }
                    suffix = '\n';
                    break;
                }
                case 'ul': case 'ol':
                    if (listDepth > 0) result.push('\n');
                    listDepth++; listType.push(tag); listCounters.push(0);
                    for (const child of node.childNodes) process(child, nf);
                    listDepth--; listType.pop(); listCounters.pop();
                    if (listDepth === 0) result.push('\n');
                    return;
                case 'blockquote': prefix='│ '; suffix='\n'; break;
                case 'hr': result.push('\n────────────────────\n'); return;
                case 'br': result.push('\n'); return;
                case 'p': case 'div': if (listDepth===0) suffix='\n'; break;
                case 'pre': suffix='\n'; break;
                case 'del': case 's': case 'strike': prefix='–'; suffix='–'; break;
            }

            if (prefix) result.push(prefix);
            for (const child of node.childNodes) process(child, nf);
            if (suffix) result.push(suffix);
        }

        process(root, { bold:false, italic:false, mono:false, underline:false });

        return result.join('')
            .replace(/\n{3,}/g, '\n\n')
            .replace(/\n{2}([◉•⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇])/g, '\n$1')
            .trim();
    }

    // ═══════════════════════════════════════════════════════════════
    // CLIPBOARD
    // ═══════════════════════════════════════════════════════════════

    async function copyToClipboard(text) {
        if (navigator.clipboard && navigator.clipboard.writeText) {
            await navigator.clipboard.writeText(text);
            return;
        }
        const ta = document.createElement('textarea');
        ta.value = text;
        ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0';
        document.body.appendChild(ta);
        ta.select();
        document.execCommand('copy');
        document.body.removeChild(ta);
    }

    // ═══════════════════════════════════════════════════════════════
    // STYLES
    // ═══════════════════════════════════════════════════════════════

    function ensureStyles() {
        if (document.getElementById('mdu-styles')) return;
        const css = document.createElement('style');
        css.id = 'mdu-styles';
        css.textContent = `
            /* ── Floating persistent button (ChatGPT only) ── */
            #mdu-float {
                position: fixed !important;
                bottom: 24px !important;
                right: 24px !important;
                z-index: 2147483647 !important;
                display: flex !important;
                align-items: center;
                gap: 7px;
                background: linear-gradient(135deg, #6366f1, #8b5cf6) !important;
                border: none !important;
                border-radius: 999px !important;
                padding: 10px 20px !important;
                cursor: pointer !important;
                color: #fff !important;
                font-size: 13px !important;
                font-weight: 600 !important;
                font-family: system-ui, -apple-system, sans-serif !important;
                box-shadow: 0 4px 18px rgba(99,102,241,0.45) !important;
                transition: background 0.2s, opacity 0.2s;
                user-select: none;
                opacity: 0.35;
                pointer-events: auto !important;
            }
            #mdu-float:hover  { opacity: 1 !important; transform: scale(1.04); }
            #mdu-float:active { opacity: 0.8 !important; transform: scale(0.96); }
            #mdu-float.has-sel { opacity: 1 !important; }
            #mdu-float.done { background: linear-gradient(135deg,#10b981,#059669) !important; }
            #mdu-float svg  { width:15px; height:15px; flex-shrink:0; }

            /* ── Selection popup (non-ChatGPT sites) ── */
            #mdu-popup {
                position: fixed;
                z-index: 2147483647;
                display: none;
                font-family: system-ui, -apple-system, sans-serif;
                pointer-events: none;
            }
            #mdu-popup.show {
                display: block;
                animation: mdu-in 0.15s ease-out;
                pointer-events: auto;
            }
            @keyframes mdu-in {
                from { opacity:0; transform:translateY(6px); }
                to   { opacity:1; transform:translateY(0); }
            }
            #mdu-btn {
                display: flex;
                align-items: center;
                gap: 6px;
                background: linear-gradient(135deg, #6366f1, #8b5cf6);
                border: none;
                border-radius: 10px;
                padding: 9px 14px;
                cursor: pointer;
                color: #fff;
                font-size: 13px;
                font-weight: 600;
                box-shadow: 0 4px 14px rgba(99,102,241,0.4);
                transition: transform 0.1s;
            }
            #mdu-btn:hover  { transform: scale(1.04); }
            #mdu-btn:active { transform: scale(0.96); }
            #mdu-btn.done   { background: linear-gradient(135deg,#10b981,#059669); }
            #mdu-btn svg    { width:16px; height:16px; }

            /* ── Toast ── */
            .mdu-toast {
                position: fixed !important;
                bottom: 80px !important;
                right: 24px !important;
                background: #10b981 !important;
                color: #fff !important;
                padding: 10px 20px !important;
                border-radius: 8px !important;
                font-size: 13px !important;
                font-weight: 500 !important;
                font-family: system-ui, -apple-system, sans-serif !important;
                z-index: 2147483647 !important;
                animation: mdu-toast-in 0.2s ease-out;
                box-shadow: 0 4px 14px rgba(0,0,0,0.15) !important;
                pointer-events: none !important;
            }
            @keyframes mdu-toast-in {
                from { opacity:0; transform:translateY(10px); }
                to   { opacity:1; transform:translateY(0); }
            }
        `;
        (document.head || document.documentElement).appendChild(css);
    }

    ensureStyles();

    // ═══════════════════════════════════════════════════════════════
    // TOAST
    // ═══════════════════════════════════════════════════════════════

    function toast(msg, duration = 2000) {
        document.querySelectorAll('.mdu-toast').forEach(el => el.remove());
        const el = document.createElement('div');
        el.className = 'mdu-toast';
        el.textContent = msg;
        document.body.appendChild(el);
        setTimeout(() => el.remove(), duration);
    }

    // ═══════════════════════════════════════════════════════════════
    // SHARED STATE
    // ═══════════════════════════════════════════════════════════════

    let savedRange = null;
    let savedText  = '';

    function snapshotSelection() {
        const sel = window.getSelection();
        if (!sel || sel.rangeCount === 0) return false;
        const text = sel.toString().trim();
        if (!text) return false;
        try {
            savedRange = sel.getRangeAt(0).cloneRange();
            savedText  = text;
            return true;
        } catch (_) { return false; }
    }

    async function doCopy(btnEl, labelEl) {
        if (!savedRange) { toast('Select some text first'); return; }
        try {
            const frag    = savedRange.cloneContents();
            const wrapper = document.createElement('div');
            wrapper.appendChild(frag);
            const unicode = htmlToUnicode(wrapper);
            if (!unicode) { toast('Nothing to copy'); return; }
            await copyToClipboard(unicode);
            btnEl.classList.add('done');
            if (labelEl) labelEl.textContent = 'Copied!';
            toast('Copied as Unicode ✓');
            setTimeout(() => {
                btnEl.classList.remove('done');
                if (labelEl) labelEl.textContent = 'Copy Unicode';
            }, 1500);
        } catch (err) {
            console.error('[mdu]', err);
            toast('Copy failed: ' + err.message);
        }
    }

    // ═══════════════════════════════════════════════════════════════
    // INPUT CHECKER
    // ═══════════════════════════════════════════════════════════════

    // Returns true if the element is a standard text input or textarea
    function isTextInput(el) {
        if (!el || !el.tagName) return false;
        const tag = el.tagName.toUpperCase();
        if (tag === 'TEXTAREA') return true;
        if (tag === 'INPUT') {
            const type = (el.type || 'text').toLowerCase();
            // Types that contain text selection
            return ['text', 'search', 'email', 'password', 'url', 'tel', 'number'].includes(type);
        }
        return false;
    }

    // ═══════════════════════════════════════════════════════════════
    // CHATGPT — PERSISTENT FLOATING BUTTON
    // ═══════════════════════════════════════════════════════════════

    if (IS_CHATGPT) {

        function updateFloatState(floatBtn) {
            const sel = window.getSelection();
            const hasText = sel && sel.toString().trim().length > 0;
            if (hasText) {
                snapshotSelection();
                floatBtn.classList.add('has-sel');
                floatBtn.title = 'Copy selected text as Unicode';
            } else if (savedText) {
                floatBtn.classList.add('has-sel');
                floatBtn.title = 'Copy last selection as Unicode';
            } else {
                floatBtn.classList.remove('has-sel');
                floatBtn.title = 'Select text, then click to copy as Unicode';
            }
        }

        function injectFloatBtn() {
            if (document.getElementById('mdu-float')) return;

            const floatBtn = document.createElement('button');
            floatBtn.id   = 'mdu-float';
            floatBtn.type = 'button';
            floatBtn.title = 'Select text, then click to copy as Unicode';
            floatBtn.innerHTML = `
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <rect x="9" y="9" width="13" height="13" rx="2"/>
                    <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
                </svg>
                <span id="mdu-float-label">Copy Unicode</span>`;

            document.body.appendChild(floatBtn);
            const floatLabel = floatBtn.querySelector('#mdu-float-label');

            document.addEventListener('selectionchange', () => updateFloatState(floatBtn));

            document.addEventListener('pointerup', () => {
                Promise.resolve().then(snapshotSelection);
            }, { capture: true, passive: true });

            floatBtn.addEventListener('click', async e => {
                e.preventDefault();
                e.stopPropagation();
                await doCopy(floatBtn, floatLabel);
            });

            updateFloatState(floatBtn);
        }

        injectFloatBtn();

        const guardian = new MutationObserver(() => {
            ensureStyles();
            if (!document.getElementById('mdu-float')) {
                injectFloatBtn();
            }
        });

        guardian.observe(document.body, { childList: true, subtree: false });

        const _pushState    = history.pushState.bind(history);
        const _replaceState = history.replaceState.bind(history);

        history.pushState = function (...args) {
            _pushState(...args);
            setTimeout(injectFloatBtn, 300);
        };
        history.replaceState = function (...args) {
            _replaceState(...args);
            setTimeout(injectFloatBtn, 300);
        };

        window.addEventListener('popstate', () => setTimeout(injectFloatBtn, 300));

    } else {

        // ═══════════════════════════════════════════════════════════
        // ALL OTHER SITES — SELECTION POPUP
        // ═══════════════════════════════════════════════════════════

        const popup = document.createElement('div');
        popup.id = 'mdu-popup';
        popup.innerHTML = `
            <button id="mdu-btn" type="button">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <rect x="9" y="9" width="13" height="13" rx="2"/>
                    <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
                </svg>
                <span id="mdu-label">Copy Unicode</span>
            </button>`;
        document.body.appendChild(popup);

        const btn   = popup.querySelector('#mdu-btn');
        const label = popup.querySelector('#mdu-label');

        let popupVisible = false;

        function showPopup(x, y) {
            popup.style.left = Math.max(10, Math.min(x-70, innerWidth-160)) + 'px';
            popup.style.top  = Math.max(10, y-55) + 'px';
            btn.classList.remove('done');
            label.textContent = 'Copy Unicode';
            popup.classList.add('show');
            popupVisible = true;
        }

        function hidePopup() {
            popup.classList.remove('show');
            popupVisible = false;
        }

        document.addEventListener('selectionchange', () => {
            // If focus is inside a text input, do not process selection.
            // This prevents cursor glitches in chat inputs (e.g. chat.z.ai).
            if (isTextInput(document.activeElement)) return;

            const sel = window.getSelection();
            if (sel && sel.toString().trim()) snapshotSelection();
        });

        document.addEventListener('pointerdown', e => {
            if (!popup.contains(e.target) && popupVisible) hidePopup();
        }, true);

        document.addEventListener('pointerup', e => {
            if (popup.contains(e.target)) return;

            // FIX: Ignore clicks inside text inputs (like the chat.z.ai textarea)
            // This prevents the cursor desync issue and unnecessary popup checks.
            if (isTextInput(e.target)) return;

            const x = e.clientX, y = e.clientY;
            Promise.resolve().then(() => {
                if (snapshotSelection()) {
                    showPopup(x, y);
                }
            });
        }, true);

        document.addEventListener('keyup', e => {
            if (!e.shiftKey) return;
            // Ignore if inside input
            if (isTextInput(document.activeElement)) return;

            setTimeout(() => {
                if (!snapshotSelection()) return;
                try {
                    const rect = window.getSelection().getRangeAt(0).getBoundingClientRect();
                    showPopup(rect.left + rect.width/2, rect.top);
                } catch (_) {}
            }, 10);
        });

        document.addEventListener('keydown', e => {
            if (e.key === 'Escape') hidePopup();
        });

        btn.addEventListener('click', async e => {
            e.preventDefault();
            e.stopPropagation();
            await doCopy(btn, label);
            setTimeout(hidePopup, 900);
        });
    }

})();