Convert HTML formatting to Unicode characters when copying text from AI chat websites
// ==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);
});
}
})();