Greasy Fork

文本选中高亮相同内容

在网页中选中一些文字时,自动将页面中相同的文字高亮显示

// ==UserScript==
// @name         文本选中高亮相同内容
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  在网页中选中一些文字时,自动将页面中相同的文字高亮显示
// @author       damu
// @match        *://*/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    let lastHighlightClass = 'tm-highlighted-text';

    function removeHighlights() {
        document.querySelectorAll('.' + lastHighlightClass).forEach(el => {
            const parent = el.parentNode;
            parent.replaceChild(document.createTextNode(el.textContent), el);
            parent.normalize(); // 合并相邻文本节点
        });
    }

    function highlightText(text) {
        if (!text || text.trim().length < 2) return;
        const regex = new RegExp(text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');

        // 获取当前选中的范围
        const selection = window.getSelection();
        const selectedNode = selection.anchorNode; // 获取选区起点所在的节点

        const treeWalker = document.createTreeWalker(
            document.body,
            NodeFilter.SHOW_TEXT,
            {
                acceptNode: function (node) {
                    // 过滤掉脚本、样式、隐藏元素、表单控件等
                    const parent = node.parentNode;
                    if (!parent || !parent.offsetParent) return NodeFilter.FILTER_REJECT;
                    const tag = parent.nodeName.toLowerCase();
                    if (['script', 'style', 'textarea', 'input', 'button', 'select'].includes(tag))
                        return NodeFilter.FILTER_REJECT;

                    // 如果是当前选中的文本节点,则跳过
                    if (node === selectedNode) return NodeFilter.FILTER_REJECT;

                    return NodeFilter.FILTER_ACCEPT;
                }
            },
            false
        );

        let node;
        while (node = treeWalker.nextNode()) {
            const matches = [...node.textContent.matchAll(regex)];
            if (matches.length === 0) continue;

            let currentNode = node;
            matches.reverse().forEach(match => {
                const start = match.index;
                const end = start + match[0].length;

                const before = currentNode.textContent.slice(0, start);
                const matched = currentNode.textContent.slice(start, end);
                const after = currentNode.textContent.slice(end);

                const span = document.createElement('span');
                span.textContent = matched;
                span.className = lastHighlightClass;
                span.style.backgroundColor = 'yellow';
                span.style.color = 'black';

                const afterNode = document.createTextNode(after);
                const matchedNode = span;
                const beforeNode = document.createTextNode(before);

                const parent = currentNode.parentNode;
                parent.insertBefore(afterNode, currentNode.nextSibling);
                parent.insertBefore(matchedNode, afterNode);
                parent.insertBefore(beforeNode, matchedNode);
                parent.removeChild(currentNode);

                currentNode = beforeNode;
            });
        }
    }

    document.addEventListener('mouseup', () => {
        setTimeout(() => {
            const selection = window.getSelection();
            const selectedText = selection.toString().trim();
            removeHighlights();
            if (selectedText.length >= 2) {
                highlightText(selectedText);
            }
        }, 10); // 延迟以确保 selection 获取正确
    });
})();