// ==UserScript== // @name 关键字高亮工具 // @name:en Keyword Highlighter // @namespace https://github.com/boomoodxf/keyword-highlighter // @version 1.0.0 // @description 支持多关键字高亮、历史记录、配置导入导出的通用工具。使用空格分隔多个关键字,按 Ctrl+Shift+H 快速启动。 // @description:en A powerful keyword highlighter with support for multiple keywords, history management, and config import/export. Press Ctrl+Shift+H to launch. // @author 小渣渣 // @license MIT // @homepage https://github.com/boomoodxf/keyword-highlighter // @supportURL https://github.com/boomoodxf/keyword-highlighter/issues // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @run-at document-end // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiBmaWxsPSIjRkZGRjAwIi8+PHRleHQgeD0iMzIiIHk9IjQyIiBmb250LXNpemU9IjM2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iQXJpYWwiPkE8L3RleHQ+PC9zdmc+ // @downloadURL https://update.greasyfork.icu/scripts/574807/%E5%85%B3%E9%94%AE%E5%AD%97%E9%AB%98%E4%BA%AE%E5%B7%A5%E5%85%B7.user.js // @updateURL https://update.greasyfork.icu/scripts/574807/%E5%85%B3%E9%94%AE%E5%AD%97%E9%AB%98%E4%BA%AE%E5%B7%A5%E5%85%B7.meta.js // ==/UserScript== (function() { 'use strict'; // 配置管理 const CONFIG_KEY = 'keyword_highlighter_config'; const HISTORY_KEY = 'keyword_highlighter_history'; const MAX_HISTORY = 50; // 默认高亮颜色 const COLORS = [ '#FFFF00', '#00FFFF', '#FF00FF', '#00FF00', '#FFB6C1', '#FFA500', '#98FB98', '#DDA0DD' ]; // 存储当前高亮状态 let currentHighlights = []; let highlightEnabled = false; // 获取历史记录 function getHistory() { try { return JSON.parse(GM_getValue(HISTORY_KEY, '[]')); } catch (e) { return []; } } // 保存历史记录 function saveHistory(keywords) { if (!keywords || keywords.trim() === '') return; let history = getHistory(); const entry = { keywords: keywords, timestamp: new Date().toISOString(), date: new Date().toLocaleString('zh-CN') }; // 去重 history = history.filter(h => h.keywords !== keywords); history.unshift(entry); // 限制历史记录数量 if (history.length > MAX_HISTORY) { history = history.slice(0, MAX_HISTORY); } GM_setValue(HISTORY_KEY, JSON.stringify(history)); } // 清空历史记录 function clearHistory() { GM_setValue(HISTORY_KEY, '[]'); } // 导出配置 function exportConfig() { const config = { history: getHistory(), version: '1.0.0', exportDate: new Date().toISOString() }; const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `keyword-highlighter-config-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); } // 导出单个历史记录 function exportSingleHistory(entry) { const config = { keywords: entry.keywords, timestamp: entry.timestamp, date: entry.date, version: '1.0.0' }; const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `keyword-${entry.keywords.substring(0, 20).replace(/\s+/g, '-')}-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); } // 导入配置 function importConfig() { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json'; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const config = JSON.parse(event.target.result); if (config.history) { // 导入完整配置 GM_setValue(HISTORY_KEY, JSON.stringify(config.history)); alert('配置导入成功!'); } else if (config.keywords) { // 导入单个历史记录 saveHistory(config.keywords); alert('历史记录导入成功!'); } else { alert('配置文件格式错误!'); } } catch (err) { alert('配置文件解析失败:' + err.message); } }; reader.readAsText(file); }; input.click(); } // 高亮文本节点 function highlightTextNode(node, keywords) { if (node.nodeType !== Node.TEXT_NODE) return; if (!node.nodeValue || node.nodeValue.trim() === '') return; const parent = node.parentNode; if (!parent) return; // 跳过已经高亮的节点和特殊标签 if (parent.classList && parent.classList.contains('keyword-highlight')) return; const skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT']; if (skipTags.includes(parent.tagName)) return; const text = node.nodeValue; const fragment = document.createDocumentFragment(); let lastIndex = 0; let matched = false; // 构建正则表达式 const escapedKeywords = keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') ); const regex = new RegExp(`(${escapedKeywords.join('|')})`, 'gi'); let match; regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { matched = true; // 添加匹配前的文本 if (match.index > lastIndex) { fragment.appendChild( document.createTextNode(text.substring(lastIndex, match.index)) ); } // 创建高亮元素 const span = document.createElement('span'); span.className = 'keyword-highlight'; span.textContent = match[0]; // 根据关键字索引分配颜色 const keywordIndex = keywords.findIndex(k => k.toLowerCase() === match[0].toLowerCase() ); span.style.backgroundColor = COLORS[keywordIndex % COLORS.length]; span.style.color = '#000'; span.style.padding = '0 2px'; span.style.borderRadius = '2px'; fragment.appendChild(span); lastIndex = regex.lastIndex; } if (matched) { // 添加剩余文本 if (lastIndex < text.length) { fragment.appendChild( document.createTextNode(text.substring(lastIndex)) ); } parent.replaceChild(fragment, node); } } // 遍历所有文本节点 function walkTextNodes(node, keywords) { if (node.nodeType === Node.TEXT_NODE) { highlightTextNode(node, keywords); } else { const children = Array.from(node.childNodes); children.forEach(child => walkTextNodes(child, keywords)); } } // 执行高亮 function performHighlight(keywordsStr) { if (!keywordsStr || keywordsStr.trim() === '') { alert('请输入要高亮的关键字!'); return; } // 先清除之前的高亮 clearHighlights(); // 解析关键字(空格分隔) const keywords = keywordsStr.split(/\s+/).filter(k => k.length > 0); if (keywords.length === 0) { alert('请输入有效的关键字!'); return; } currentHighlights = keywords; highlightEnabled = true; // 保存到历史记录 saveHistory(keywordsStr); // 执行高亮 walkTextNodes(document.body, keywords); alert(`已高亮 ${keywords.length} 个关键字`); } // 清除高亮 function clearHighlights() { const highlights = document.querySelectorAll('.keyword-highlight'); highlights.forEach(span => { const parent = span.parentNode; if (parent) { parent.replaceChild(document.createTextNode(span.textContent), span); parent.normalize(); // 合并相邻文本节点 } }); currentHighlights = []; highlightEnabled = false; } // 创建主界面 function showMainDialog() { const dialog = createDialog(); const content = dialog.querySelector('.dialog-content'); const mainContent = `

