Greasy Fork is available in English.
支持多关键字高亮、历史记录、配置导入导出的通用工具。使用空格分隔多个关键字,按 Ctrl+Shift+H 快速启动。
// ==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+
// ==/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 = `
<h3 style="margin-top:0; padding-right:30px;">关键字高亮工具</h3>
<div style="margin-bottom: 15px;">
<label style="display:block; margin-bottom:5px;">输入关键字(空格分隔):</label>
<textarea id="keywords-input" style="width:100%; height:80px; padding:5px; font-size:14px; border:1px solid #ccc; border-radius:3px;"></textarea>
</div>
<div style="margin-bottom: 15px;">
<button id="btn-highlight" style="padding:8px 15px; background:#4CAF50; color:white; border:none; border-radius:3px; cursor:pointer; margin-right:10px;">开始高亮</button>
<button id="btn-clear" style="padding:8px 15px; background:#f44336; color:white; border:none; border-radius:3px; cursor:pointer; margin-right:10px;">清除高亮</button>
<button id="btn-history" style="padding:8px 15px; background:#2196F3; color:white; border:none; border-radius:3px; cursor:pointer;">历史记录</button>
</div>
<div style="margin-bottom: 15px;">
<button id="btn-export" style="padding:6px 12px; background:#FF9800; color:white; border:none; border-radius:3px; cursor:pointer; margin-right:10px; font-size:12px;">导出配置</button>
<button id="btn-import" style="padding:6px 12px; background:#9C27B0; color:white; border:none; border-radius:3px; cursor:pointer; font-size:12px;">导入配置</button>
</div>
<div style="font-size:12px; color:#666;">
<p style="margin:5px 0;">当前状态: <span id="status-text">${highlightEnabled ? '已启用' : '未启用'}</span></p>
<p style="margin:5px 0;">提示: 使用空格分隔多个关键字</p>
</div>
`;
// 保存关闭按钮
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 = '<h3 style="margin-top:0; padding-right:30px;">历史记录</h3>';
if (history.length === 0) {
historyHtml += '<p style="color:#666;">暂无历史记录</p>';
} else {
historyHtml += '<div style="max-height: 400px; overflow-y: auto;">';
history.forEach((entry, index) => {
historyHtml += `
<div style="border:1px solid #ddd; padding:10px; margin-bottom:10px; border-radius:3px; background:#f9f9f9;">
<div style="font-weight:bold; margin-bottom:5px;">${entry.keywords}</div>
<div style="font-size:12px; color:#666; margin-bottom:8px;">${entry.date}</div>
<button class="btn-use-history" data-index="${index}" style="padding:5px 10px; background:#4CAF50; color:white; border:none; border-radius:3px; cursor:pointer; margin-right:5px; font-size:12px;">使用</button>
<button class="btn-export-history" data-index="${index}" style="padding:5px 10px; background:#FF9800; color:white; border:none; border-radius:3px; cursor:pointer; margin-right:5px; font-size:12px;">导出</button>
<button class="btn-delete-history" data-index="${index}" style="padding:5px 10px; background:#f44336; color:white; border:none; border-radius:3px; cursor:pointer; font-size:12px;">删除</button>
</div>
`;
});
historyHtml += '</div>';
}
historyHtml += `
<div style="margin-top:15px; padding-top:15px; border-top:1px solid #ddd;">
<button id="btn-clear-history" style="padding:6px 12px; background:#f44336; color:white; border:none; border-radius:3px; cursor:pointer; margin-right:10px;">清空历史</button>
<button id="btn-back" style="padding:6px 12px; background:#757575; color:white; border:none; border-radius:3px; cursor:pointer;">返回</button>
</div>
`;
// 保存关闭按钮
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 = `
<div class="dialog-content" style="
background: white;
padding: 20px;
border-radius: 8px;
min-width: 500px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
position: relative;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
">
<button class="dialog-close" style="
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
line-height: 1;
padding: 0;
width: 30px;
height: 30px;
">×</button>
</div>
`;
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');
})();