Webpage text Highlighter

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Advertisement:

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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