Greasy Fork is available in English.

Webpage text Highlighter

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

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