// ==UserScript== // @name 多语种生词高亮助手 // @namespace 公众号: 337自修室 // @version 26.06.23.1 // @description 自定义多语种网页生词高亮工具,支持欧路生词本自动选择与覆盖/合并同步模式。 // @author Domus // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect my.eudic.net // @license MIT // ==/UserScript== (function() { 'use strict'; // 初始化默认配置 const defaultConfig = { words: ['English', '日本語', 'العربية', '...'], bgColor: '#EAEAEA', textColor: '#000000', highlightMode: 'all', whitelist: [], bookId: '', syncMode: 'overwrite' }; let config = GM_getValue('jp_hl_config', defaultConfig); if (!config.highlightMode) config.highlightMode = 'all'; if (!config.whitelist) config.whitelist = []; if (config.bookId === undefined) config.bookId = ''; if (!config.syncMode) config.syncMode = 'overwrite'; let wordsRegex = null; function compileRegex() { if (!config.words || config.words.length === 0) { wordsRegex = null; return; } const sortedWords = [...config.words].sort((a, b) => b.length - a.length); const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const pattern = sortedWords.map(word => { const escaped = escapeRegExp(word); if (/^[a-zA-Z\s]+$/.test(word)) { return `\\b${escaped}\\b`; } return escaped; }).join('|'); wordsRegex = new RegExp(`(${pattern})`, 'gi'); } compileRegex(); // 判断当前页面是否应该高亮 function shouldHighlight() { if (config.highlightMode === 'all') return true; if (config.highlightMode === 'whitelist') { const currentHost = window.location.hostname; return config.whitelist.some(domain => currentHost.includes(domain)); } return true; } function highlightText(node) { if (!shouldHighlight()) return; if (!wordsRegex) return; const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, { acceptNode: function(textNode) { const parentElement = textNode.parentElement; const parentName = parentElement.nodeName; const parentClass = parentElement.className; if (['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'RT', 'RP', 'NOSCRIPT'].includes(parentName)) { return NodeFilter.FILTER_REJECT; } if (parentClass === 'jp-hl-word' || parentElement.closest('#jp-hl-settings') || parentElement.closest('a, button, [class*="button"], [class*="btn"], nav, header, footer')) { return NodeFilter.FILTER_REJECT; } return textNode.data.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; } }); const nodesToReplace = []; let currentNode; while ((currentNode = walker.nextNode())) { if (wordsRegex.test(currentNode.data)) { nodesToReplace.push(currentNode); } } nodesToReplace.forEach(textNode => { const span = document.createElement('span'); span.innerHTML = textNode.data.replace(wordsRegex, (match) => { return `${match}`; }); textNode.parentNode.replaceChild(span, textNode); }); } function observeDOM() { const observer = new MutationObserver((mutations) => { if (!shouldHighlight()) return; mutations.forEach(mutation => { if (mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(addedNode => { if (addedNode.nodeType === Node.ELEMENT_NODE) { highlightText(addedNode); } else if (addedNode.nodeType === Node.TEXT_NODE) { highlightText(addedNode.parentNode); } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); } // ---------- 欧路同步功能 ---------- async function syncEudic() { const limit = 4000; let start = 0; let allWords = []; let hasMore = true; const statusEl = document.getElementById('jp-hl-sync-status'); const btn = document.getElementById('jp-hl-sync-eudic'); const bookSelect = document.getElementById('jp-hl-book-select'); const syncModeSelect = document.getElementById('jp-hl-sync-mode'); if (!btn) return; const bookId = bookSelect ? bookSelect.value : ''; const syncMode = syncModeSelect ? syncModeSelect.value : 'overwrite'; config.bookId = bookId; config.syncMode = syncMode; GM_setValue('jp_hl_config', config); btn.disabled = true; btn.textContent = '同步中...'; if (statusEl) statusEl.textContent = `正在获取${bookId ? '指定' : '默认/全部'}生词...`; while (hasMore) { try { let url = `https://my.eudic.net/StudyList/WordsDataSource?start=${start}&length=${limit}`; if (bookId) { url += `&categoryId=${encodeURIComponent(bookId)}`; } const result = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, withCredentials: true, onload: function(response) { try { resolve(JSON.parse(response.responseText)); } catch(e) { reject(e); } }, onerror: reject, ontimeout: () => reject(new Error("请求超时")) }); }); if (result.data && Array.isArray(result.data) && result.data.length > 0) { const words = result.data.map(item => item.uuid.trim()).filter(w => w !== ''); allWords = allWords.concat(words); start += limit; await new Promise(resolve => setTimeout(resolve, 500)); } else { hasMore = false; } } catch(e) { if (statusEl) statusEl.textContent = '同步失败:' + e.message; btn.disabled = false; btn.textContent = '同步生词'; alert('同步失败,请确保已登录欧路网站 (my.eudic.net) 并检查网络。'); return; } } const uniqueWords = [...new Set(allWords)]; if (uniqueWords.length === 0) { if (statusEl) statusEl.textContent = '该生词本为空或未登录'; btn.disabled = false; btn.textContent = '同步生词'; alert('未获取到任何生词,请确认已登录且有内容。'); return; } let finalWords; if (syncMode === 'overwrite') { finalWords = uniqueWords; } else { const existing = config.words || []; finalWords = [...new Set([...existing, ...uniqueWords])]; } config.words = finalWords; GM_setValue('jp_hl_config', config); compileRegex(); const wordsTextarea = document.getElementById('jp-hl-words'); if (wordsTextarea) { wordsTextarea.value = finalWords.join('\n'); } if (statusEl) statusEl.textContent = `同步成功,共 ${finalWords.length} 个单词 (${syncMode === 'overwrite' ? '覆盖' : '合并'})`; btn.disabled = false; btn.textContent = '同步生词'; alert(`同步成功!共 ${finalWords.length} 个生词,即将刷新页面。`); location.reload(); } // ---------- 抓取生词本列表 ---------- function fetchBookList() { const btn = document.getElementById('jp-hl-refresh-books'); const select = document.getElementById('jp-hl-book-select'); const status = document.getElementById('jp-hl-book-status'); if (!btn || !select) return; btn.disabled = true; btn.textContent = '获取中...'; if (status) status.textContent = '正在请求欧路页面...'; GM_xmlhttpRequest({ method: "GET", url: "https://my.eudic.net/studylist", withCredentials: true, onload: function(response) { try { const html = response.responseText; const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const items = doc.querySelectorAll('a.new_cateitem_click'); let options = []; options.push({ id: '', name: '📚 默认/全部生词本' }); items.forEach(el => { const id = el.getAttribute('data-id'); const title = el.getAttribute('title') || el.textContent.trim(); if (id !== null && id !== undefined && id !== '-1') { options.push({ id: id, name: title }); } }); const seen = new Set(); options = options.filter(opt => { const key = opt.id; if (seen.has(key)) return false; seen.add(key); return true; }); select.innerHTML = ''; options.forEach(opt => { const option = document.createElement('option'); option.value = opt.id; option.textContent = opt.name; select.appendChild(option); }); if (config.bookId) { select.value = config.bookId; } else { select.value = ''; } if (status) status.textContent = `获取成功,共 ${options.length} 个生词本`; config.bookId = select.value; GM_setValue('jp_hl_config', config); } catch(e) { if (status) status.textContent = '解析失败:' + e.message; alert('解析生词本列表失败,请确保已登录欧路。'); } btn.disabled = false; btn.textContent = '刷新词本'; }, onerror: function(err) { if (status) status.textContent = '请求失败'; alert('无法访问欧路页面,请确保已登录 my.eudic.net 并检查网络。'); btn.disabled = false; btn.textContent = '刷新词本'; }, ontimeout: function() { if (status) status.textContent = '请求超时'; alert('请求超时,请检查网络。'); btn.disabled = false; btn.textContent = '刷新词本'; } }); } // --------------------------------- // UI 设置面板(新布局) function createSettingsPanel() { if (document.getElementById('jp-hl-settings')) return; const panel = document.createElement('div'); panel.id = 'jp-hl-settings'; panel.innerHTML = `