Highlight text, erase, export, and format. Tap eraser to remove single, Long-press eraser to remove ALL. No alert messages.
// ==UserScript==
// @name Webpage text Highlighter
// @namespace http://tampermonkey.net/
// @version 24.05.2026.19
// @description Highlight text, erase, export, and format. Tap eraser to remove single, Long-press eraser to remove ALL. No alert messages.
// @author Sspuramgemi
// @match *://*/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
let container = null;
let formatMenu = null;
let currentDocumentCtx = null;
const highlightColors = [
{ name: 'Yellow', color: '#ffeb3b', gradient: 'linear-gradient(yellow, yellow)', text: 'black' },
{ name: 'Green', color: '#00ff00', gradient: 'linear-gradient(#00ff00, #00ff00)', text: 'black' },
{ name: 'Red', color: '#ff3333', gradient: 'linear-gradient(#ff3333, #ff3333)', text: 'white' },
{ name: 'Blue', color: '#00ccff', gradient: 'linear-gradient(#00ccff, #00ccff)', text: 'black' }
];
function handleSelection() {
if (!container) return;
const selection = window.getSelection();
if (selection && selection.toString().trim().length > 0) {
container.style.display = 'flex';
} else {
container.style.display = 'none';
if (formatMenu) formatMenu.style.display = 'none';
}
}
function applyHighlight(colorConfig) {
const selection = window.getSelection();
if (!selection || !selection.rangeCount) return;
const range = selection.getRangeAt(0);
if (range.toString().trim().length === 0) return;
const highlightNode = document.createElement('span');
highlightNode.setAttribute('data-tm-highlight', colorConfig.color);
highlightNode.setAttribute('data-tm-textcolor-val', colorConfig.text);
highlightNode.style.background = colorConfig.gradient;
highlightNode.style.backgroundColor = colorConfig.color;
highlightNode.style.color = colorConfig.text;
try {
range.surroundContents(highlightNode);
selection.removeAllRanges();
if (container) container.style.display = 'none';
} catch (e) {
console.warn("Can't highlight across multiple complex elements");
}
}
function applyTextColor(colorConfig) {
const selection = window.getSelection();
if (!selection || !selection.rangeCount) return;
const range = selection.getRangeAt(0);
if (range.toString().trim().length === 0) return;
const node = document.createElement('span');
node.setAttribute('data-tm-textcolor', colorConfig.color);
node.style.color = colorConfig.color;
try {
range.surroundContents(node);
selection.removeAllRanges();
if (container) container.style.display = 'none';
} catch (e) {
console.warn("Can't apply text color across multiple complex elements");
}
}
function applyCustomFormat(tagName) {
const selection = window.getSelection();
if (!selection || !selection.rangeCount) return;
const range = selection.getRangeAt(0);
if (range.toString().trim().length === 0) return;
const node = document.createElement(tagName);
node.setAttribute('data-tm-format', tagName);
try {
range.surroundContents(node);
selection.removeAllRanges();
if (container) container.style.display = 'none';
} catch (e) {
console.warn("Can't format across multiple complex elements");
}
}
function removeHighlight() {
const selection = window.getSelection();
if (!selection || !selection.rangeCount) return;
let node = selection.getRangeAt(0).startContainer;
let formatNode = null;
while (node && node !== document.body && node !== document.documentElement) {
if (node.nodeType === 1 && (
node.hasAttribute('data-tm-highlight') ||
node.hasAttribute('data-tm-textcolor') ||
node.hasAttribute('data-tm-format') ||
['b','u','i','em','span'].includes(node.tagName.toLowerCase())
)) {
formatNode = node;
break;
}
node = node.parentNode;
}
if (formatNode) {
const parent = formatNode.parentNode;
while (formatNode.firstChild) {
parent.insertBefore(formatNode.firstChild, formatNode);
}
formatNode.remove();
selection.removeAllRanges();
parent.normalize();
if (container) container.style.display = 'none';
}
}
function removeAllHighlights() {
const elements = document.querySelectorAll(
'span[data-tm-highlight], span[data-tm-textcolor], [data-tm-format], b, i, u, em, strong'
);
if (elements.length === 0) return;
elements.forEach(el => {
if (el.hasAttribute('data-tm-highlight') ||
el.hasAttribute('data-tm-textcolor') ||
el.hasAttribute('data-tm-format')) {
const parent = el.parentNode;
while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
el.remove();
}
else if (['b', 'i', 'u', 'em', 'strong'].includes(el.tagName.toLowerCase())) {
if (!el.hasAttribute('class') && !el.hasAttribute('style')) {
const parent = el.parentNode;
while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
el.remove();
}
}
});
document.body.normalize();
if (container) container.style.display = 'none';
}
function getFormattedMarkdown(node) {
let text = "";
node.childNodes.forEach(child => {
if (child.nodeType === Node.TEXT_NODE) {
text += child.textContent;
} else if (child.nodeType === Node.ELEMENT_NODE) {
let inner = getFormattedMarkdown(child);
let tag = child.tagName.toLowerCase();
if (tag === 'b' || child.getAttribute('data-tm-format') === 'b') inner = `**${inner}**`;
if (tag === 'i' || tag === 'em' || child.getAttribute('data-tm-format') === 'i') inner = `*${inner}*`;
if (tag === 'u' || child.getAttribute('data-tm-format') === 'u') inner = `<u>${inner}</u>`;
if (child.hasAttribute('data-tm-textcolor')) {
inner = `<span style="color:${child.getAttribute('data-tm-textcolor')}">${inner}</span>`;
}
if (child.hasAttribute('data-tm-highlight')) {
const bgColor = child.getAttribute('data-tm-highlight');
const textColor = child.getAttribute('data-tm-textcolor-val') || 'black';
inner = `<mark style="background:${bgColor};color:${textColor}">${inner}</mark>`;
}
text += inner;
}
});
return text;
}
function getAllFormattedElements() {
const elements = document.querySelectorAll(
'span[data-tm-highlight], span[data-tm-textcolor], [data-tm-format]'
);
return Array.from(elements).filter(el => {
let parent = el.parentNode;
while (parent && parent !== document.body && parent !== document.documentElement) {
if (
parent.hasAttribute('data-tm-highlight') ||
parent.hasAttribute('data-tm-textcolor') ||
parent.hasAttribute('data-tm-format')
) {
return false;
}
parent = parent.parentNode;
}
return true;
});
}
function exportHighlightsToText() {
const elements = getAllFormattedElements();
if (elements.length === 0) return;
const text = elements.map(el => getFormattedMarkdown(el)).join('\n\n').trim();
const blob = new Blob([text], { type: 'text/plain' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `highlights_export_${Date.now()}.txt`;
link.click();
URL.revokeObjectURL(link.href);
}
function exportHighlightsToClipboard() {
const elements = getAllFormattedElements();
if (elements.length === 0) return;
const text = elements.map(el => getFormattedMarkdown(el)).join('\n\n').trim();
navigator.clipboard.writeText(text).catch(() => {});
}
function parseFormattedElements() {
const elements = getAllFormattedElements();
if (elements.length === 0) return null;
return elements.map(el => {
let items = [];
function parse(node, currentStyles) {
node.childNodes.forEach(child => {
let styles = { ...currentStyles };
if (child.nodeType === Node.TEXT_NODE) {
if (child.textContent.trim().length > 0) {
items.push({ text: child.textContent, styles });
}
} else if (child.nodeType === Node.ELEMENT_NODE) {
let tag = child.tagName.toLowerCase();
if (tag === 'b' || child.getAttribute('data-tm-format') === 'b') styles.bold = true;
if (tag === 'i' || child.getAttribute('data-tm-format') === 'i') styles.italic = true;
if (tag === 'u' || child.getAttribute('data-tm-format') === 'u') styles.underline = true;
if (child.hasAttribute('data-tm-highlight')) {
styles.highlight = child.getAttribute('data-tm-highlight');
styles.textColor = child.getAttribute('data-tm-textcolor-val') || 'black';
}
if (child.hasAttribute('data-tm-textcolor')) {
styles.textColor = child.getAttribute('data-tm-textcolor');
}
parse(child, styles);
}
});
}
let rootStyles = { bold: false, italic: false, underline: false, highlight: null, textColor: '#000000' };
let rootTag = el.tagName.toLowerCase();
if (rootTag === 'b' || el.getAttribute('data-tm-format') === 'b') rootStyles.bold = true;
if (rootTag === 'i' || el.getAttribute('data-tm-format') === 'i') rootStyles.italic = true;
if (rootTag === 'u' || el.getAttribute('data-tm-format') === 'u') rootStyles.underline = true;
if (el.hasAttribute('data-tm-highlight')) {
rootStyles.highlight = el.getAttribute('data-tm-highlight');
rootStyles.textColor = el.getAttribute('data-tm-textcolor-val') || 'black';
}
if (el.hasAttribute('data-tm-textcolor')) {
rootStyles.textColor = el.getAttribute('data-tm-textcolor');
}
parse(el, rootStyles);
return items;
});
}
function exportToImage(customWidth) {
const elements = getAllFormattedElements();
if (elements.length === 0) return;
const paragraphs = elements.map(el => {
let items = [];
function parse(node, currentStyles) {
node.childNodes.forEach(child => {
let styles = { ...currentStyles };
if (child.nodeType === Node.TEXT_NODE) {
if (child.textContent.trim().length > 0) {
items.push({ text: child.textContent, styles });
}
} else if (child.nodeType === Node.ELEMENT_NODE) {
let tag = child.tagName.toLowerCase();
if (tag === 'b' || child.getAttribute('data-tm-format') === 'b') styles.bold = true;
if (tag === 'i' || child.getAttribute('data-tm-format') === 'i') styles.italic = true;
if (tag === 'u' || child.getAttribute('data-tm-format') === 'u') styles.underline = true;
if (child.hasAttribute('data-tm-highlight')) {
styles.highlight = child.getAttribute('data-tm-highlight');
styles.textColor = child.getAttribute('data-tm-textcolor-val') || 'black';
}
if (child.hasAttribute('data-tm-textcolor')) {
styles.textColor = child.getAttribute('data-tm-textcolor');
}
parse(child, styles);
}
});
}
let rootStyles = { bold: false, italic: false, underline: false, highlight: null, textColor: '#000000' };
let rootTag = el.tagName.toLowerCase();
if (rootTag === 'b' || el.getAttribute('data-tm-format') === 'b') rootStyles.bold = true;
if (rootTag === 'i' || el.getAttribute('data-tm-format') === 'i') rootStyles.italic = true;
if (rootTag === 'u' || el.getAttribute('data-tm-format') === 'u') rootStyles.underline = true;
if (el.hasAttribute('data-tm-highlight')) {
rootStyles.highlight = el.getAttribute('data-tm-highlight');
rootStyles.textColor = el.getAttribute('data-tm-textcolor-val') || 'black';
}
if (el.hasAttribute('data-tm-textcolor')) {
rootStyles.textColor = el.getAttribute('data-tm-textcolor');
}
parse(el, rootStyles);
return items;
});
const baseWidth = customWidth;
const padding = 40;
const maxTextWidth = baseWidth - (padding * 2);
const fontSize = 16;
const lineHeight = 23;
const scaleFactor = 3;
const calcCanvas = document.createElement('canvas');
const calcCtx = calcCanvas.getContext('2d');
calcCtx.font = `${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`;
let layoutLines = [];
paragraphs.forEach(para => {
let currentLine = [];
let spaceRemaining = maxTextWidth;
para.forEach(chunk => {
let fontMod = "";
if (chunk.styles.italic) fontMod += "italic ";
if (chunk.styles.bold) fontMod += "bold ";
calcCtx.font = `${fontMod.trim() || 'normal'} ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`;
let words = chunk.text.split(/(\s+)/);
words.forEach(word => {
if (word.length === 0) return;
let wordWidth = calcCtx.measureText(word).width;
if (wordWidth <= spaceRemaining) {
currentLine.push({ text: word, width: wordWidth, styles: chunk.styles });
spaceRemaining -= wordWidth;
} else {
if (currentLine.length > 0) {
layoutLines.push(currentLine);
}
currentLine = [{ text: word, width: wordWidth, styles: chunk.styles }];
spaceRemaining = maxTextWidth - wordWidth;
}
});
});
if (currentLine.length > 0) layoutLines.push(currentLine);
layoutLines.push([]);
});
if (layoutLines.length > 0 && layoutLines[layoutLines.length - 1].length === 0) layoutLines.pop();
const baseHeight = (layoutLines.length * lineHeight) + (padding * 2);
const exportCanvas = document.createElement('canvas');
exportCanvas.width = baseWidth * scaleFactor;
exportCanvas.height = baseHeight * scaleFactor;
const ctx = exportCanvas.getContext('2d');
ctx.scale(scaleFactor, scaleFactor);
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, baseWidth, baseHeight);
ctx.textBaseline = "alphabetic";
let currentY = padding + fontSize;
layoutLines.forEach(line => {
if (line.length === 0) {
currentY += lineHeight * 0.4;
return;
}
let currentX = padding;
line.forEach(item => {
if (item.styles.highlight) {
ctx.fillStyle = item.styles.highlight;
ctx.fillRect(currentX, currentY - (fontSize * 1.05), item.width + 0.5, lineHeight + 5);
}
currentX += item.width;
});
currentX = padding;
line.forEach(item => {
let fontMod = "";
if (item.styles.italic) fontMod += "italic ";
if (item.styles.bold) fontMod += "bold ";
ctx.font = `${fontMod.trim() || 'normal'} ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`;
ctx.fillStyle = item.styles.textColor || '#000000';
ctx.fillText(item.text, currentX, currentY);
if (item.styles.underline) {
ctx.strokeStyle = item.styles.textColor || '#000000';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(currentX, currentY + 3);
ctx.lineTo(currentX + item.width, currentY + 3);
ctx.stroke();
}
currentX += item.width;
});
currentY += lineHeight;
});
try {
const link = document.createElement('a');
const widthLabel = customWidth === 390 ? 'mobile' : 'desktop';
link.download = `highlights_${widthLabel}_${Date.now()}.png`;
link.href = exportCanvas.toDataURL('image/png', 1.0);
link.click();
} catch (e) {
console.error("Canvas export error:", e);
}
}
function exportMobileView() {
exportToImage(390);
}
function exportDesktopView() {
exportToImage(750);
}
function styleButton(btn, color = '#eeeeee', gradient = null, dashed = false) {
btn.style.cssText = `
padding: 6px !important;
border-radius: 50% !important;
background: ${gradient || color} !important;
background-color: ${color} !important;
border: ${dashed ? '2px dashed #cc0000' : '1px solid #000000'} !important;
font-size: 16px !important;
cursor: pointer !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.3) !important;
transition: transform 0.1s ease !important;
width: 36px !important;
height: 36px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 !important;
touch-action: manipulation !important;
`;
btn.addEventListener('mouseenter', () => btn.style.transform = 'scale(1.15)');
btn.addEventListener('mouseleave', () => btn.style.transform = 'scale(1.0)');
}
function checkAndInit() {
const targetElement = document.getElementById('tm-highlight-container');
if (!targetElement || currentDocumentCtx !== document) {
if (targetElement) targetElement.remove();
if (formatMenu && formatMenu.remove) formatMenu.remove();
container = document.createElement('div');
container.id = 'tm-highlight-container';
container.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 2147483647;
display: none;
flex-direction: column;
gap: 8px;
`;
highlightColors.forEach(colorConfig => {
const btn = document.createElement('button');
btn.innerText = '🖍️';
btn.title = `Highlight ${colorConfig.name} (Hold to change Text Color)`;
styleButton(btn, colorConfig.color, colorConfig.gradient);
let pressTimer = null;
let isLongPress = false;
btn.addEventListener('pointerdown', (e) => {
e.preventDefault(); e.stopPropagation();
isLongPress = false;
pressTimer = setTimeout(() => {
isLongPress = true;
applyTextColor(colorConfig);
}, 500);
});
btn.addEventListener('pointerup', (e) => {
e.preventDefault(); e.stopPropagation();
clearTimeout(pressTimer);
if (!isLongPress) {
applyHighlight(colorConfig);
}
});
btn.addEventListener('pointerleave', () => {
clearTimeout(pressTimer);
});
container.appendChild(btn);
});
const eraserBtn = document.createElement('button');
eraserBtn.innerText = '🧹';
eraserBtn.title = 'Tap to remove highlight/format | Long-press to remove ALL';
styleButton(eraserBtn, '#ffffff', '#ffffff', true);
let eraserPressTimer = null;
let eraserIsLongPress = false;
eraserBtn.addEventListener('pointerdown', (e) => {
e.preventDefault(); e.stopPropagation();
eraserIsLongPress = false;
eraserPressTimer = setTimeout(() => {
eraserIsLongPress = true;
removeAllHighlights();
}, 500);
});
eraserBtn.addEventListener('pointerup', (e) => {
e.preventDefault(); e.stopPropagation();
clearTimeout(eraserPressTimer);
if (!eraserIsLongPress) {
removeHighlight();
}
});
eraserBtn.addEventListener('pointerleave', () => {
clearTimeout(eraserPressTimer);
});
container.appendChild(eraserBtn);
const textBtn = document.createElement('button');
textBtn.innerText = '📄';
textBtn.title = 'Export to text';
styleButton(textBtn);
textBtn.addEventListener('click', exportHighlightsToText);
container.appendChild(textBtn);
const imgBtn = document.createElement('button');
imgBtn.innerText = '🖼️';
imgBtn.title = 'Tap for Mobile (390px) | Long-press for Desktop (800px)';
styleButton(imgBtn, '#e0e0e0');
let imgPressTimer = null;
let imgIsLongPress = false;
imgBtn.addEventListener('pointerdown', (e) => {
e.preventDefault(); e.stopPropagation();
imgIsLongPress = false;
imgPressTimer = setTimeout(() => {
imgIsLongPress = true;
exportDesktopView();
}, 500);
});
imgBtn.addEventListener('pointerup', (e) => {
e.preventDefault(); e.stopPropagation();
clearTimeout(imgPressTimer);
if (!imgIsLongPress) {
exportMobileView();
}
});
imgBtn.addEventListener('pointerleave', () => {
clearTimeout(imgPressTimer);
});
container.appendChild(imgBtn);
const clipBtn = document.createElement('button');
clipBtn.innerText = '📋';
clipBtn.title = 'Copy to clipboard';
styleButton(clipBtn);
clipBtn.addEventListener('click', exportHighlightsToClipboard);
container.appendChild(clipBtn);
const formatBtn = document.createElement('button');
formatBtn.innerText = '🔤';
formatBtn.title = 'Bold / Italic / Underline';
styleButton(formatBtn);
container.appendChild(formatBtn);
formatMenu = document.createElement('div');
formatMenu.style.cssText = `
position: fixed;
bottom: 80px;
right: 70px;
z-index: 2147483647;
display: none;
flex-direction: column;
gap: 6px;
background: #ffffff;
border: 1px solid #000000;
padding: 6px;
border-radius: 6px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
`;
document.body.appendChild(formatMenu);
function createFormatOption(label, tagName) {
const btn = document.createElement('button');
btn.innerText = label;
styleButton(btn);
btn.addEventListener('click', () => {
applyCustomFormat(tagName);
formatMenu.style.display = 'none';
});
formatMenu.appendChild(btn);
}
createFormatOption('B', 'b');
createFormatOption('I', 'i');
createFormatOption('U', 'u');
formatBtn.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
formatMenu.style.display = formatMenu.style.display === 'none' ? 'flex' : 'none';
});
document.addEventListener('click', (e) => {
if (formatMenu && formatMenu.style.display === 'flex') {
if (!formatMenu.contains(e.target) && e.target !== formatBtn) {
formatMenu.style.display = 'none';
}
}
});
if (document.body) {
document.body.appendChild(container);
} else if (document.documentElement) {
document.documentElement.appendChild(container);
}
document.removeEventListener('selectionchange', handleSelection);
document.addEventListener('selectionchange', handleSelection);
document.removeEventListener('pointerup', handleSelection);
document.addEventListener('pointerup', handleSelection);
currentDocumentCtx = document;
}
}
checkAndInit();
setInterval(checkAndInit, 600);
})();