├── .gitignore ├── images ├── icons │ ├── 16.png │ ├── 32.png │ ├── 48.png │ ├── 64.png │ └── 128.png └── getting-started │ ├── 5.jpg │ ├── 1_new.jpg │ ├── 2_new.jpg │ └── 3_new.jpg ├── _locales ├── zh_CN │ └── messages.json └── en │ └── messages.json ├── content_scripts ├── content_scripts.css └── highlight.js ├── README.md ├── manifest.json ├── popup ├── popup.css ├── popup.html └── popup.js ├── service_worker.js └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /images/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqdd/highlight_new_words/HEAD/images/icons/16.png -------------------------------------------------------------------------------- /images/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqdd/highlight_new_words/HEAD/images/icons/32.png -------------------------------------------------------------------------------- /images/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqdd/highlight_new_words/HEAD/images/icons/48.png -------------------------------------------------------------------------------- /images/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqdd/highlight_new_words/HEAD/images/icons/64.png -------------------------------------------------------------------------------- /images/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqdd/highlight_new_words/HEAD/images/icons/128.png -------------------------------------------------------------------------------- /images/getting-started/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqdd/highlight_new_words/HEAD/images/getting-started/5.jpg -------------------------------------------------------------------------------- /images/getting-started/1_new.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqdd/highlight_new_words/HEAD/images/getting-started/1_new.jpg -------------------------------------------------------------------------------- /images/getting-started/2_new.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqdd/highlight_new_words/HEAD/images/getting-started/2_new.jpg -------------------------------------------------------------------------------- /images/getting-started/3_new.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqdd/highlight_new_words/HEAD/images/getting-started/3_new.jpg -------------------------------------------------------------------------------- /_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "有道词典生词本高亮", 4 | "description": "The title of the application, displayed in the web store." 5 | }, 6 | "appDesc": { 7 | "message": "在英文网站中高亮显示有道词典生词本中的单词", 8 | "description": "The description of the application, displayed in the web store." 9 | } 10 | } -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "highlight the youdao dictionary new words book", 4 | "description": "The title of the application, displayed in the web store." 5 | }, 6 | "appDesc": { 7 | "message": "highlight the youdao dictionary new words book content in the english web site", 8 | "description": "The description of the application, displayed in the web store." 9 | } 10 | } -------------------------------------------------------------------------------- /content_scripts/content_scripts.css: -------------------------------------------------------------------------------- 1 | .xqdd_highlight_disable { 2 | } 3 | 4 | .xqdd_highlight_new_word { 5 | } 6 | 7 | 8 | .xqdd_bubble { 9 | flex-direction: column; 10 | width: 200px; 11 | align-items: center; 12 | position: fixed; 13 | display: none; 14 | z-index: 1000000; 15 | } 16 | .xqdd_bubble_word{ 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | justify-content: center; 21 | word-break: break-all; 22 | } 23 | 24 | .xqdd_bubble_word .xqdd_bubble_trans { 25 | width: auto; 26 | height: auto; 27 | } 28 | 29 | .xqdd_bubble_delete { 30 | position: absolute; 31 | color: rgba(255, 255, 255, 0.6); 32 | right: 2px; 33 | top: 0; 34 | cursor: pointer; 35 | } 36 | 37 | .xqdd_bubble_delete:hover { 38 | background-color: #e81123; 39 | color: white; 40 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 有道/欧路词典生词本网页高亮 2 | chrome插件,根据有道词典导出的xml生词文件或有道和欧路网上的生词本,在网页中标出生词,达到单词加强记忆效果 3 | 4 | 5 | # 快速使用 6 | > 欧路词典步骤一致 7 | ### 1. 登录有道同步生词本 8 | ![](./images/getting-started/1_new.jpg '登录有道步骤1') 9 | ![](./images/getting-started/2_new.jpg '登录有道步骤2') 10 | ![](./images/getting-started/3_new.jpg '登录有道步骤3') 11 | 12 | 13 | ### 2. 刷新页面并使用 14 | ![](./images/getting-started/5.jpg '效果演示') 15 | 16 | 17 | 18 | > # 为什么是有道词典 19 | > 1. 有道老牌单词软件,用户量较多 20 | > 2. 有道查词方便,支持快捷键呼出界面查词 21 | > 3. 有道支持将查询过的单词自动加入生词本功能(不足的是中文也会自动加入) 22 | > 4. 有道支持生词本导出功能 23 | > 5. 有道支持自定义释义功能 24 | > 6. 我在使用有道词典 25 | 26 | # 待加强功能 27 | - [x] 鼠标悬浮提示单词意思 28 | - [ ] 支持根据文件路径解析xml文件 29 | - [x] 校验文件类型 30 | - [ ] 记忆xml导出路径,自动更新xml文件 31 | - [x] 鼠标悬浮发音 32 | - [x] 发音可配置(开关、语速、男女、种类等) 33 | - [x] 提供多种高亮样式和自定义高亮样式 34 | - [ ] 支持高亮短语 35 | - [x] 手动导入导出生词本确实不便,寻找后台自动导入的可能性 36 | - [x] 拓展图标显示页面生词个数 37 | 38 | # 鸣谢 39 | - https://github.com/mechatroner/aided_reading 40 | - http://iconfont.cn/ 41 | >待研究 42 | >- https://github.com/waynecz/dadda-translate-crx 43 | >- https://github.com/cclient/chrome-extensions-youdaowithwordnode -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_appName__", 4 | "version": "2.0.1", 5 | "default_locale": "zh_CN", 6 | "description": "__MSG_appDesc__", 7 | "background": { 8 | "service_worker": "service_worker.js" 9 | }, 10 | "options_ui": { 11 | "page": "popup/popup.html" 12 | }, 13 | "options_page": "options.html", 14 | "icons": { 15 | "16": "images/icons/16.png", 16 | "32": "images/icons/32.png", 17 | "48": "images/icons/48.png", 18 | "64": "images/icons/64.png", 19 | "128": "images/icons/128.png" 20 | }, 21 | "content_scripts": [ 22 | { 23 | "all_frames": true, 24 | "matches": [ 25 | "" 26 | ], 27 | "js": [ 28 | "lib/jquery-3.3.1.js", 29 | "content_scripts/highlight.js" 30 | ], 31 | "css": [ 32 | "content_scripts/content_scripts.css" 33 | ] 34 | } 35 | ], 36 | "permissions": [ 37 | "cookies", 38 | "storage", 39 | "tts", 40 | "tabs", 41 | "activeTab", 42 | "scripting" 43 | ], 44 | "action": { 45 | "default_popup": "popup/popup.html", 46 | "default_icon": { 47 | "16": "images/icons/16.png", 48 | "32": "images/icons/32.png", 49 | "48": "images/icons/48.png", 50 | "64": "images/icons/64.png", 51 | "128": "images/icons/128.png" 52 | } 53 | }, 54 | "content_security_policy": {}, 55 | "host_permissions": [ 56 | "http://*/*", 57 | "https://*/*", 58 | "file://*/*", 59 | "ftp://*/*" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /popup/popup.css: -------------------------------------------------------------------------------- 1 | .setting { 2 | min-width: 320px; 3 | padding: 5px; 4 | font-family: sans-serif; 5 | } 6 | 7 | .setting > div { 8 | height: auto; 9 | display: flex; 10 | align-items: center; 11 | margin-bottom: 2px; 12 | } 13 | 14 | .setting > div > span, .setting > div > div > span, .setting > div > label > span { 15 | font-weight: bold; 16 | font-size: 13px; 17 | margin-right: 5px; 18 | min-width: 80px; 19 | display: inline-block; 20 | } 21 | 22 | .setting > div > a { 23 | margin-left: 5px; 24 | text-decoration: none; 25 | color: #007bff; 26 | } 27 | 28 | .setting > div > a:hover { 29 | text-decoration: underline; 30 | } 31 | 32 | .setting > div > input, .setting > div > select { 33 | vertical-align: middle; 34 | margin-left: 5px; 35 | width: auto; 36 | flex: 1; 37 | } 38 | 39 | .setting > div > input[type="file"] { 40 | width: auto; 41 | flex: 1; 42 | margin-left: 5px; 43 | } 44 | 45 | .setting > div > button { 46 | margin-left: 5px; 47 | padding: 4px 8px; 48 | border: 1px solid #ccc; 49 | border-radius: 3px; 50 | cursor: pointer; 51 | font-size: 12px; 52 | } 53 | 54 | .setting > div > button:hover { 55 | background-color: #f0f0f0; 56 | } 57 | 58 | .template { 59 | margin-left: 5px; 60 | display: inline; 61 | font-size: 14px; 62 | } 63 | 64 | .color-schemes { 65 | display: flex; 66 | align-items: center; 67 | margin-bottom: 5px; 68 | } 69 | 70 | .color-schemes > span { 71 | font-weight: bold; 72 | font-size: small; 73 | margin-right: 5px; 74 | } 75 | 76 | .color-scheme-button { 77 | border: none; 78 | padding: 5px 10px; 79 | margin-right: 5px; 80 | cursor: pointer; 81 | border-radius: 3px; 82 | } 83 | 84 | .setting > div > label { 85 | display: flex; 86 | align-items: center; 87 | margin-right: 10px; 88 | width: 100%; 89 | } 90 | 91 | .setting > hr { 92 | margin: 5px 0; 93 | border: 0; 94 | border-top: 1px solid #eee; 95 | } 96 | 97 | .collapsible .collapsible-button { 98 | background-color: #f0f0f0; 99 | color: #444; 100 | cursor: pointer; 101 | padding: 8px; 102 | width: 100%; 103 | border: none; 104 | text-align: left; 105 | outline: none; 106 | font-size: 13px; 107 | margin-bottom: 5px; 108 | display: flex; 109 | justify-content: space-between; 110 | align-items: center; 111 | } 112 | 113 | .collapsible .collapsible-button:after { 114 | content: '\002B'; 115 | color: #777; 116 | font-weight: bold; 117 | float: right; 118 | margin-left: 5px; 119 | } 120 | 121 | .collapsible .collapsible-button.active:after { 122 | content: "\2212"; 123 | } 124 | 125 | .collapsible .collapsible-content { 126 | padding: 0 10px; 127 | max-height: 0; 128 | overflow: hidden; 129 | transition: max-height 0.2s ease-out; 130 | background-color: #f1f1f1; 131 | margin-bottom: 5px; 132 | } 133 | 134 | .collapsible .collapsible-content:target { 135 | max-height: 500px; /* Adjust as needed */ 136 | } 137 | -------------------------------------------------------------------------------- /popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 13 | 16 | 19 |
20 |
21 | 发音来源: 22 | 23 |
24 |
25 | 同步状态: 26 |

未导入单词, 请登陆相关平台后点击同步单词进行同步

27 |
28 |
29 |
30 | 有道词典: 31 | 32 | 登录 34 |
35 |
36 | 手动导入: 37 |
38 |
39 |
40 | 欧路词典: 41 | 42 | 登录 43 |
44 |
45 |
46 | 47 | 配色方案: 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 |
57 |
58 | 高亮背景: 59 |
English
60 |
61 |
62 | 高亮文字: 63 |
64 |
65 | 弹框背景: 66 |
English
67 |
68 |
69 | 弹框文字: 70 |
71 |
72 |
73 |
74 | 75 | 使用说明 76 | 更多声音(TTS 78 | Engine) 79 |
80 |
81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /service_worker.js: -------------------------------------------------------------------------------- 1 | function initSettings() { 2 | chrome.storage.local.set({ 3 | toggle: true, 4 | ttsToggle: true, 5 | ttsVoices: { 6 | lang: "en", 7 | }, 8 | highlightBackground: "#FFFF0010", 9 | highlightText: "", 10 | bubbleBackground: "#FFE4C4", 11 | bubbleText: "", 12 | syncTime: 0, 13 | //生词本类型,0有道,1欧路 14 | dictionaryType: 0, 15 | autoSync: true, 16 | cookie: false, 17 | }) 18 | } 19 | 20 | 21 | function setStyle(result) { 22 | let highlightCss = ".xqdd_highlight_new_word {background-color: " + result.highlightBackground + ";"; 23 | let bubbleCss = ".xqdd_bubble {background-color: " + result.bubbleBackground + ";"; 24 | if (result.highlightText.trim() !== "") { 25 | highlightCss += "color:" + result.highlightText + ";"; 26 | bubbleCss += "color:" + result.bubbleText + ";"; 27 | } 28 | highlightCss += "}"; 29 | bubbleCss += "}"; 30 | 31 | // 获取所有标签页 32 | chrome.tabs.query({}, function (tabs) { 33 | tabs.forEach(function (tab) { 34 | // 为每个标签页插入样式 35 | chrome.scripting.insertCSS({ 36 | target: { tabId: tab.id }, 37 | css: highlightCss + bubbleCss 38 | }); 39 | }); 40 | }); 41 | } 42 | 43 | //初始化配置 44 | chrome.storage.local.get(["ttsVoices", "syncTime", "autoSync", "cookie", "dictionaryType"], function (result) { 45 | if (!result.ttsVoices) { 46 | initSettings(); 47 | } 48 | if (result.autoSync && (new Date().getTime() - result.syncTime) > 60 * 60 * 24 * 1000) { 49 | console.log("同步中。。。") 50 | sync((msg) => { 51 | console.log(msg) 52 | }, result.dictionaryType) 53 | } 54 | }); 55 | 56 | function syncSuccess(wordInfos, cookie, sendResponse, dictionaryType) { 57 | chrome.storage.local.set({ newWords: { wordInfos }, dictionaryType, cookie, syncTime: new Date().getTime() }, () => { 58 | sendResponse("同步成功"); 59 | }); 60 | } 61 | 62 | let sync = function (sendResponse, dictionaryType) { 63 | //同步有道词典 64 | if (dictionaryType == 0) { 65 | getCookie(cookie => { 66 | fetch("http://dict.youdao.com/wordbook/webapi/words?limit=100000000&offset=0") 67 | .then(res => res.json()) 68 | .then(({ data }) => { 69 | console.log(data) 70 | 71 | let wordInfos = {}; 72 | if (data.total === 0) { 73 | sendResponse("同步成功,但生词本无内容,若实际有内容,请尝试重新登录"); 74 | return 75 | } 76 | data.itemList.forEach(w => { 77 | wordInfos[w.word.toLowerCase()] = w 78 | }); 79 | syncSuccess(wordInfos, cookie, sendResponse, dictionaryType); 80 | }).catch(e => { 81 | sendResponse("网络错误或服务器出错,请检查网络后再试或重新登录"); 82 | }); 83 | }) 84 | } 85 | //同步欧路词典 86 | else if (dictionaryType == 1) { 87 | syncEudic(sendResponse, dictionaryType); 88 | } 89 | }; 90 | 91 | 92 | async function syncEudic(sendResponse, dictionaryType) { 93 | let start = 0; 94 | let wordInfos = {}; 95 | let hasMoreData = true; 96 | let limit = 4000; 97 | 98 | while (hasMoreData) { 99 | try { 100 | const response = await fetch(`https://my.eudic.net/StudyList/WordsDataSource?start=${start}&length=${limit}`); 101 | const result = await response.json(); 102 | 103 | if (result.data && result.data.length > 0) { 104 | result.data.forEach(item => { 105 | wordInfos[item.uuid.toLowerCase()] = { 106 | phonetic: item.phon, 107 | trans: item.exp, 108 | word: item.uuid, 109 | link: item.word, 110 | }; 111 | }); 112 | 113 | start += limit; 114 | await new Promise(resolve => setTimeout(resolve, 500)); 115 | } else { 116 | hasMoreData = false; 117 | } 118 | } catch (e) { 119 | sendResponse("网络错误或服务器出错,请检查网络后再试或重新登录"); 120 | return; 121 | } 122 | } 123 | 124 | if (Object.keys(wordInfos).length > 0) { 125 | syncSuccess(wordInfos, null, sendResponse, dictionaryType); 126 | } else { 127 | sendResponse("同步成功,但生词本无内容,若实际有内容,请尝试重新登录"); 128 | } 129 | } 130 | 131 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 132 | let needResponse = true; 133 | if (request.type === "tts") { 134 | if (!!request.word && typeof request.word === "string") { 135 | chrome.storage.local.get(["ttsToggle", "ttsVoices"], function (result) { 136 | if (result.ttsToggle) { 137 | chrome.tts.speak(request.word, { ...result.ttsVoices }); 138 | } 139 | }) 140 | } 141 | } else if (request.type === "resetSettings") { 142 | initSettings() 143 | } else if (request.type === "setStyle") { 144 | chrome.storage.local.get(["highlightBackground", "highlightText", "bubbleBackground", "bubbleText"], function (result) { 145 | setStyle(result); 146 | }) 147 | } else if (request.type === "sync") { 148 | needResponse = false; 149 | sync(sendResponse, request.dictionaryType); 150 | } else if (request.type === "delete") { 151 | needResponse = false; 152 | let wordData = request.wordData; 153 | let word = wordData.word; 154 | chrome.storage.local.get(["dictionaryType"], function (result) { 155 | if (result.dictionaryType == 0) { 156 | fetch(`http://dict.youdao.com/wordbook/webapi/delete?itemId=${wordData.itemId}`) 157 | } else if (result.dictionaryType == 1) { 158 | fetch("https://dict.eudic.net/Dicts/SetStarRating", { 159 | method: 'POST', 160 | headers: { 161 | 'Content-Type': 'application/json' 162 | }, 163 | body: JSON.stringify({ rating: -1, word, lang: 'en' }) 164 | }) 165 | } 166 | }) 167 | chrome.storage.local.get("newWords", function (result) { 168 | let wordInfos = result.newWords.wordInfos; 169 | delete wordInfos[word.toLowerCase()]; 170 | chrome.storage.local.set({ newWords: { wordInfos } }); 171 | sendResponse("删除成功,刷新页面后生效"); 172 | }); 173 | } else if (request.type === "count") { 174 | const id = sender.tab.id 175 | if (!wordCount[id]) { 176 | wordCount[id] = new Set() 177 | } 178 | const word = request.word.toLowerCase() 179 | if (!wordCount[id].has(word)) { 180 | wordCount[id].add(word) 181 | updateWordCount(id) 182 | } 183 | } 184 | if (needResponse) { 185 | sendResponse(); 186 | } 187 | return true; 188 | // sendResponse('我是后台,我已收到你的消息:' + JSON.stringify(request)); 189 | }); 190 | 191 | 192 | function getCookie(callback) { 193 | chrome.storage.local.get("cookie", function (result) { 194 | if (!!result.cookie) { 195 | callback(result.cookie) 196 | } else { 197 | chrome.cookies.getAll({ url: "http://dict.youdao.com/wordbook/wordlist" }, cookies => { 198 | cookies.map(c => c.name + "=" + c.value).join(";") 199 | callback(cookies.map(c => c.name + "=" + c.value).join(";")) 200 | }) 201 | } 202 | }); 203 | } 204 | 205 | 206 | //处理单词计数 207 | let wordCount = {} 208 | let wordCountUrl = {} 209 | chrome.tabs.onUpdated.addListener(function (id, info, tab) { 210 | if (wordCountUrl[id] !== tab.url) { 211 | wordCountUrl[id] = tab.url 212 | if (wordCount[id]) { 213 | wordCount[id].clear(); 214 | } else { 215 | wordCount[id] = new Set() 216 | } 217 | updateWordCount(id) 218 | } 219 | if (info.status === "complete") { 220 | updateWordCount(id) 221 | } 222 | }) 223 | 224 | 225 | function updateWordCount(tabId) { 226 | if (wordCount[tabId]) { 227 | const count = wordCount[tabId].size; 228 | 229 | chrome.action.getBadgeText({ tabId }, function (countStr) { 230 | if (count === 0 && (!countStr || countStr === "")) { 231 | // 如果当前计数为0且徽章文字为空,则什么都不做 232 | return; 233 | } 234 | // 将徽章文字设置为当前的单词计数 235 | chrome.action.setBadgeText({ 236 | text: count.toString(), 237 | tabId 238 | }); 239 | }); 240 | } 241 | } 242 | 243 | chrome.tabs.onRemoved.addListener(function (tabId) { 244 | if (wordCount[tabId]) { 245 | wordCount[tabId].clear() 246 | delete wordCount[tabId] 247 | delete wordCountUrl[tabId] 248 | } 249 | }) 250 | -------------------------------------------------------------------------------- /popup/popup.js: -------------------------------------------------------------------------------- 1 | let ttsVoices = $("#tts_voices"); 2 | let ttsToggle = $("#tts_toggle"); 3 | let highlightBackground = $("#highlight_background"); 4 | let highlightText = $("#highlight_text"); 5 | let highlightTemplate = $("#highlight_template"); 6 | let bubbleBackground = $("#bubble_background"); 7 | let bubbleTemplate = $("#bubble_template"); 8 | let bubbleText = $("#bubble_text"); 9 | let toggle = $("#toggle"); 10 | let reset = $("#reset"); 11 | let wordStatus = $("#word_status"); 12 | let xmlFile = $("#file"); 13 | let syncButton = $(".sync"); 14 | let autoSync = $("#auto_sync"); 15 | let youdaoLoginATag = $("#youdao_login"); 16 | let oluLoginATag = $("#olu_login"); 17 | 18 | //日期加强 19 | 20 | // 对Date的扩展,将 Date 转化为指定格式的String 21 | // 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符, 22 | // 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字) 23 | // 例子: 24 | // (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423 25 | // (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18 26 | Date.prototype.format = function (fmt) { //author: meizz 27 | var o = { 28 | "M+": this.getMonth() + 1, //月份 29 | "d+": this.getDate(), //日 30 | "h+": this.getHours(), //小时 31 | "m+": this.getMinutes(), //分 32 | "s+": this.getSeconds(), //秒 33 | "q+": Math.floor((this.getMonth() + 3) / 3), //季度 34 | "S": this.getMilliseconds() //毫秒 35 | }; 36 | if (/(y+)/.test(fmt)) 37 | fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); 38 | for (var k in o) 39 | if (new RegExp("(" + k + ")").test(fmt)) 40 | fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); 41 | return fmt; 42 | } 43 | 44 | 45 | function getRandomColor() { 46 | var rand = Math.floor(Math.random() * 0xFFFFFF).toString(16); 47 | if (rand.length == 6) { 48 | return rand; 49 | } else { 50 | return getRandomColor(); 51 | } 52 | } 53 | 54 | //单词样式相关 55 | function initStyle() { 56 | chrome.storage.local.get([ 57 | "highlightBackground" 58 | , "highlightText" 59 | , "bubbleBackground" 60 | , "bubbleText"], function (result) { 61 | highlightBackground.val(result.highlightBackground) 62 | highlightText.val(result.highlightText) 63 | highlightTemplate.css("background-color", result.highlightBackground) 64 | if (result.highlightText.trim() !== "") { 65 | highlightTemplate.css("color", result.highlightText) 66 | } 67 | bubbleBackground.val(result.bubbleBackground) 68 | bubbleText.val(result.bubbleText) 69 | bubbleTemplate.css("background-color", result.bubbleBackground) 70 | if (result.bubbleText.trim() !== "") { 71 | bubbleTemplate.css("color", result.bubbleText) 72 | } 73 | chrome.runtime.sendMessage({ type: "setStyle" }) 74 | }) 75 | } 76 | 77 | 78 | function updateWordStatus(color, err) { 79 | let status = ""; 80 | if (err) { 81 | status = "导入失败" 82 | wordStatus.css("color", "#" + color); 83 | } else { 84 | chrome.storage.local.get(["newWords", "syncTime", "dictionaryType"], function (result) { 85 | if (result.newWords) { 86 | status = Object.keys(result.newWords.wordInfos).length + "个词" 87 | if (!!color) { 88 | wordStatus.css("color", "#" + color); 89 | } 90 | status += ", 同步于" + (new Date(result.syncTime)).format("yyyy-MM-dd hh:mm:ss") 91 | if (result.dictionaryType == 0) { 92 | status = "有道, " + status 93 | } else if (result.dictionaryType == 1) { 94 | status = "欧路, " + status 95 | } 96 | } else { 97 | status = "未导入单词, 请登陆相关平台后点击同步单词进行同步" 98 | } 99 | wordStatus.text(status) 100 | }) 101 | } 102 | wordStatus.text(status) 103 | } 104 | 105 | //配置 106 | function initSettings() { 107 | chrome.storage.local.get([ 108 | "ttsToggle" 109 | , "ttsVoices" 110 | , "autoSync" 111 | , "syncTime" 112 | , "cookie" 113 | , "dictionaryType" 114 | , "toggle" 115 | ], function (result) { 116 | //总开关 117 | toggle.prop("checked", result.toggle) 118 | //发音开关 119 | ttsToggle.prop("checked", result.ttsToggle) 120 | //自动同步开关 121 | autoSync.prop("checked", result.autoSync) 122 | //发音人员 123 | chrome.tts.getVoices( 124 | function (voices) { 125 | let oldVoice = result.ttsVoices 126 | let index = 1; 127 | let isSelected = false 128 | ttsVoices.empty() 129 | for (let i = 0; i < voices.length; i++) { 130 | let voice = voices[i]; 131 | delete voice.eventTypes; 132 | if (!voice.lang 133 | || voice.lang.toLowerCase().startsWith("en") 134 | || voice.lang.toLowerCase().startsWith("xx")) { 135 | let option = $("")); 153 | } 154 | }); 155 | //高亮 156 | initStyle(); 157 | //同步状态 158 | updateWordStatus() 159 | }) 160 | } 161 | 162 | 163 | xmlFile.on("change", function () { 164 | let file = this.files[0] 165 | if (file) { 166 | let reader = new FileReader() 167 | reader.onload = function () { 168 | try { 169 | let xml = $($.parseXML(this.result)) 170 | let wordInfos = {} 171 | let transList = []; 172 | let phoneticList = []; 173 | // var originWordList = []; 174 | // var wordList = []; 175 | var items = xml.find("item"); 176 | items.each(function (i, val) { 177 | let node = $(val) 178 | let word = node.find("word").text() 179 | // originWordList.push(word) 180 | // wordList.push(word.toLowerCase()) 181 | wordInfos[word.toLowerCase()] = { 182 | word, 183 | trans: node.find("trans").text(), 184 | phonetic: node.find("phonetic").text() 185 | } 186 | }) 187 | chrome.storage.local.set({ newWords: { wordInfos } }) 188 | //提示导入成功 189 | // chrome.notifications.create({ 190 | // type: "basic", 191 | // title: "test", 192 | // message: "解析单词本成功" 193 | // }) 194 | updateWordStatus(getRandomColor(), false) 195 | // chrome.tabs.query({active: true, currentWindow: true}, function (tabs) { 196 | // chrome.tabs.sendMessage(tabs[0].id, {type: "importSuccess"}); 197 | // }); 198 | } catch (err) { 199 | updateWordStatus(getRandomColor(), true) 200 | } finally { 201 | xmlFile.val(""); 202 | } 203 | } 204 | reader.readAsText(file) 205 | } 206 | }) 207 | 208 | 209 | //事件监听 210 | ttsVoices.on("click", function () { 211 | let val = $("#tts_voices option:selected").val(); 212 | if (val && val !== "" && val !== "无") { 213 | let ttsVoices = JSON.parse(val); 214 | chrome.storage.local.set({ ttsVoices }) 215 | } 216 | }) 217 | ttsToggle.on("click", function () { 218 | chrome.storage.local.set({ ttsToggle: ttsToggle.prop("checked") }) 219 | }) 220 | toggle.on("click", function () { 221 | chrome.storage.local.set({ toggle: toggle.prop("checked") }) 222 | }) 223 | autoSync.on("click", function () { 224 | chrome.storage.local.set({ autoSync: autoSync.prop("checked") }) 225 | }) 226 | highlightText.on("input", function () { 227 | chrome.storage.local.set({ highlightText: highlightText.val() }, function () { 228 | initStyle() 229 | }) 230 | }) 231 | highlightBackground.on("input", function () { 232 | chrome.storage.local.set({ highlightBackground: highlightBackground.val() }, function () { 233 | initStyle() 234 | }) 235 | }) 236 | bubbleText.on("input", function () { 237 | chrome.storage.local.set({ bubbleText: bubbleText.val() }, function () { 238 | initStyle() 239 | }) 240 | }) 241 | bubbleBackground.on("input", function () { 242 | chrome.storage.local.set({ bubbleBackground: bubbleBackground.val() }, function () { 243 | initStyle() 244 | }) 245 | }) 246 | reset.on("click", function () { 247 | if (window.confirm("确认恢复默认设置?")) { 248 | chrome.runtime.sendMessage({ type: "resetSettings" }, function () { 249 | initSettings() 250 | }) 251 | } 252 | }) 253 | $("#help").on("click", function () { 254 | chrome.tabs.create({ url: 'https://github.com/XQDD/highlight_new_words' }); 255 | }) 256 | $("#moreVoices").on("click", function () { 257 | chrome.tabs.create({ url: 'https://chrome.google.com/webstore/detail/speakit/pgeolalilifpodheeocdmbhehgnkkbak' }); 258 | }) 259 | youdaoLoginATag.on("click", function () { 260 | chrome.tabs.create({ url: 'http://dict.youdao.com/wordbook/wordlist' }); 261 | }) 262 | oluLoginATag.on("click", function () { 263 | chrome.tabs.create({ url: 'https://my.eudic.net/studylist' }); 264 | }) 265 | syncButton.click(function () { 266 | let clickedSyncButton = $(this) 267 | clickedSyncButton.attr("disabled", "") 268 | chrome.runtime.sendMessage({ type: "sync", dictionaryType: $(this).attr("dictionaryType") }, function (msg) { 269 | if (msg) { 270 | alert(msg) 271 | } 272 | initSettings() 273 | clickedSyncButton.removeAttr("disabled") 274 | }) 275 | }) 276 | 277 | $(".color-scheme-button").on("click", function () { 278 | const bgColor = $(this).data("bg"); 279 | const textColor = $(this).data("text"); 280 | highlightBackground.val(bgColor); 281 | highlightText.val(textColor); 282 | bubbleBackground.val(bgColor); 283 | bubbleText.val(textColor); 284 | chrome.storage.local.set({ 285 | highlightBackground: bgColor, 286 | highlightText: textColor, 287 | bubbleBackground: bgColor, 288 | bubbleText: textColor 289 | }, function () { 290 | initStyle(); 291 | }); 292 | }); 293 | 294 | 295 | initSettings() 296 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /content_scripts/highlight.js: -------------------------------------------------------------------------------- 1 | //生词本 2 | var newWords; 3 | //当前要显示的节点 4 | var currNode 5 | //鼠标节点(实时) 6 | var mouseNode 7 | //已经显示了的节点 8 | var showedNode 9 | //是否允许隐藏气泡 10 | var isAllowHideBubble = true 11 | //气泡显示/隐藏延迟时间(ms) 12 | var delayed = 100 13 | //生词信息列表 14 | var currWord 15 | var currWordData 16 | 17 | $(function () { 18 | init() 19 | }) 20 | 21 | 22 | /** 23 | * 初始化 24 | */ 25 | function init() { 26 | //从localstorege获取生词列表,高亮所有匹配的节点 27 | chrome.storage.local.get(["toggle"], function (r) { 28 | if (r.toggle) { 29 | chrome.storage.local.get(["newWords"], function (result) { 30 | // var before = new Date().getTime() 31 | newWords = result.newWords; 32 | highlight(textNodesUnder(document.body)) 33 | //console.log("解析总耗时:" + (new Date().getTime() - before) + " ms") 34 | 35 | //在插入节点时修改 36 | // Create an observer instance 37 | const observer = new MutationObserver(onNodeInserted); 38 | // Configuration object for the observer 39 | const config = { childList: true, subtree: true }; 40 | // Start observing a target node (document.body in this case) 41 | observer.observe(document.body, config); 42 | chrome.runtime.sendMessage({ type: "setStyle" }) 43 | }) 44 | 45 | } 46 | }) 47 | 48 | //创建鼠标悬浮气泡 49 | createBubble(); 50 | 51 | //监听xml导入成功消息 52 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 53 | if (request.type == "importSuccess") { 54 | alert("导入生词本成功") 55 | } 56 | }) 57 | 58 | 59 | } 60 | 61 | /** 62 | * 创建鼠标悬浮气泡 63 | */ 64 | function createBubble() { 65 | //创建添加到body中 66 | var div = $("
").attr("class", "xqdd_bubble") 67 | var deleteButton = $("") 68 | .attr("class", "xqdd_bubble_delete") 69 | .text("✖") 70 | .click(() => { 71 | if (window.confirm("确认删除单词?(若已登录云端,云端单词会同时删除)")) { 72 | chrome.runtime.sendMessage({ type: "delete", wordData: currWordData }, function (msg) { 73 | //取消高亮删除的单词 74 | $(`xqdd_highlight_new_word[word='${currWordData.word}']`).attr("class", "xqdd_highlight_disable") 75 | if (msg) { 76 | // alert(msg) 77 | } 78 | }) 79 | } 80 | }) 81 | var word = $("").attr("class", "xqdd_bubble_word") 82 | var trans = $("").attr("class", "xqdd_bubble_trans") 83 | div.append(deleteButton).append(word).append(trans) 84 | $(document.body).append(div) 85 | 86 | //添加鼠标进入离开事件 87 | div.on("mouseleave", function (e) { 88 | hideBubbleDelayed() 89 | }) 90 | div.on("mouseenter", function (e) { 91 | isAllowHideBubble = false 92 | }) 93 | 94 | //监听鼠标位置 95 | document.addEventListener("mousemove", handleMouseMove, false) 96 | // document.addEventListener("mousedown", hideBubble(), false) 97 | 98 | //监听窗口滚动 99 | window.addEventListener("scroll", function () { 100 | isAllowHideBubble = true 101 | hideBubble() 102 | }) 103 | } 104 | 105 | /** 106 | * 显示气泡 107 | */ 108 | function showBubble() { 109 | if (!!currNode) { 110 | var bubble = $(".xqdd_bubble") 111 | if (showedNode != currNode || bubble.css("display") != "flex") { 112 | var nodeRect = currNode.getBoundingClientRect(); 113 | var word = $(currNode).text() 114 | var wordInfo = newWords.wordInfos[word.toLowerCase()] 115 | $(".xqdd_bubble_word").html((wordInfo.link ? wordInfo.link : wordInfo.word) + " " + `${wordInfo["phonetic"]}`) 116 | $(".xqdd_bubble_trans").html(wordInfo["trans"]) 117 | currWord = wordInfo["word"] 118 | currWordData = wordInfo 119 | bubble 120 | .css("top", nodeRect.bottom + 'px') 121 | .css("left", Math.max(5, Math.floor((nodeRect.left + nodeRect.right) / 2) - 100) + 'px') 122 | .css("display", 'flex') 123 | chrome.runtime.sendMessage({ type: "tts", word }) 124 | showedNode = currNode 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * 处理鼠标移动 131 | * @param e 132 | */ 133 | function handleMouseMove(e) { 134 | //获取鼠标所在节点 135 | mouseNode = document.elementFromPoint(e.clientX, e.clientY); 136 | if (!mouseNode) { 137 | hideBubbleDelayed(mouseNode) 138 | return 139 | } 140 | var classAttr = ""; 141 | try { 142 | classAttr = mouseNode.getAttribute("class"); 143 | } catch (exc) { 144 | hideBubbleDelayed(mouseNode) 145 | return 146 | } 147 | if (!classAttr || !classAttr.startsWith("xqdd_")) { 148 | hideBubbleDelayed(mouseNode) 149 | return 150 | } 151 | isAllowHideBubble = false 152 | if (!classAttr.startsWith("xqdd_highlight_new_word")) { 153 | return; 154 | } 155 | currNode = mouseNode 156 | //延迟显示(防止鼠标一闪而过的情况) 157 | setTimeout(function () { 158 | //是本节点 159 | if (currNode == mouseNode) { 160 | showBubble(); 161 | } 162 | //非本节点 163 | else if ($(mouseNode).attr("class") && !$(mouseNode).attr("class").startsWith("xqdd_")) { 164 | isAllowHideBubble = true 165 | } 166 | }, delayed) 167 | } 168 | 169 | /** 170 | * 延迟隐藏气泡 171 | */ 172 | function hideBubbleDelayed(mouseNode) { 173 | if (!isAllowHideBubble) { 174 | if (mouseNode) { 175 | if ($(mouseNode).parents(".xqdd_bubble").length > 0) { 176 | return; 177 | } 178 | } 179 | isAllowHideBubble = true 180 | setTimeout(function () { 181 | hideBubble(); 182 | }, delayed); 183 | } 184 | } 185 | 186 | 187 | /** 188 | * 隐藏气泡 189 | */ 190 | function hideBubble() { 191 | if (isAllowHideBubble) { 192 | $(".xqdd_bubble").css("display", "none") 193 | } 194 | } 195 | 196 | /** 197 | * 198 | * @param nodes 高亮所有节点 199 | * 200 | */ 201 | function highlight(nodes) { 202 | for (var i = 0; i < nodes.length; i++) { 203 | var node = nodes[i] 204 | var text = node.textContent 205 | if (text.trim() == "") { 206 | continue 207 | } 208 | //处理单个节点 209 | //新节点的内容 210 | var newNodeChildrens = highlightNode(text) 211 | var parent_node = node.parentNode 212 | //替换新节点 213 | if (newNodeChildrens.length == 0) { 214 | continue; 215 | } 216 | //处理a标签显示异常 217 | if (parent_node.tagName.toLowerCase() == "a") { 218 | parent_node.style.display = "inline-block"; 219 | parent_node.style.margin = "auto"; 220 | } 221 | for (var j = 0; j < newNodeChildrens.length; j++) { 222 | parent_node.insertBefore(newNodeChildrens[j], node); 223 | } 224 | parent_node.removeChild(node); 225 | } 226 | 227 | 228 | } 229 | 230 | /** 231 | * 高亮单个节点 232 | * @param text 233 | */ 234 | function highlightNode(texts) { 235 | // return [$("").css("background", "red").text(texts)[0]] 236 | //将句子解析成待检测单词列表 237 | var words = []; 238 | //使用indexof 239 | // var tempTexts = texts 240 | // while (tempTexts.length > 0) { 241 | // tempTexts = tempTexts.trim() 242 | // var pos = tempTexts.indexOf(" ") 243 | // if (pos < 0) { 244 | // words.push(tempTexts) 245 | // break 246 | // } else { 247 | // words.push(tempTexts.slice(0, pos)) 248 | // tempTexts = tempTexts.slice(pos) 249 | // } 250 | // } 251 | 252 | //使用split 253 | var tempTexts = texts.split(/\s/) 254 | for (i in tempTexts) { 255 | var tempText = tempTexts[i].trim() 256 | if (tempText != "") { 257 | words.push(tempText) 258 | } 259 | } 260 | 261 | if (words.length >= 1) { 262 | //处理后结果 263 | var newNodeChildrens = [] 264 | //剩下未处理的字符串 265 | var remainTexts = texts 266 | //已处理部分字符串 267 | var checkedText = "" 268 | for (var i = 0; i < words.length; i++) { 269 | var word = words[i] 270 | //当前所处位置 271 | var currPos = remainTexts.indexOf(word) 272 | //匹配单词 273 | // if (newWords.indexOf(word.toLowerCase()) !== -1) { 274 | if (newWords && newWords.wordInfos && newWords.wordInfos.hasOwnProperty(word.toLowerCase())) { 275 | //匹配成功 276 | //添加已处理部分到节点 277 | if (checkedText != "") { 278 | newNodeChildrens.push(document.createTextNode(checkedText)) 279 | checkedText = "" 280 | } 281 | if (currPos == 0) { 282 | // wordxx类型 283 | newNodeChildrens.push(hightlightText(word)) 284 | } else { 285 | //xxwordxx类型 286 | // var preText = remainTexts.slice(0, currPos) 287 | // if (i == 0 && preText.trim() == " ") { 288 | // //处理 之间的空格问题 289 | // newNodeChildrens.push($("").text(preText)[0]) 290 | // } else { 291 | newNodeChildrens.push(document.createTextNode(remainTexts.slice(0, currPos))) 292 | // } 293 | newNodeChildrens.push(hightlightText(word)) 294 | } 295 | chrome.runtime.sendMessage({ type: "count", word }) 296 | } else { 297 | //匹配失败,追加到已处理字符串 298 | checkedText += remainTexts.slice(0, currPos + word.length) 299 | } 300 | //删除已处理的字符(到当前单词的位置) 301 | remainTexts = remainTexts.slice(currPos + word.length) 302 | } 303 | //处理最末尾 304 | if (newNodeChildrens.length != 0) { 305 | if (checkedText != "") { 306 | newNodeChildrens.push(document.createTextNode(checkedText)) 307 | } 308 | newNodeChildrens.push(document.createTextNode(remainTexts)) 309 | } 310 | } 311 | return newNodeChildrens 312 | } 313 | 314 | 315 | /** 316 | * 高亮单个单词 317 | * @param text 318 | * @returns {*} 319 | */ 320 | function hightlightText(text) { 321 | //注意jqury对象转为dom对象使用[0]或者.get(0) 322 | return $("") 323 | .attr("word", text.toLowerCase()) 324 | .attr("class", "xqdd_highlight_new_word") 325 | .text(text)[0]; 326 | } 327 | 328 | 329 | /** 330 | * 过滤所有文本节点 331 | * @param el 332 | * @returns {Array} 333 | */ 334 | function textNodesUnder(el) { 335 | var n, a = [], 336 | walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, mygoodfilter, false); 337 | while (n = walk.nextNode()) { 338 | a.push(n); 339 | } 340 | return a; 341 | } 342 | 343 | 344 | /** 345 | * 节点过滤器 346 | * @param node 347 | * @returns {number} 348 | */ 349 | function mygoodfilter(node) { 350 | var good_tags_list = [ 351 | "PRE", 352 | "A", 353 | "P", 354 | "H1", 355 | "H2", 356 | "H3", 357 | "H4", 358 | "H5", 359 | "H6", 360 | "B", 361 | "SMALL", 362 | "STRONG", 363 | "Q", 364 | "DIV", 365 | "SPAN", 366 | "LI", 367 | "TD", 368 | "OPTION", 369 | "I", 370 | "BUTTON", 371 | "UL", 372 | "CODE", 373 | "EM", 374 | "TH", 375 | "CITE", 376 | ]; 377 | if (good_tags_list.indexOf(node.parentNode.tagName) !== -1) { 378 | return NodeFilter.FILTER_ACCEPT; 379 | } 380 | return NodeFilter.FILTER_SKIP; 381 | } 382 | 383 | 384 | /** 385 | * 节点插入时判断高亮 386 | * @param mutationsList 387 | */ 388 | function onNodeInserted(mutationsList) { 389 | mutationsList.forEach(mutation => { 390 | if (mutation.type === 'childList') { 391 | mutation.addedNodes.forEach(node => { 392 | var inobj = node; 393 | if (!inobj) return; 394 | if ($(inobj).parents(".xqdd_bubble").length > 0) return; 395 | 396 | var classattr = null; 397 | if (typeof inobj.getAttribute !== 'function') return; 398 | try { 399 | classattr = inobj.getAttribute('class'); 400 | } catch (e) { 401 | return; 402 | } 403 | 404 | if (!classattr || !classattr.startsWith("xqdd")) { 405 | highlight(textNodesUnder(inobj)); 406 | } 407 | }); 408 | } 409 | }); 410 | } 411 | --------------------------------------------------------------------------------