关键字高亮工具

当前状态: ${highlightEnabled ? '已启用' : '未启用'}

提示: 使用空格分隔多个关键字

`; // 保存关闭按钮 const closeBtn = content.querySelector('.dialog-close'); content.innerHTML = mainContent; content.insertBefore(closeBtn, content.firstChild); document.body.appendChild(dialog); // 绑定事件 document.getElementById('btn-highlight').onclick = () => { const keywords = document.getElementById('keywords-input').value; performHighlight(keywords); document.getElementById('status-text').textContent = '已启用'; }; document.getElementById('btn-clear').onclick = () => { clearHighlights(); document.getElementById('status-text').textContent = '未启用'; alert('已清除所有高亮'); }; document.getElementById('btn-history').onclick = () => { dialog.remove(); showHistoryDialog(); }; document.getElementById('btn-export').onclick = () => { exportConfig(); }; document.getElementById('btn-import').onclick = () => { importConfig(); }; closeBtn.onclick = () => { dialog.remove(); }; // 点击背景关闭 dialog.onclick = (e) => { if (e.target === dialog) { dialog.remove(); } }; } // 显示历史记录 function showHistoryDialog() { const dialog = createDialog(); const content = dialog.querySelector('.dialog-content'); const history = getHistory(); let historyHtml = '

历史记录

'; if (history.length === 0) { historyHtml += '

暂无历史记录

'; } else { historyHtml += '
'; history.forEach((entry, index) => { historyHtml += `
${entry.keywords}
${entry.date}
`; }); historyHtml += '
'; } historyHtml += `
`; // 保存关闭按钮 const closeBtn = content.querySelector('.dialog-close'); content.innerHTML = historyHtml; content.insertBefore(closeBtn, content.firstChild); document.body.appendChild(dialog); // 绑定事件 content.querySelectorAll('.btn-use-history').forEach(btn => { btn.onclick = () => { const index = parseInt(btn.dataset.index); const entry = history[index]; performHighlight(entry.keywords); dialog.remove(); }; }); content.querySelectorAll('.btn-export-history').forEach(btn => { btn.onclick = () => { const index = parseInt(btn.dataset.index); const entry = history[index]; exportSingleHistory(entry); }; }); content.querySelectorAll('.btn-delete-history').forEach(btn => { btn.onclick = () => { const index = parseInt(btn.dataset.index); history.splice(index, 1); GM_setValue(HISTORY_KEY, JSON.stringify(history)); dialog.remove(); showHistoryDialog(); }; }); const clearBtn = document.getElementById('btn-clear-history'); if (clearBtn) { clearBtn.onclick = () => { if (confirm('确定要清空所有历史记录吗?')) { clearHistory(); dialog.remove(); showHistoryDialog(); } }; } document.getElementById('btn-back').onclick = () => { dialog.remove(); showMainDialog(); }; closeBtn.onclick = () => { dialog.remove(); }; dialog.onclick = (e) => { if (e.target === dialog) { dialog.remove(); } }; } // 创建对话框 function createDialog() { const dialog = document.createElement('div'); dialog.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 999999; font-family: Arial, sans-serif; `; dialog.innerHTML = `
`; return dialog; } // 注册菜单命令 GM_registerMenuCommand('打开关键字高亮工具', showMainDialog); GM_registerMenuCommand('清除当前高亮', () => { clearHighlights(); alert('已清除所有高亮'); }); // 监听快捷键 Ctrl+Shift+H document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey && e.key === 'H') { e.preventDefault(); showMainDialog(); } }); console.log('关键字高亮工具已加载 - 快捷键: Ctrl+Shift+H'); })();