Highlights quoted text with customizable colors, styles, and features
// ==UserScript==
// @name 💬 Quote Highlighter
// @version 1.2
// @description Highlights quoted text with customizable colors, styles, and features
// @author Misspent
// @namespace ChatGPT / Grok AI
// @icon https://i.imgur.com/bhdcGzd.png
// @match *://*/*
// @exclude /^https?:\/\/(www\.)?(x\.com|youtube\.com|github\.com|twitter\.com|steam\.com|search.brave\.com|twitch\.tv)(\/.*)?$/
// @grant GM_addStyle
// @run-at document-idle
// @license MIT
// ==/UserScript==
// https://www.iconsdb.com/icons/preview/orange/quote-xxl.png
(function () {
'use strict';
// ================== CONFIGURATION ==================
const CONFIG = {
// Highlight appearance
textColor: '#EA9D9C',
backgroundColor: 'rgba(234, 157, 156, 0.12)',
borderRadius: '0px',
padding: '0 2px',
// fontWeight: '500',
// Enable subtle underline instead of bg
underline: false,
// Ignore short quoted text
minimumLength: 2,
// Prevent absurdly large matches
maximumLength: 500,
// Ignore these tags entirely
ignoredTags: new Set([
'SCRIPT',
'STYLE',
'TEXTAREA',
'INPUT',
'CODE',
'PRE',
'NOSCRIPT',
'OPTION',
// Mine
'a',
'video',
'[href]',
'.flair',
'.subtitle',
'#subtitle',
'#ace-editor',
'.no-highlight',
'.flair-content',
'#TitleClipboard',
'clipboard-notification'
]),
// Ignore editable areas
ignoreContentEditable: true,
// MutationObserver debounce
observerDebounce: 150,
// Debug mode
debug: false,
// Your regex
quoteRegex:
/(?<!\w)(["“”““'‘`"])(.*?)(["””””'’`.])(?!\w)/g
};
// ================== STYLES ==================
GM_addStyle(`
.tm-quote-highlight {
color: ${CONFIG.textColor} !important;
background: ${CONFIG.underline ? 'transparent' : CONFIG.backgroundColor};
border-radius: ${CONFIG.borderRadius};
padding: ${CONFIG.padding};
font-weight: ${CONFIG.fontWeight};
transition: all 0.15s ease;
cursor: text;
}
.tm-quote-highlight:hover {
filter: brightness(1.08);
}
${
CONFIG.underline
? `
.tm-quote-highlight {
text-decoration: underline;
text-decoration-color: ${CONFIG.textColor};
text-decoration-thickness: 2px;
}
`
: ''
}
`);
// ================== HELPERS ==================
function log(...args) {
if (CONFIG.debug) {
console.log('[QuoteHighlighter]', ...args);
}
}
function shouldSkipNode(node) {
if (!node || !node.parentNode) return true;
const parent = node.parentNode;
if (parent.classList?.contains('tm-quote-highlight')) {
return true;
}
if (CONFIG.ignoredTags.has(parent.nodeName)) {
return true;
}
if (
CONFIG.ignoreContentEditable &&
parent.closest('[contenteditable="true"]')
) {
return true;
}
return false;
}
// ================== MAIN HIGHLIGHT FUNCTION ==================
function processTextNode(textNode) {
if (shouldSkipNode(textNode)) return;
const text = textNode.nodeValue;
if (!text || !text.trim()) return;
CONFIG.quoteRegex.lastIndex = 0;
const matches = [...text.matchAll(CONFIG.quoteRegex)];
if (!matches.length) return;
const fragment = document.createDocumentFragment();
let lastIndex = 0;
for (const match of matches) {
const fullMatch = match[0];
if (
fullMatch.length < CONFIG.minimumLength ||
fullMatch.length > CONFIG.maximumLength
) {
continue;
}
const start = match.index;
// Normal text before match
if (start > lastIndex) {
fragment.appendChild(
document.createTextNode(
text.slice(lastIndex, start)
)
);
}
// Highlighted quote
const span = document.createElement('span');
span.className = 'tm-quote-highlight';
span.textContent = fullMatch;
// Tooltip
span.title = 'Quoted from Script';
fragment.appendChild(span);
lastIndex = start + fullMatch.length;
}
// Remaining text
if (lastIndex < text.length) {
fragment.appendChild(
document.createTextNode(text.slice(lastIndex))
);
}
// Replace only if something changed
if (fragment.childNodes.length) {
textNode.parentNode.replaceChild(fragment, textNode);
}
}
// ================== TREE WALKER ==================
function scan(root = document.body) {
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
null
);
const textNodes = [];
while (walker.nextNode()) {
textNodes.push(walker.currentNode);
}
for (const node of textNodes) {
processTextNode(node);
}
log(`Processed ${textNodes.length} text nodes`);
}
// ================== LIVE PAGE SUPPORT ==================
let debounceTimer;
const observer = new MutationObserver((mutations) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.TEXT_NODE) {
processTextNode(node);
} else if (node.nodeType === Node.ELEMENT_NODE) {
scan(node);
}
}
}
}, CONFIG.observerDebounce);
});
// ================== START ==================
function init() {
log('Initializing...');
scan(document.body);
observer.observe(document.body, {
childList: true,
subtree: true
});
log('Observer started');
}
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ================== OPTIONAL GLOBAL API (Useful for debugging in DevTools) ==================
window.QuoteHighlighter = {
rescan: () => scan(document.body),
disconnect: () => observer.disconnect(),
config: CONFIG
};
})();
/* 📌 Grok Version 1:
// ⭐ Features:
- Customizable highlight color (default `#EA9D9C`)
- Multiple highlight styles (text color, background, both, underline)
- Click to copy quote
- Tooltip on hover
- Persistent settings via TamperMonkey storage
- Settings menu (accessible via TamperMonkey dashboard)
- Smart mutation observer for dynamic content (Twitter/X, Reddit, news sites, etc.)
- Debounced processing to avoid performance issues
- Ignores script/style tags
// off
// GM_addStyle
// GM_getValue
// GM_setValue
// GM_registerMenuCommand
// document-end
(() => {
'use strict';
// ================== CONFIGURATION ==================
const CONFIG = {
enabled: GM_getValue('enabled', true),
color: GM_getValue('color', '#EA9D9C'),
background: GM_getValue('background', 'rgba(234, 157, 156, 0.15)'),
style: GM_getValue('style', 'color'), // 'color', 'background', 'both', 'underline'
borderRadius: GM_getValue('borderRadius', '0px'),
padding: GM_getValue('padding', '0px 2px'),
showTooltip: GM_getValue('showTooltip', true),
copyOnClick: GM_getValue('copyOnClick', true),
debounceTime: 800,
};
// Regex for various quote types
const QUOTE_REGEX = /(?<!\w)(["“”'"‘’`])(.*?)(["”'"’`])/g;
let observer = null;
// ================== STYLES ==================
function addGlobalStyles() {
GM_addStyle(`
.quote-highlight {
color: ${CONFIG.color};
background: ${CONFIG.style === 'background' || CONFIG.style === 'both' ? CONFIG.background : 'transparent'};
padding: ${CONFIG.padding};
border-radius: ${CONFIG.borderRadius};
${CONFIG.style === 'underline' ? 'text-decoration: underline wavy #EA9D9C;' : ''}
transition: all 0.2s ease;
cursor: pointer;
}
.quote-highlight:hover {
filter: brightness(1.1);
box-shadow: 0 0 0 2px rgba(234, 157, 156, 0.3);
}
`);
}
// ================== CORE FUNCTIONS ==================
function highlightQuotes(node) {
if (!CONFIG.enabled) return;
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
const nodesToProcess = [];
let textNode;
while ((textNode = walker.nextNode())) {
if (textNode.parentNode.tagName === 'SCRIPT' ||
textNode.parentNode.tagName === 'STYLE' ||
textNode.parentNode.classList.contains('quote-highlight')) {
continue;
}
if (QUOTE_REGEX.test(textNode.textContent)) {
nodesToProcess.push(textNode);
}
}
nodesToProcess.forEach(textNode => {
const fragment = document.createDocumentFragment();
let lastIndex = 0;
let match;
// Reset regex
QUOTE_REGEX.lastIndex = 0;
while ((match = QUOTE_REGEX.exec(textNode.textContent)) !== null) {
const [full, openQuote, content, closeQuote] = match;
const start = match.index;
// Add text before match
if (start > lastIndex) {
fragment.appendChild(document.createTextNode(
textNode.textContent.slice(lastIndex, start)
));
}
// Create highlight span
const span = document.createElement('span');
span.className = 'quote-highlight';
span.textContent = full;
if (CONFIG.showTooltip) {
span.title = `Quoted text • Click to copy`;
}
if (CONFIG.copyOnClick) {
span.addEventListener('click', (e) => {
e.stopImmediatePropagation();
navigator.clipboard.writeText(full).then(() => {
const original = span.style.background;
span.style.background = '#A8E6A8';
setTimeout(() => span.style.background = original, 600);
});
});
}
fragment.appendChild(span);
lastIndex = start + full.length;
}
// Add remaining text
if (lastIndex < textNode.textContent.length) {
fragment.appendChild(document.createTextNode(
textNode.textContent.slice(lastIndex)
));
}
textNode.parentNode.replaceChild(fragment, textNode);
});
}
// ================== MUTATION OBSERVER ==================
function startObserver() {
if (observer) observer.disconnect();
observer = new MutationObserver(debounce((mutations) => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) highlightQuotes(node);
});
}
});
}, CONFIG.debounceTime));
observer.observe(document.body, {
childList: true,
subtree: true
});
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// ================== SETTINGS MENU ==================
function registerMenuCommands() {
GM_registerMenuCommand(`Quote Highlighter: ${CONFIG.enabled ? 'ON' : 'OFF'}`, () => {
CONFIG.enabled = !CONFIG.enabled;
GM_setValue('enabled', CONFIG.enabled);
location.reload();
});
GM_registerMenuCommand("Open Settings", openSettings);
}
function openSettings() {
const settingsHTML = `
<div style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#222;color:#eee;padding:20px;border-radius:12px;z-index:999999;width:380px;border:2px solid #EA9D9C;">
<h2 style="margin:0 0 15px 0;color:#EA9D9C;">Quote Highlighter Settings</h2>
<label>Highlight Color: <input type="color" id="color" value="${CONFIG.color}"></label><br><br>
<label>Background: <input type="color" id="bg" value="${CONFIG.background.replace('rgba(234, 157, 156, 0.15)', '#EA9D9C')}"></label><br><br>
<label>Style:
<select id="style">
<option value="color" ${CONFIG.style==='color'?'selected':''}>Text Color Only</option>
<option value="background" ${CONFIG.style==='background'?'selected':''}>Background</option>
<option value="both" ${CONFIG.style==='both'?'selected':''}>Both</option>
<option value="underline" ${CONFIG.style==='underline'?'selected':''}>Underline</option>
</select>
</label><br><br>
<label><input type="checkbox" id="tooltip" ${CONFIG.showTooltip?'checked':''}> Show tooltip</label><br>
<label><input type="checkbox" id="copy" ${CONFIG.copyOnClick?'checked':''}> Click to copy quote</label><br><br>
<button id="save" style="background:#EA9D9C;color:black;padding:8px 16px;border:none;border-radius:6px;cursor:pointer;">Save & Refresh</button>
<button id="close" style="margin-left:10px;padding:8px 16px;">Close</button>
</div>`;
const div = document.createElement('div');
div.innerHTML = settingsHTML;
document.body.appendChild(div);
div.querySelector('#save').onclick = () => {
CONFIG.color = div.querySelector('#color').value;
CONFIG.background = div.querySelector('#bg').value + '33'; // add slight transparency
CONFIG.style = div.querySelector('#style').value;
CONFIG.showTooltip = div.querySelector('#tooltip').checked;
CONFIG.copyOnClick = div.querySelector('#copy').checked;
Object.keys(CONFIG).forEach(key => {
if (typeof CONFIG[key] !== 'function') GM_setValue(key, CONFIG[key]);
});
location.reload();
};
div.querySelector('#close').onclick = () => div.remove();
}
// ================== INIT ==================
function init() {
addGlobalStyles();
highlightQuotes(document.body);
startObserver();
registerMenuCommands();
console.log('%cQuote Highlighter initialized ✓', 'color:#EA9D9C;font-weight:bold');
}
init();
})();
*/
/* 📌 CHATGPT Version 1:
(function() {
'use strict';
// --- CONFIGURATION ---
const highlightColor = '#EA9D9C'; // Color for highlighting
const quoteRegex = /(?<!\w)(["“”““'‘`"])(.*?)(["””””'’`.])(?!\w)/g;
// You can adjust the regex above to change what counts as "quotes"
// --- FUNCTION TO HIGHLIGHT QUOTED TEXT ---
function highlightQuotes(node) {
if (node.nodeType === Node.TEXT_NODE) {
const parent = node.parentNode;
let text = node.nodeValue;
let match;
let lastIndex = 0;
const frag = document.createDocumentFragment();
while ((match = quoteRegex.exec(text)) !== null) {
// Add text before the match
frag.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
// Create highlighted span
const span = document.createElement('span');
span.textContent = match[0];
span.style.backgroundColor = highlightColor;
frag.appendChild(span);
lastIndex = match.index + match[0].length;
}
// Add remaining text
frag.appendChild(document.createTextNode(text.slice(lastIndex)));
parent.replaceChild(frag, node);
} else if (node.nodeType === Node.ELEMENT_NODE && node.nodeName !== "SCRIPT" && node.nodeName !== "STYLE") {
for (let child of Array.from(node.childNodes)) {
highlightQuotes(child);
}
}
}
// Run the highlighter on the whole page
highlightQuotes(document.body);
})();
*/