Created
August 17, 2025 20:48
-
-
Save Taik/f8cea74f527f7585a027604d862131c0 to your computer and use it in GitHub Desktop.
ChatGPT Copy Cleaner
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name ChatGPT Copy Cleaner | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2025-08-06 | |
| // @description Cleans text copied from ChatGPT | |
| // @author Thinh Nguyen | |
| // @match https://chat.openai.com/* | |
| // @match https://chatgpt.com/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (() => { | |
| /* ───────── Config ───────── */ | |
| const BUTTON_COPY_DELAY = 60; // ms – delay before we patch the clipboard | |
| const COPY_BUTTON_SELECTOR = 'button[aria-label="Copy"],button[data-testid="copy-turn-action-button"]'; // Centralized selector | |
| /* ───────── Utils ───────── */ | |
| // Invisible/control characters to strip entirely | |
| const INVISIBLES = /[\u00AD\u180E\u200B-\u200D\u2060-\u2064\u2066-\u206F\u200E\u200F\u202A-\u202E\uFEFF]/g; | |
| const VARIATION_SELECTORS = /[\uFE00-\uFE0F]/g; | |
| // Exotic spaces to normalize to regular space | |
| const EXOTIC_SPACES = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g; | |
| // All dash/hyphen variants to normalize to regular hyphen | |
| const DASHES = /[‐-‒–—―⁃−﹣-]/g; | |
| // Smart quotes to normalize to ASCII quotes | |
| const CURLY_SINGLE = /[''‚‛]/g; | |
| const CURLY_DOUBLE = /[""„‟]/g; | |
| // Ellipsis to normalize to three dots | |
| const ELLIPSIS = /\u2026/g; | |
| const filterText = t => { | |
| // Guard against very large strings to prevent freezing | |
| if (!t || typeof t !== 'string' || t.length > 1000000) return t; | |
| return t | |
| .replace(INVISIBLES, '') | |
| .replace(VARIATION_SELECTORS, '') | |
| .replace(EXOTIC_SPACES, ' ') | |
| .replace(DASHES, '-') | |
| .replace(CURLY_SINGLE, "'") | |
| .replace(CURLY_DOUBLE, '"') | |
| .replace(ELLIPSIS, '...') | |
| .replace(/ {2,}/g, ' '); // Collapse multiple spaces | |
| }; | |
| const unchanged = (a, b) => a === b; // fast equality | |
| /* ───────── Logging ───────── */ | |
| const log = (raw, clean, origin) => { | |
| if (unchanged(raw, clean)) return; | |
| console.log(`[ChatGPT-CC] COPY (${origin})\n raw : ${JSON.stringify(raw)}\n clean : ${JSON.stringify(clean)}`); | |
| }; | |
| /* ───────── Toast ───────── */ | |
| const showToast = (() => { | |
| let toastElement = null; | |
| let fadeTimer = null; | |
| let removeTimer = null; | |
| return (message = 'Clipboard cleaned') => { | |
| // Create toast element only once | |
| if (!toastElement) { | |
| toastElement = document.createElement('div'); | |
| Object.assign(toastElement.style, { | |
| position: 'fixed', | |
| top: '12px', | |
| left: '50%', | |
| transform: 'translateX(-50%)', | |
| background: '#2ecc71', | |
| color: '#fff', | |
| padding: '6px 12px', | |
| borderRadius: '6px', | |
| fontSize: '14px', | |
| fontWeight: '600', | |
| zIndex: 2147483647, | |
| boxShadow: '0 2px 6px rgba(0,0,0,.2)', | |
| opacity: '0', | |
| transition: 'opacity .3s ease' | |
| }); | |
| document.body.appendChild(toastElement); | |
| } | |
| // Clear existing timers | |
| clearTimeout(fadeTimer); | |
| clearTimeout(removeTimer); | |
| // Update content and show | |
| toastElement.textContent = message; | |
| toastElement.style.opacity = '1'; | |
| // Schedule fade and hide | |
| fadeTimer = setTimeout(() => { toastElement.style.opacity = '0' }, 1300); | |
| }; | |
| })(); | |
| /* ───────── Selection helper ───────── */ | |
| const getSelectionText = () => { | |
| const sel = window.getSelection(); | |
| if (sel?.toString()) return sel.toString(); | |
| const el = document.activeElement; | |
| if (el && (el.tagName === 'TEXTAREA' || | |
| (el.tagName === 'INPUT' && el.type === 'text'))) { | |
| return el.value.slice(el.selectionStart, el.selectionEnd); | |
| } | |
| return ''; | |
| }; | |
| /* ───────── Clipboard helper ───────── */ | |
| const setClipboardData = (event, text) => { | |
| event.clipboardData.setData('text/plain', text); | |
| event.stopImmediatePropagation(); | |
| event.preventDefault(); | |
| }; | |
| /* ───────── Generic copy/cut handler ───────── */ | |
| const onCopyCut = e => { | |
| const cd = e.clipboardData; | |
| if (!cd) return; | |
| // Path 1: page already populated clipboardData | |
| if (cd.types?.includes('text/plain')) { | |
| const raw = cd.getData('text/plain'); | |
| const clean = filterText(raw); | |
| if (unchanged(raw, clean)) return; | |
| setClipboardData(e, clean); | |
| log(raw, clean, 'clipboardData'); | |
| showToast(); | |
| return; | |
| } | |
| // Path 2: normal selection | |
| const rawSel = getSelectionText(); | |
| if (!rawSel) return; | |
| const cleanSel = filterText(rawSel); | |
| if (unchanged(rawSel, cleanSel)) return; | |
| setClipboardData(e, cleanSel); | |
| log(rawSel, cleanSel, 'selection'); | |
| showToast(); | |
| }; | |
| document.addEventListener('copy', onCopyCut, true); | |
| document.addEventListener('cut', onCopyCut, true); | |
| /* ───────── “Copy”‑button fallback ───────── */ | |
| document.addEventListener('click', e => { | |
| const btn = e.target.closest(COPY_BUTTON_SELECTOR); | |
| if (!btn) return; | |
| // Retry mechanism for reliable clipboard access | |
| const attemptClipboardPatch = async (retries = 5, delay = 50) => { | |
| // Guard against missing clipboard API | |
| if (!navigator.clipboard) return; | |
| // Skip if document is not focused to prevent retry loops | |
| if (!document.hasFocus()) return; | |
| try { | |
| const raw = await navigator.clipboard.readText(); | |
| // If clipboard is empty, it might not be ready. Retry. | |
| if (!raw && retries > 0) { | |
| setTimeout(() => attemptClipboardPatch(retries - 1, delay), delay); | |
| return; | |
| } | |
| const clean = filterText(raw); | |
| if (unchanged(raw, clean)) return; | |
| await navigator.clipboard.writeText(clean); | |
| log(raw, clean, 'button'); | |
| showToast(); | |
| } catch (err) { | |
| // Only log error if it's not a permission denied or not focused error | |
| if (err.name !== 'NotAllowedError' && err.name !== 'DOMException') { | |
| console.warn('[ChatGPT-CC] Button copy failed:', err); | |
| } | |
| } | |
| }; | |
| // Start the clipboard patching process | |
| setTimeout(() => attemptClipboardPatch(), BUTTON_COPY_DELAY); | |
| }, true); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment