Skip to content

Instantly share code, notes, and snippets.

@lxl66566
Last active July 23, 2025 06:54
Show Gist options
  • Select an option

  • Save lxl66566/c314e5eeb03f4f2504bd1c09561a12cc to your computer and use it in GitHub Desktop.

Select an option

Save lxl66566/c314e5eeb03f4f2504bd1c09561a12cc to your computer and use it in GitHub Desktop.
划词高亮页面内相同文字(效果类似划词自动 ctrl + f 在页面上搜索)
// ==UserScript==
// @name 划词高亮页面内相同文字
// @namespace http://tampermonkey.net/
// @version 2.1
// @description 当在网页上选中一段文字后,自动高亮所有其他相同文字。点击或选择新内容后自动清除旧高亮。当选中的文本是在输入框或可编辑区域内的时候,不会触发高亮。
// @author lxl66566 (Gemini 2.5 pro)
// @match *://*/*
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- 配置区域 ---
const HIGHLIGHT_BACKGROUND_COLOR = 'yellow';
const HIGHLIGHT_TEXT_COLOR = 'black';
const HIGHLIGHT_CLASS = 'gemini-text-highlighter-e4f5g6'; // 使用一个独特的类名以避免冲突
// 注入高亮样式
GM_addStyle(`
.${HIGHLIGHT_CLASS} {
background-color: ${HIGHLIGHT_BACKGROUND_COLOR} !important;
color: ${HIGHLIGHT_TEXT_COLOR} !important;
padding: 1px 0;
border-radius: 3px;
box-shadow: 0 0 3px rgba(255, 255, 0, 0.5);
font-style: normal;
}
`);
let lastSelectionText = '';
// 移除所有由本脚本添加的高亮
function removeHighlights() {
// 使用 Array.from 将 NodeList 转换为数组,以避免在遍历时修改集合导致的问题
const highlights = Array.from(document.querySelectorAll(`.${HIGHLIGHT_CLASS}`));
highlights.forEach(highlightNode => {
const parent = highlightNode.parentNode;
if (parent) {
// 将高亮节点的内容(文本)替换掉高亮节点本身
parent.replaceChild(document.createTextNode(highlightNode.textContent), highlightNode);
// 合并相邻的文本节点,保持DOM整洁
parent.normalize();
}
});
}
// 高亮指定文本,并跳过原始选择区域
function highlightText(text, selection) {
if (!text || !selection) {
return;
}
// 获取选区的起始和结束节点,用于后续判断和跳过
const anchorNode = selection.anchorNode;
const focusNode = selection.focusNode;
const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
const nodesToProcess = [];
// 步骤1:先收集所有相关的文本节点
let node;
while (node = treeWalker.nextNode()) {
const parentName = node.parentNode.nodeName.toLowerCase();
// 排除脚本、样式、已高亮的和不可见元素内的文本
if (parentName === 'script' || parentName === 'style' || parentName === 'noscript' || node.parentNode.classList.contains(HIGHLIGHT_CLASS) || node.textContent.trim() === '') {
continue;
}
nodesToProcess.push(node);
}
// 步骤2:遍历收集到的节点并进行高亮处理
nodesToProcess.forEach(textNode => {
// **核心优化点**:如果当前文本节点就是用户选择的节点,则跳过,以保留原始选择
if (textNode === anchorNode || textNode === focusNode) {
return;
}
const textContent = textNode.nodeValue;
// 使用不区分大小写的全局正则表达式进行匹配
const regex = new RegExp(escapeRegExp(text), 'gi');
let match;
let lastIndex = 0;
const fragment = document.createDocumentFragment();
let hasMatch = false;
while ((match = regex.exec(textContent)) !== null) {
hasMatch = true;
// 添加匹配前的文本
if (match.index > lastIndex) {
fragment.appendChild(document.createTextNode(textContent.substring(lastIndex, match.index)));
}
// 创建并添加高亮元素
const mark = document.createElement('span');
mark.className = HIGHLIGHT_CLASS;
mark.textContent = match[0];
fragment.appendChild(mark);
lastIndex = regex.lastIndex;
}
// 如果确实发生了匹配,才用新的文档片段替换旧的文本节点
if (hasMatch) {
// 添加最后一次匹配后的剩余文本
if (lastIndex < textContent.length) {
fragment.appendChild(document.createTextNode(textContent.substring(lastIndex)));
}
textNode.parentNode.replaceChild(fragment, textNode);
}
});
}
// 用于转义正则表达式中的特殊字符,防止用户选择的文本中包含特殊字符而导致正则错误
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& 表示整个被匹配的字符串
}
// 监听鼠标抬起事件(文本选择结束时触发)
document.addEventListener('mouseup', () => {
// 使用微小的延迟来确保浏览器完成选择操作
setTimeout(() => {
// --- 新增需求:检查当前激活的元素 ---
const activeElement = document.activeElement;
if (activeElement) {
const nodeName = activeElement.nodeName.toUpperCase();
// 如果焦点在输入框、文本域或任何可编辑元素内,则不执行任何操作
if (nodeName === 'INPUT' || nodeName === 'TEXTAREA' || activeElement.isContentEditable) {
return; // 直接退出,不做任何处理
}
}
// --- 新增需求结束 ---
const selectionObj = window.getSelection();
const selectedText = selectionObj ? selectionObj.toString().trim() : '';
// 仅在选择内容发生变化时才执行操作
if (selectedText !== lastSelectionText) {
removeHighlights(); // 先移除旧的高亮
if (selectedText.length > 0) {
// 传入选择的文本和整个 selection 对象
highlightText(selectedText, selectionObj);
}
lastSelectionText = selectedText;
}
}, 10);
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment