Greasy Fork

Greasy Fork is available in English.

关键字高亮工具

支持多关键字高亮、历史记录、配置导入导出的通用工具。使用空格分隔多个关键字,按 Ctrl+Shift+H 快速启动。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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');
})();