CogniRead

增强型阅读脚本,能够加粗部分单词,并高亮显示词汇表中的特定词汇,同时通过美观的悬浮框展示定义。可通过Alt+B(Windows/Linux)或Control+B(Mac)进行切换。

  1. // ==UserScript==
  2. // @name CogniRead
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description 增强型阅读脚本,能够加粗部分单词,并高亮显示词汇表中的特定词汇,同时通过美观的悬浮框展示定义。可通过Alt+B(Windows/Linux)或Control+B(Mac)进行切换。
  6. // @match *://*/*
  7. // @grant none
  8. // @license MIT
  9. // @author codeboy
  10. // @icon https://cdn.icon-icons.com/icons2/609/PNG/512/book-glasses_icon-icons.com_56355.png
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. let cogniEnabled = false;
  17. let wordData = {};
  18.  
  19. // Word list URLs
  20. const wordListSources = {
  21. 'TOEFL': 'https://raw.githubusercontent.com/CodeBoy2006/english-wordlists/master/TOEFL_abridged.txt',
  22. 'GRE': 'https://raw.githubusercontent.com/CodeBoy2006/english-wordlists/master/GRE_abridged.txt',
  23. 'OALD8': 'https://raw.githubusercontent.com/CodeBoy2006/english-wordlists/master/OALD8_abridged_edited.txt',
  24. 'TEM-8': 'https://raw.githubusercontent.com/CodeBoy2006/english-wordlists/refs/heads/master/%E8%8B%B1%E8%AF%AD%E4%B8%93%E4%B8%9A%E6%98%9F%E6%A0%87%E5%85%AB%E7%BA%A7%E8%AF%8D%E6%B1%87.txt'
  25. };
  26.  
  27. // Load word lists
  28. async function fetchWordLists() {
  29. for (let [listName, url] of Object.entries(wordListSources)) {
  30. try {
  31. const response = await fetch(url);
  32. if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  33. const text = await response.text();
  34. wordData[listName] = processWordList(text);
  35. console.log(`Loaded ${listName} wordlist with ${Object.keys(wordData[listName]).length} words.`);
  36. } catch (error) {
  37. console.error(`Failed to load ${listName} wordlist:`, error);
  38. }
  39. }
  40. }
  41.  
  42. // Process word list text
  43. function processWordList(text) {
  44. const lines = text.split('\n');
  45. const dictionary = {};
  46. lines.forEach(line => {
  47. const [word, ...rest] = line.split(/\s+/);
  48. if (word) {
  49. dictionary[word.toLowerCase()] = rest.join(' ');
  50. }
  51. });
  52. return dictionary;
  53. }
  54.  
  55. // Common suffixes
  56. const suffixes = ['s', 'es', 'ed', 'ing', 'er', 'est', 'ly'];
  57.  
  58. // Match word in lists
  59. function matchWordInLists(word) {
  60. word = word.toLowerCase();
  61.  
  62. // Direct match
  63. for (let list in wordData) {
  64. if (wordData[list].hasOwnProperty(word)) {
  65. return { word, list, definition: wordData[list][word] };
  66. }
  67. }
  68.  
  69. // Match after removing common suffixes
  70. for (let suffix of suffixes) {
  71. if (word.endsWith(suffix)) {
  72. let stem = word.slice(0, -suffix.length);
  73. for (let list in wordData) {
  74. if (wordData[list].hasOwnProperty(stem)) {
  75. return { word: stem, list, definition: wordData[list][stem] };
  76. }
  77. }
  78. }
  79. }
  80.  
  81. // Special cases
  82. if (word.endsWith('ies')) {
  83. let stem = word.slice(0, -3) + 'y';
  84. for (let list in wordData) {
  85. if (wordData[list].hasOwnProperty(stem)) {
  86. return { word: stem, list, definition: wordData[list][stem] };
  87. }
  88. }
  89. }
  90. if (word.endsWith('ves')) {
  91. let stem = word.slice(0, -3) + 'f';
  92. for (let list in wordData) {
  93. if (wordData[list].hasOwnProperty(stem)) {
  94. return { word: stem, list, definition: wordData[list][stem] };
  95. }
  96. }
  97. }
  98.  
  99. return null;
  100. }
  101.  
  102. function isWordInData(word) {
  103. return matchWordInLists(word) !== null;
  104. }
  105.  
  106. // Style word
  107. function styleWord(word) {
  108. const match = word.match(/^([\w'-]+)([^\w'-]*)$/);
  109. if (!match) return word;
  110.  
  111. const [, cleanWord, punctuation] = match;
  112. const wordLower = cleanWord.toLowerCase();
  113. const isHighlighted = isWordInData(cleanWord);
  114.  
  115. let boldLength = Math.ceil(cleanWord.length / 2);
  116. let formattedWord = `<b>${cleanWord.slice(0, boldLength)}</b>${cleanWord.slice(boldLength)}`;
  117.  
  118. if (isHighlighted) {
  119. formattedWord = `<span class="cogni-highlight" data-word="${wordLower}">${formattedWord}</span>`;
  120. } else {
  121. formattedWord = `<span class="cogni-word">${formattedWord}</span>`;
  122. }
  123.  
  124. return formattedWord + punctuation;
  125. }
  126.  
  127. // Handle text node
  128. function handleTextNode(textNode) {
  129. const words = textNode.textContent.split(/(\s+)/);
  130. const formattedText = words.map(word => word.trim() ? styleWord(word) : word).join('');
  131. const span = document.createElement('span');
  132. span.innerHTML = formattedText;
  133. textNode.replaceWith(span);
  134. }
  135.  
  136. // Traverse and style text
  137. function traverseAndStyleText(node) {
  138. if (node.nodeType === Node.TEXT_NODE) {
  139. if (node.textContent.trim().length > 0) {
  140. handleTextNode(node);
  141. }
  142. } else if (node.nodeType === Node.ELEMENT_NODE) {
  143. if (!['SCRIPT', 'STYLE', 'TEXTAREA'].includes(node.tagName)) {
  144. Array.from(node.childNodes).forEach(traverseAndStyleText);
  145. }
  146. }
  147. }
  148.  
  149. // Initialize tooltip
  150. function initTooltip() {
  151. let tooltip = document.createElement('div');
  152. tooltip.id = 'cogniread-tooltip';
  153. tooltip.style.cssText = `
  154. position: fixed;
  155. background: #ffffff;
  156. border: none;
  157. border-radius: 8px;
  158. padding: 12px 16px;
  159. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
  160. display: none;
  161. z-index: 9999;
  162. max-width: 300px;
  163. font-size: 14px;
  164. line-height: 1.6;
  165. color: #333333;
  166. pointer-events: none;
  167. transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
  168. opacity: 0;
  169. transform: translateY(10px);
  170. `;
  171. document.body.appendChild(tooltip);
  172.  
  173. return tooltip;
  174. }
  175.  
  176. // Display tooltip
  177. function displayTooltip(word, event) {
  178. const tooltip = document.getElementById('cogniread-tooltip');
  179. if (!tooltip) return;
  180.  
  181. const matchedWord = matchWordInLists(word);
  182. if (matchedWord) {
  183. const { word: matchedWordText, list: sourceList, definition } = matchedWord;
  184.  
  185. tooltip.innerHTML = `
  186. <strong>${word}</strong> ${word !== matchedWordText ? `(${matchedWordText})` : ''}<br>
  187. ${definition}<br>
  188. <small>Source: ${sourceList}</small>
  189. `;
  190. tooltip.style.display = 'block';
  191.  
  192. requestAnimationFrame(() => {
  193. adjustTooltipPosition(tooltip, event);
  194. tooltip.style.opacity = '1';
  195. tooltip.style.transform = 'translateY(0)';
  196. });
  197. }
  198. }
  199.  
  200. function removeTooltip() {
  201. const tooltip = document.getElementById('cogniread-tooltip');
  202. if (tooltip) {
  203. tooltip.style.opacity = '0';
  204. tooltip.style.transform = 'translateY(10px)';
  205. setTimeout(() => {
  206. if (tooltip.style.opacity === '0') {
  207. tooltip.style.display = 'none';
  208. }
  209. }, 200);
  210. }
  211. }
  212.  
  213. function adjustTooltipPosition(tooltip, event) {
  214. const tooltipRect = tooltip.getBoundingClientRect();
  215. let left = event.clientX + 10;
  216. let top = event.clientY + 10;
  217.  
  218. if ((left + tooltipRect.width) > window.innerWidth) {
  219. left = event.clientX - tooltipRect.width - 10;
  220. }
  221. if ((top + tooltipRect.height) > window.innerHeight) {
  222. top = event.clientY - tooltipRect.height - 10;
  223. }
  224.  
  225. tooltip.style.left = `${left}px`;
  226. tooltip.style.top = `${top}px`;
  227. }
  228.  
  229. const updateTooltipPositionDebounced = debounce((tooltip, event) => {
  230. adjustTooltipPosition(tooltip, event);
  231. }, 10);
  232.  
  233. function debounce(func, wait) {
  234. let timeout;
  235. return function executedFunction(...args) {
  236. const later = () => {
  237. clearTimeout(timeout);
  238. func(...args);
  239. };
  240. clearTimeout(timeout);
  241. timeout = setTimeout(later, wait);
  242. };
  243. }
  244.  
  245. // Inject CSS styles
  246. function addStyles() {
  247. const style = document.createElement('style');
  248. style.innerHTML = `
  249. .cogni-word b { color: #000080; }
  250. .cogni-highlight { background-color: yellow; cursor: pointer; }
  251. #cogniread-tooltip { font-family: Arial, sans-serif; }
  252. `;
  253. document.head.appendChild(style);
  254. }
  255.  
  256. // Enable CogniRead
  257. async function enableCogniRead() {
  258. console.log("Applying CogniRead...");
  259. await fetchWordLists();
  260. addStyles();
  261. traverseAndStyleText(document.body);
  262. initTooltip();
  263. console.log("CogniRead is now active.");
  264. }
  265.  
  266. // Toggle CogniRead
  267. function toggleCogniRead() {
  268. cogniEnabled = !cogniEnabled;
  269. if (cogniEnabled) {
  270. enableCogniRead();
  271. } else {
  272. location.reload();
  273. }
  274. }
  275.  
  276. // Set keyboard shortcut
  277. const isMacOS = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
  278. document.addEventListener('keydown', function(e) {
  279. if ((isMacOS && e.ctrlKey && e.key.toLowerCase() === 'b') || (!isMacOS && e.altKey && e.key.toLowerCase() === 'b')) {
  280. e.preventDefault();
  281. toggleCogniRead();
  282. }
  283. });
  284.  
  285. // Event delegation for tooltip display
  286. document.body.addEventListener('mouseover', function(e) {
  287. const target = e.target.closest('.cogni-highlight');
  288. if (target) {
  289. const word = target.getAttribute('data-word');
  290. displayTooltip(word, e);
  291. }
  292. });
  293.  
  294. document.body.addEventListener('mousemove', function(e) {
  295. const tooltip = document.getElementById('cogniread-tooltip');
  296. if (tooltip && tooltip.style.display !== 'none') {
  297. updateTooltipPositionDebounced(tooltip, e);
  298. }
  299. });
  300.  
  301. document.body.addEventListener('mouseout', function(e) {
  302. if (!e.relatedTarget || !e.relatedTarget.closest('.cogni-highlight')) {
  303. removeTooltip();
  304. }
  305. });
  306.  
  307. window.addEventListener('scroll', debounce(function() {
  308. const tooltip = document.getElementById('cogniread-tooltip');
  309. if (tooltip && tooltip.style.display !== 'none') {
  310. removeTooltip();
  311. }
  312. }, 100));
  313.  
  314. console.log("CogniRead script loaded. Use Ctrl+B (Mac) or Alt+B (Windows/Linux) to toggle.");
  315. })();