Webpage text Highlighter

Highlight text, erase, export, and format. Tap eraser to remove single, Long-press eraser to remove ALL. No alert messages.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

Advertisement:

// ==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);
})();