// ==UserScript==
// @name 英语单词高亮关键词并支持多种变形
// @namespace http://tampermonkey.net/
// @version 2.0
// @description 高效高亮关键词,支持单词变形匹配和删除标记规则,优化性能防止卡死
// @author You
// @match *://*/*
// @grant GM_xmlhttpRequest
// @connect *
// ==/UserScript==
(function () {
'use strict';
if (!document.documentElement.lang || !document.documentElement.lang.startsWith('en')) {
console.log("Not an English page. Script halted.");
return;
}
const urls = {
group1: "https://example.com/group1.txt", // 替换为实际链接
group2: "https://example.com/group2.txt", // 替换为实际链接
group3: "https://example.com/group3.txt", // 替换为实际链接
delete: "https://example.com/delete.txt", // 替换为实际链接
};
const colors = {
group1: "green",
group2: "blue",
group3: "red",
};
const addTimestamp = url => `${url}?t=${Date.now()}`;
async function loadKeywords(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: addTimestamp(url),
onload: res => res.status === 200
? resolve(res.responseText.split("\n").map(word => word.trim()).filter(Boolean))
: reject(`Failed to load ${url}`),
onerror: err => reject(err),
});
});
}
function buildRegex(word) {
const forms = [
word,
`${word}s?`,
word.replace(/y$/, "i") + "es?",
`${word}ed`,
word.replace(/e$/, "") + "ing",
`${word}ing`,
`${word}d`,
`${word}er`,
`${word}est`,
`${word}ly`,
word.replace(/y$/, "ily"),
word.replace(/ic$/, "ically"),
word.replace(/le$/, "ly"),
];
return new RegExp(`\\b(${forms.join("|")})\\b`, "gi");
}
function traverseAndRestore(node, regexList) {
if (node.nodeType === 1) {
const spans = node.querySelectorAll("span[data-highlighted]");
spans.forEach(span => {
const text = span.textContent;
if (regexList.some(regex => regex.test(text))) {
span.replaceWith(document.createTextNode(text));
}
});
}
}
function highlightTextNode(node, regexList, color) {
const matches = regexList.find(regex => regex.test(node.nodeValue));
if (matches) {
const span = document.createElement("span");
span.setAttribute("data-highlighted", "true");
span.innerHTML = node.nodeValue.replace(matches, match => `<span style="color: ${color}; font-weight: bold;">${match}</span>`);
node.replaceWith(span);
}
}
function traverseAndHighlight(node, regexList, color) {
if (node.nodeType === 3 && node.nodeValue.trim()) {
highlightTextNode(node, regexList, color);
} else if (node.nodeType === 1 && node.childNodes && !/^(script|style|iframe|noscript|textarea)$/i.test(node.tagName)) {
node.childNodes.forEach(child => traverseAndHighlight(child, regexList, color));
}
}
async function main() {
try {
const [deleteKeywords, group1Keywords, group2Keywords, group3Keywords] = await Promise.all([
loadKeywords(urls.delete),
loadKeywords(urls.group1),
loadKeywords(urls.group2),
loadKeywords(urls.group3),
]);
const deleteRegexList = deleteKeywords.map(buildRegex);
const groupRegexes = {
group1: group1Keywords.filter(k => !deleteRegexList.some(regex => regex.test(k))).map(buildRegex),
group2: group2Keywords.filter(k => !deleteRegexList.some(regex => regex.test(k))).map(buildRegex),
group3: group3Keywords.filter(k => !deleteRegexList.some(regex => regex.test(k))).map(buildRegex),
};
traverseAndRestore(document.body, deleteRegexList);
const highlightGroups = [
{ regexes: groupRegexes.group1, color: colors.group1 },
{ regexes: groupRegexes.group2, color: colors.group2 },
{ regexes: groupRegexes.group3, color: colors.group3 },
];
const processNodes = () => {
highlightGroups.forEach(({ regexes, color }) => {
traverseAndHighlight(document.body, regexes, color);
});
};
if (window.requestIdleCallback) {
requestIdleCallback(processNodes);
} else {
setTimeout(processNodes, 100);
}
} catch (err) {
console.error("Error during execution:", err);
}
}
main();
})();