浏览器文本高亮脚本
一、使用效果二、添加文本添加文本 选择颜色然后点击添加按钮即可出现步骤一中的高亮效果。tips①支持批量导入和删除②如果刷新不出来按照刷新网页 重启浏览器 重启电脑的优先级进行三、代码粘贴把下述代码添加到油猴里面。// UserScript // name 文本高亮工具 (自定义) // namespace http://tampermonkey.net/ // version 2.2 // description 高亮页面中的自定义文本支持批量导入、批量删除、拖拽图标、自动保存修复文字可见性 // author You // match *://*/* // grant GM_setValue // grant GM_getValue // grant GM_addStyle // run-at document-end // /UserScript (function() { use strict; // ─── 默认配置空───────────────────────────────────────── const DEFAULT_CONFIG {}; // ─── 配置管理 ────────────────────────────────────────── function loadConfig() { const saved GM_getValue(hlConfig, null); if (saved) { try { return JSON.parse(saved); } catch (e) { /* ignore */ } } return { ...DEFAULT_CONFIG }; } function saveConfig(config) { GM_setValue(hlConfig, JSON.stringify(config)); } function resetConfig() { saveConfig({ ...DEFAULT_CONFIG }); return { ...DEFAULT_CONFIG }; } let config loadConfig(); // ─── 位置存储 ────────────────────────────────────────── function loadPosition() { const pos GM_getValue(hlTogglePos, null); if (pos) { try { return JSON.parse(pos); } catch (e) { /* ignore */ } } return null; } function savePosition(left, top) { GM_setValue(hlTogglePos, JSON.stringify({ left, top })); } // ─── 样式 ────────────────────────────────────────────── GM_addStyle( .hl-text { padding: 0 2px; border-radius: 2px; font-weight: 600; box-shadow: 0 0 0 1px rgba(0,0,0,0.08); /* 新增强制白色文字 阴影保证任何背景下可见 */ color: #fff !important; text-shadow: 0 0 3px rgba(0,0,0,0.6), 0 0 1px rgba(0,0,0,0.3); } #hl-panel { position: fixed; bottom: 80px; right: 20px; width: 340px; max-height: 420px; background: #1e1e2f; color: #eee; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.6); padding: 16px 18px; z-index: 999999; display: none; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; overflow: auto; border: 1px solid #3a3a4a; transition: all 0.15s; } #hl-panel.open { display: block; } #hl-panel h3 { margin: 0 0 10px 0; font-size: 15px; font-weight: 600; color: #fff; display: flex; align-items: center; gap: 8px; } #hl-panel h3 span { opacity: 0.5; font-weight: 400; font-size: 12px; } #hl-list { max-height: 210px; overflow-y: auto; margin-bottom: 10px; border-radius: 6px; background: #2a2a3a; padding: 4px 0; } .hl-item { display: flex; align-items: center; gap: 8px; padding: 5px 10px; border-bottom: 1px solid #33334a; font-size: 13px; } .hl-item:last-child { border-bottom: none; } .hl-item .hl-checkbox { flex-shrink: 0; accent-color: #ff9800; } .hl-item .hl-text-display { flex: 1; font-weight: 500; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .hl-item .hl-color-preview { width: 28px; height: 22px; border-radius: 4px; border: 1px solid #555; flex-shrink: 0; cursor: pointer; } .hl-item .hl-del { background: none; border: none; color: #888; cursor: pointer; font-size: 16px; padding: 0 4px; line-height: 1; flex-shrink: 0; } .hl-item .hl-del:hover { color: #ff6b6b; } #hl-add-row { display: flex; gap: 6px; margin: 6px 0 10px 0; } #hl-add-row input[typetext] { flex: 1; padding: 5px 8px; border-radius: 6px; border: 1px solid #444; background: #2a2a3a; color: #eee; font-size: 13px; outline: none; } #hl-add-row input[typetext]:focus { border-color: #ff9800; } #hl-add-row input[typecolor] { width: 34px; height: 32px; border: none; padding: 0; border-radius: 6px; cursor: pointer; background: transparent; } #hl-add-row button { padding: 4px 14px; border-radius: 6px; border: none; background: #ff9800; color: #1e1e2f; font-weight: 600; cursor: pointer; font-size: 13px; } #hl-add-row button:hover { background: #ffb74d; } #hl-actions { display: flex; gap: 8px; justify-content: flex-end; border-top: 1px solid #33334a; padding-top: 10px; flex-wrap: wrap; } #hl-actions button { padding: 4px 14px; border-radius: 6px; border: 1px solid #444; background: transparent; color: #ccc; cursor: pointer; font-size: 12px; } #hl-actions button:hover { background: #33334a; } #hl-actions .hl-delete-selected { color: #ff6b6b; border-color: #553333; } #hl-actions .hl-delete-selected:hover { background: #442222; } #hl-actions .hl-close { color: #aaa; } #hl-actions .hl-batch { color: #7ec8e3; border-color: #2a4a5a; } #hl-actions .hl-batch:hover { background: #1a3a4a; } #hl-toggle { position: fixed; bottom: 20px; right: 20px; z-index: 999998; width: 44px; height: 44px; border-radius: 50%; background: #1e1e2f; border: 1px solid #3a3a4a; color: #ff9800; font-size: 22px; cursor: grab; box-shadow: 0 4px 16px rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; transition: transform 0.15s; user-select: none; touch-action: none; } #hl-toggle:active { cursor: grabbing; } #hl-toggle:hover { transform: scale(1.08); } #hl-toggle .badge { position: absolute; top: -4px; right: -4px; background: #ff9800; color: #1e1e2f; font-size: 9px; font-weight: 700; border-radius: 50%; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; } .hl-empty { color: #666; text-align: center; padding: 16px 0; font-style: italic; } #hl-list::-webkit-scrollbar { width: 4px; } #hl-list::-webkit-scrollbar-track { background: #222; border-radius: 4px; } #hl-list::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; } /* 批量导入模态框 */ #hl-batch-modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 1000000; align-items: center; justify-content: center; } #hl-batch-modal.open { display: flex; } #hl-batch-modal .modal-box { background: #1e1e2f; color: #eee; padding: 20px 24px; border-radius: 12px; width: 440px; max-width: 90vw; max-height: 80vh; display: flex; flex-direction: column; border: 1px solid #3a3a4a; box-shadow: 0 8px 32px rgba(0,0,0,0.8); } #hl-batch-modal .modal-box h4 { margin: 0 0 8px 0; font-size: 15px; } #hl-batch-modal .modal-box p { margin: 4px 0 10px 0; opacity: 0.6; font-size: 12px; } #hl-batch-modal textarea { flex: 1; min-height: 200px; background: #2a2a3a; border: 1px solid #444; border-radius: 6px; color: #eee; padding: 8px; font-size: 13px; font-family: monospace; resize: vertical; outline: none; } #hl-batch-modal textarea:focus { border-color: #ff9800; } #hl-batch-modal .modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 12px; } #hl-batch-modal .modal-actions button { padding: 6px 18px; border-radius: 6px; border: none; cursor: pointer; font-size: 13px; } #hl-batch-modal .modal-actions .btn-import { background: #ff9800; color: #1e1e2f; font-weight: 600; } #hl-batch-modal .modal-actions .btn-import:hover { background: #ffb74d; } #hl-batch-modal .modal-actions .btn-cancel { background: transparent; color: #aaa; border: 1px solid #444; } #hl-batch-modal .modal-actions .btn-cancel:hover { background: #33334a; } ); // ─── 高亮引擎 ────────────────────────────────────────── let highlightObserver null; let highlightTimeout null; function getColorFor(text) { return config[text] || #FF9800; } function buildRegex() { const texts Object.keys(config); if (texts.length 0) return null; const sorted texts.slice().sort((a, b) b.length - a.length); const escaped sorted.map(t t.replace(/[.*?^${}()|[\]\\]/g, \\$)); return new RegExp((${escaped.join(|)}), g); } let regex buildRegex(); function clearHighlights(root) { const elements root.querySelectorAll(span.hl-text); elements.forEach(el { const parent el.parentNode; if (parent) { const text document.createTextNode(el.textContent); parent.replaceChild(text, el); parent.normalize(); } }); } function applyHighlights(root document.body) { if (!regex) return; const walker document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode: (node) { const parent node.parentNode; if (!parent) return NodeFilter.FILTER_REJECT; const tag parent.tagName; if ([SCRIPT, STYLE, NOSCRIPT, TEXTAREA, INPUT, SELECT].includes(tag)) { return NodeFilter.FILTER_REJECT; } if (parent.closest parent.closest(.hl-text)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); const nodesToProcess []; let node; while ((node walker.nextNode())) { nodesToProcess.push(node); } for (const textNode of nodesToProcess) { const text textNode.textContent; if (!text || !regex.test(text)) continue; regex.lastIndex 0; const fragment document.createDocumentFragment(); let lastIndex 0; let match; const localRegex buildRegex(); if (!localRegex) continue; localRegex.lastIndex 0; while ((match localRegex.exec(text)) ! null) { const matchText match[0]; const matchIndex match.index; const color getColorFor(matchText); if (matchIndex lastIndex) { fragment.appendChild(document.createTextNode(text.substring(lastIndex, matchIndex))); } const span document.createElement(span); span.className hl-text; span.textContent matchText; span.style.backgroundColor color; fragment.appendChild(span); lastIndex matchIndex matchText.length; } if (lastIndex text.length) { fragment.appendChild(document.createTextNode(text.substring(lastIndex))); } if (fragment.childNodes.length 0) { const parent textNode.parentNode; if (parent) { parent.replaceChild(fragment, textNode); } } } } function refreshHighlights() { regex buildRegex(); clearHighlights(document.body); if (regex) { applyHighlights(document.body); } } // ─── MutationObserver ────────────────────────────────── function startObserver() { if (highlightObserver) highlightObserver.disconnect(); highlightObserver new MutationObserver(() { if (highlightTimeout) clearTimeout(highlightTimeout); highlightTimeout setTimeout(() { refreshHighlights(); highlightTimeout null; }, 200); }); highlightObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); } // ─── UI 面板 ────────────────────────────────────────── function renderList() { const container document.getElementById(hl-list); if (!container) return; const keys Object.keys(config); if (keys.length 0) { container.innerHTML div classhl-empty暂无高亮规则/div; return; } let html ; for (const text of keys) { const color config[text]; html div classhl-item data-text${text.replace(//g, quot;)} input typecheckbox classhl-checkbox data-text${text.replace(//g, quot;)} span classhl-text-display${text}/span input typecolor classhl-color-preview value${color} data-text${text.replace(//g, quot;)} button classhl-del data-text${text.replace(//g, quot;)}✕/button /div ; } container.innerHTML html; container.querySelectorAll(.hl-color-preview).forEach(input { input.addEventListener(input, function(e) { const text this.dataset.text; const newColor this.value; config[text] newColor; saveConfig(config); refreshHighlights(); const preview this.closest(.hl-item).querySelector(.hl-color-preview); if (preview) preview.style.background newColor; }); input.style.background input.value; }); container.querySelectorAll(.hl-del).forEach(btn { btn.addEventListener(click, function() { const text this.dataset.text; delete config[text]; saveConfig(config); refreshHighlights(); renderList(); updateBadge(); updateTitleCount(); }); }); } function updateBadge() { const badge document.querySelector(#hl-toggle .badge); if (badge) { const count Object.keys(config).length; badge.textContent count 99 ? 99 : count; badge.style.display count 0 ? flex : none; } } function updateTitleCount() { const span document.querySelector(#hl-panel h3 span); if (span) span.textContent ${Object.keys(config).length} 个规则; } // ─── 批量删除 ────────────────────────────────────────── function deleteSelected() { const checkboxes document.querySelectorAll(.hl-checkbox:checked); if (checkboxes.length 0) { alert(请至少勾选一条规则。); return; } const texts Array.from(checkboxes).map(cb cb.dataset.text); if (!confirm(确定要删除选中的 ${texts.length} 条规则吗)) return; for (const text of texts) { delete config[text]; } saveConfig(config); refreshHighlights(); renderList(); updateBadge(); updateTitleCount(); } // ─── 批量导入 ────────────────────────────────────────── function showBatchModal() { const modal document.getElementById(hl-batch-modal); if (!modal) return; modal.classList.add(open); const textarea modal.querySelector(textarea); if (textarea) { textarea.value ; textarea.focus(); } } function closeBatchModal() { const modal document.getElementById(hl-batch-modal); if (modal) modal.classList.remove(open); } function importBatch() { const textarea document.getElementById(hl-batch-textarea); if (!textarea) return; const raw textarea.value; const lines raw.split(/\r?\n/).filter(line line.trim() ! ); if (lines.length 0) { alert(请至少输入一条规则。); return; } let added 0; let skipped 0; const newConfig { ...config }; for (const line of lines) { let text, color; if (line.includes(,)) { const parts line.split(,).map(s s.trim()); text parts[0]; color parts[1] || #FF9800; } else if (line.includes(#)) { const idx line.lastIndexOf(#); text line.substring(0, idx).trim(); let c line.substring(idx).trim(); if (/^#[0-9a-fA-F]{6}$/.test(c)) { color c; } else { color #FF9800; text line.trim(); } } else { text line.trim(); color #FF9800; } if (!text) continue; if (newConfig[text] ! undefined) { skipped; } newConfig[text] color; added; } if (added 0) { alert(没有有效的规则被添加。); return; } config newConfig; saveConfig(config); regex buildRegex(); refreshHighlights(); renderList(); updateBadge(); updateTitleCount(); closeBatchModal(); alert(成功导入 ${added} 条规则${skipped 0 ? 其中 ${skipped} 条已存在并被覆盖 : }); } // ─── 拖拽功能 ────────────────────────────────────────── function setupDraggable() { const toggle document.getElementById(hl-toggle); if (!toggle) return; const pos loadPosition(); if (pos) { toggle.style.left pos.left px; toggle.style.top pos.top px; toggle.style.bottom auto; toggle.style.right auto; } let isDragging false; let startX, startY, startLeft, startTop; const onStart (e) { e.preventDefault(); if (e.type mousedown e.button ! 0) return; isDragging false; const rect toggle.getBoundingClientRect(); startX e.clientX || e.touches[0].clientX; startY e.clientY || e.touches[0].clientY; let left parseFloat(toggle.style.left); let top parseFloat(toggle.style.top); if (isNaN(left) || isNaN(top)) { left rect.left; top rect.top; toggle.style.left left px; toggle.style.top top px; toggle.style.bottom auto; toggle.style.right auto; } startLeft left; startTop top; toggle.style.cursor grabbing; document.addEventListener(mousemove, onMove); document.addEventListener(mouseup, onEnd); document.addEventListener(touchmove, onMove, { passive: false }); document.addEventListener(touchend, onEnd, { passive: false }); }; const onMove (e) { e.preventDefault(); const clientX e.clientX || e.touches[0].clientX; const clientY e.clientY || e.touches[0].clientY; const dx clientX - startX; const dy clientY - startY; let newLeft startLeft dx; let newTop startTop dy; const rect toggle.getBoundingClientRect(); const w rect.width || 44; const h rect.height || 44; const maxX window.innerWidth - w; const maxY window.innerHeight - h; newLeft Math.max(0, Math.min(newLeft, maxX)); newTop Math.max(0, Math.min(newTop, maxY)); toggle.style.left newLeft px; toggle.style.top newTop px; isDragging true; }; const onEnd (e) { document.removeEventListener(mousemove, onMove); document.removeEventListener(mouseup, onEnd); document.removeEventListener(touchmove, onMove); document.removeEventListener(touchend, onEnd); toggle.style.cursor grab; const left parseFloat(toggle.style.left); const top parseFloat(toggle.style.top); if (!isNaN(left) !isNaN(top)) { savePosition(left, top); } isDragging false; }; toggle.addEventListener(mousedown, onStart); toggle.addEventListener(touchstart, onStart, { passive: false }); let clickTimer null; toggle.addEventListener(click, function(e) { if (isDragging) return; if (clickTimer) clearTimeout(clickTimer); clickTimer setTimeout(() { const panel document.getElementById(hl-panel); if (panel) { panel.classList.toggle(open); if (panel.classList.contains(open)) { renderList(); } } clickTimer null; }, 10); }); } // ─── 构建UI ────────────────────────────────────────── function buildUI() { const toggle document.createElement(div); toggle.id hl-toggle; toggle.innerHTML ⚙ span classbadge${Object.keys(config).length}/span ; document.body.appendChild(toggle); const panel document.createElement(div); panel.id hl-panel; panel.innerHTML h3 文本高亮 span${Object.keys(config).length} 个规则/span/h3 div idhl-list/div div idhl-add-row input typetext idhl-add-text placeholder输入文本... input typecolor idhl-add-color value#FF9800 button idhl-add-btn添加/button /div div idhl-actions button classhl-batch idhl-batch-btn 批量导入/button button classhl-delete-selected idhl-delete-selected-btn 删除选中/button button classhl-close idhl-close-btn关闭/button /div ; document.body.appendChild(panel); const modal document.createElement(div); modal.id hl-batch-modal; modal.innerHTML div classmodal-box h4批量导入规则/h4 p每行一条格式strong文本,颜色/strong 或 strong文本 #颜色/strong颜色可省略默认 #FF9800/p textarea idhl-batch-textarea placeholder示例#10;苹果, #FF0000#10;香蕉 #FFD700#10;橘子/textarea div classmodal-actions button classbtn-cancel idhl-batch-cancel取消/button button classbtn-import idhl-batch-import导入/button /div /div ; document.body.appendChild(modal); document.getElementById(hl-close-btn).addEventListener(click, () { panel.classList.remove(open); }); document.getElementById(hl-add-btn).addEventListener(click, () { const input document.getElementById(hl-add-text); const colorInput document.getElementById(hl-add-color); const text input.value.trim(); const color colorInput.value; if (!text) return; if (config[text]) { alert(${text} 已存在); return; } config[text] color; saveConfig(config); regex buildRegex(); refreshHighlights(); renderList(); updateBadge(); updateTitleCount(); input.value ; }); document.getElementById(hl-add-text).addEventListener(keydown, (e) { if (e.key Enter) document.getElementById(hl-add-btn).click(); }); document.getElementById(hl-batch-btn).addEventListener(click, showBatchModal); document.getElementById(hl-batch-cancel).addEventListener(click, closeBatchModal); document.getElementById(hl-batch-import).addEventListener(click, importBatch); modal.addEventListener(click, (e) { if (e.target modal) closeBatchModal(); }); document.getElementById(hl-delete-selected-btn).addEventListener(click, deleteSelected); document.addEventListener(click, (e) { if (panel.classList.contains(open)) { const target e.target; if (!panel.contains(target) target ! toggle !modal.contains(target)) { panel.classList.remove(open); } } }); renderList(); updateBadge(); updateTitleCount(); setupDraggable(); } // ─── 初始化 ──────────────────────────────────────────── function init() { buildUI(); refreshHighlights(); startObserver(); setTimeout(refreshHighlights, 800); window.addEventListener(load, () { setTimeout(refreshHighlights, 300); }); } if (document.readyState complete || document.readyState interactive) { init(); } else { document.addEventListener(DOMContentLoaded, init); } })();