// ==UserScript== // @name 多语种生词高亮助手 // @name:en Multilingual Vocabulary Highlighting Assistant // @name:ja 多言語新語ハイライトアシスタント // @namespace https://github.com/duxueminsi/Multilingual-Vocabulary-Highlighting-Assistant // @version 26.06.22.1 // @description 自定义多语种网页生词高亮工具,支持欧路生词本自动选择与覆盖/合并同步模式。 // @description:en Customizable multilingual web page new word highlighter, with Eudic notebook selection and overwrite/merge sync. // @description:ja カスタマイズ可能な多言語ウェブページ用単語ハイライトツール、欧路単語帳の自動選択とカバー/マージ同期モードをサポート。 // @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 highlightText(node) { 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) => { 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 = `

生词高亮设置

×
📌 操作提示:
1. 请先登录欧路词典网页(https://my.eudic.net/
2. 点击“刷新词本”获取生词本(需已登录)
3. 尽量在欧路内修改,然后使用“覆盖列表”模式同步
4. 建议搭配欧路词典的鼠标取词功能使用
高亮配色
背景颜色 (HEX):
文字颜色 (HEX):
`; document.body.appendChild(panel); // UI 交互绑定 document.getElementById('jp-hl-close').onclick = () => panel.remove(); const bgPicker = document.getElementById('jp-hl-bg'); const bgHex = document.getElementById('jp-hl-bg-hex'); const textPicker = document.getElementById('jp-hl-text'); const textHex = document.getElementById('jp-hl-text-hex'); bgPicker.addEventListener('input', e => bgHex.value = e.target.value); bgHex.addEventListener('input', e => bgPicker.value = e.target.value); textPicker.addEventListener('input', e => textHex.value = e.target.value); textHex.addEventListener('input', e => textPicker.value = e.target.value); const modeRadios = document.querySelectorAll('input[name="jp-hl-mode"]'); const whitelistContainer = document.getElementById('jp-hl-whitelist-container'); modeRadios.forEach(radio => { radio.addEventListener('change', (e) => { whitelistContainer.style.display = e.target.value === 'whitelist' ? 'flex' : 'none'; }); }); // 保存逻辑 document.getElementById('jp-hl-save').onclick = () => { const rawWords = document.getElementById('jp-hl-words').value; const parsedWords = [...new Set(rawWords.split('\n').map(w => w.trim()).filter(w => w !== ''))]; const selectedMode = document.querySelector('input[name="jp-hl-mode"]:checked').value; const rawWhitelist = document.getElementById('jp-hl-whitelist').value; const parsedWhitelist = [...new Set(rawWhitelist.split('\n').map(d => d.trim()).filter(d => d !== ''))]; const bookSelect = document.getElementById('jp-hl-book-select'); const bookId = bookSelect ? bookSelect.value : ''; const syncMode = document.getElementById('jp-hl-sync-mode').value; config.bgColor = bgHex.value; config.textColor = textHex.value; config.words = parsedWords; config.highlightMode = selectedMode; config.whitelist = parsedWhitelist; config.bookId = bookId; config.syncMode = syncMode; GM_setValue('jp_hl_config', config); compileRegex(); alert('保存成功!正在刷新页面...'); location.reload(); }; document.getElementById('jp-hl-sync-eudic').addEventListener('click', syncEudic); document.getElementById('jp-hl-refresh-books').addEventListener('click', fetchBookList); // 自动加载生词本列表 setTimeout(() => { const select = document.getElementById('jp-hl-book-select'); if (select && select.options.length <= 1) { fetchBookList(); } }, 1000); } GM_addStyle(` #jp-hl-settings { position: fixed; top: 50px; right: 50px; width: 380px; background: #fff; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); padding: 15px; z-index: 999999; font-family: sans-serif; color: #333; max-height: 90vh; overflow-y: auto; } .jp-hl-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 10px; } .jp-hl-header h3 { margin: 0; font-size: 16px; } #jp-hl-close { font-size: 20px; cursor: pointer; color: #999; } #jp-hl-close:hover { color: #333; } .jp-hl-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 14px; } .jp-hl-row input[type="color"] { width: 30px; height: 30px; border: none; padding: 0; cursor: pointer; } .jp-hl-row input[type="text"] { width: 80px; text-align: center; border: 1px solid #ccc; border-radius: 4px; padding: 4px; } .jp-hl-row-col { display: flex; flex-direction: column; margin-bottom: 15px; } .jp-hl-row-col label { margin-bottom: 5px; font-size: 13px; color: #333; } .jp-hl-row-col textarea { width: 100%; border: 1px solid #ccc; border-radius: 4px; padding: 5px; resize: vertical; box-sizing: border-box; font-family: inherit; } .jp-hl-actions { text-align: right; } .jp-hl-actions button { background: #189fd8; color: #fff; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; } .jp-hl-actions button:hover { background: #1588b6; } #jp-hl-sync-eudic:hover { background: #e0e0e0; } #jp-hl-sync-eudic:disabled { opacity: 0.6; cursor: not-allowed; } #jp-hl-refresh-books:hover { background: #e0e0e0; } #jp-hl-book-select { font-size: 12px; } .jp-hl-row-col a { color: #189fd8; text-decoration: none; } .jp-hl-row-col a:hover { text-decoration: underline; } #jp-hl-sync-mode { font-size: 12px; } #jp-hl-sync-status { margin-left: 4px; } `); GM_registerMenuCommand("⚙️ 配置生词高亮", createSettingsPanel); 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; } window.addEventListener('load', () => { if (shouldHighlight()) { highlightText(document.body); observeDOM(); } }); })();