// ==UserScript== // @name 关键词高亮(支持分组隔离) // @namespace http://tampermonkey.net/ // @version 2.9 // @description Highlight keywords on a webpage with grouping and isolated styles. Supports inflection matching. // @author You // @match *://*/* // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @downloadURL none // ==/UserScript== const styles = ` .highlighted[data-group="1"] { color: red; font-weight: bold; } .highlighted[data-group="2"] { color: green; font-weight: bold; } .highlighted[data-group="3"] { color: blue; font-weight: bold; } .highlighted[data-group="4"] { color: orange; font-weight: bold; } .highlighted[data-group="5"] { color: purple; font-weight: bold; } `; GM_addStyle(styles); async function getGroupedKeywords() { return (await GM_getValue("groupedKeywords", {})); } async function setGroupedKeywords(groupedKeywords) { await GM_setValue("groupedKeywords", groupedKeywords); } // Function to remove highlights from the page function clearHighlights(container) { container.querySelectorAll(".highlighted").forEach(span => { span.replaceWith(document.createTextNode(span.textContent)); }); } // Function to highlight grouped keywords async function doHighlight(container) { const groupedKeywords = await getGroupedKeywords(); if (!Object.keys(groupedKeywords).length) return; const groupRegexes = Object.entries(groupedKeywords).map(([group, keywords]) => { const patterns = keywords.map(wordText => { return '\\b(' + wordText + '|' + wordText + 's?' + '|' + wordText.replace(/y$/, 'i') + 'es?' + '|' + wordText + 'ed' + '|' + wordText + 'ing' + '|' + wordText + 'd' + '|' + wordText + 'er' + '|' + wordText + 'est' + '|' + wordText + 'ly' + '|' + wordText.replace(/y$/, 'ily') + '|' + wordText.replace(/ic$/, 'ically') + '|' + wordText.replace(/le$/, 'ly') + ')\\b'; }).join("|"); return { regex: new RegExp(patterns, "gi"), group }; }); const walker = document.createTreeWalker( container, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { return groupRegexes.some(({ regex }) => regex.test(node.nodeValue)) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } } ); const nodesToHighlight = []; while (walker.nextNode()) { nodesToHighlight.push(walker.currentNode); } nodesToHighlight.forEach(node => { let highlightedHTML = node.nodeValue; groupRegexes.forEach(({ regex, group }) => { highlightedHTML = highlightedHTML.replace(regex, match => `${match}`); }); const span = document.createElement("span"); span.innerHTML = highlightedHTML; node.parentNode.replaceChild(span, node); }); } // Menu command to import keywords from a remote TXT file for highlighting GM_registerMenuCommand("Import Keywords for Highlighting", async () => { const baseURL = prompt("Enter the base URL of the TXT file (timestamp will be added automatically):"); if (!baseURL) return alert("URL is required."); try { const timestampedURL = `${baseURL}?t=${Date.now()}`; const response = await fetch(timestampedURL); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); const text = await response.text(); const keywords = text.split(/\r?\n/).filter(word => word.trim()); if (!keywords.length) return alert("No keywords found in the file."); const groupedKeywords = await getGroupedKeywords(); const group = prompt("Enter group number (1-5) to add these keywords:"); if (!group || isNaN(group) || group < 1 || group > 5) return alert("Invalid group number."); if (!groupedKeywords[group]) groupedKeywords[group] = []; keywords.forEach(keyword => { if (!groupedKeywords[group].includes(keyword)) { groupedKeywords[group].push(keyword); } }); await setGroupedKeywords(groupedKeywords); alert(`Imported ${keywords.length} keywords into group ${group}.`); await doHighlight(document.body); } catch (error) { alert(`Failed to fetch keywords: ${error.message}`); } }); // Menu command to import keywords for removing highlights GM_registerMenuCommand("Import Keywords for Removing Highlights", async () => { const baseURL = prompt("Enter the base URL of the TXT file for keywords to remove (timestamp will be added automatically):"); if (!baseURL) return alert("URL is required."); try { const timestampedURL = `${baseURL}?t=${Date.now()}`; const response = await fetch(timestampedURL); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); const text = await response.text(); const keywordsToRemove = text.split(/\r?\n/).filter(word => word.trim()); if (!keywordsToRemove.length) return alert("No keywords found in the file."); const groupedKeywords = await getGroupedKeywords(); Object.entries(groupedKeywords).forEach(([group, keywords]) => { groupedKeywords[group] = keywords.filter(keyword => !keywordsToRemove.includes(keyword)); }); await setGroupedKeywords(groupedKeywords); // Remove current highlights and reapply after update clearHighlights(document.body); alert(`Removed ${keywordsToRemove.length} keywords.`); await doHighlight(document.body); } catch (error) { alert(`Failed to fetch keywords: ${error.message}`); } }); // Initial setup (async () => { await doHighlight(document.body); })();