CogniRead

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

// ==UserScript==
// @name         CogniRead
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  增强型阅读脚本,能够加粗部分单词,并高亮显示词汇表中的特定词汇,同时通过美观的悬浮框展示定义。可通过Alt+B(Windows/Linux)或Control+B(Mac)进行切换。
// @match        *://*/*
// @grant        none
// @license      MIT
// @author       codeboy 
// @icon         https://cdn.icon-icons.com/icons2/609/PNG/512/book-glasses_icon-icons.com_56355.png
// ==/UserScript==

(function () {
    'use strict';

    let cogniEnabled = false;
    let wordData = {};

    // Word list URLs
    const wordListSources = {
        'TOEFL': 'https://raw.githubusercontent.com/CodeBoy2006/english-wordlists/master/TOEFL_abridged.txt',
        'GRE': 'https://raw.githubusercontent.com/CodeBoy2006/english-wordlists/master/GRE_abridged.txt',
        'OALD8': 'https://raw.githubusercontent.com/CodeBoy2006/english-wordlists/master/OALD8_abridged_edited.txt',
        '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'
    };

    // Load word lists
    async function fetchWordLists() {
        for (let [listName, url] of Object.entries(wordListSources)) {
            try {
                const response = await fetch(url);
                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                const text = await response.text();
                wordData[listName] = processWordList(text);
                console.log(`Loaded ${listName} wordlist with ${Object.keys(wordData[listName]).length} words.`);
            } catch (error) {
                console.error(`Failed to load ${listName} wordlist:`, error);
            }
        }
    }

    // Process word list text
    function processWordList(text) {
        const lines = text.split('\n');
        const dictionary = {};
        lines.forEach(line => {
            const [word, ...rest] = line.split(/\s+/);
            if (word) {
                dictionary[word.toLowerCase()] = rest.join(' ');
            }
        });
        return dictionary;
    }

    // Common suffixes
    const suffixes = ['s', 'es', 'ed', 'ing', 'er', 'est', 'ly'];

    // Match word in lists
    function matchWordInLists(word) {
        word = word.toLowerCase();

        // Direct match
        for (let list in wordData) {
            if (wordData[list].hasOwnProperty(word)) {
                return { word, list, definition: wordData[list][word] };
            }
        }

        // Match after removing common suffixes
        for (let suffix of suffixes) {
            if (word.endsWith(suffix)) {
                let stem = word.slice(0, -suffix.length);
                for (let list in wordData) {
                    if (wordData[list].hasOwnProperty(stem)) {
                        return { word: stem, list, definition: wordData[list][stem] };
                    }
                }
            }
        }

        // Special cases
        if (word.endsWith('ies')) {
            let stem = word.slice(0, -3) + 'y';
            for (let list in wordData) {
                if (wordData[list].hasOwnProperty(stem)) {
                    return { word: stem, list, definition: wordData[list][stem] };
                }
            }
        }
        if (word.endsWith('ves')) {
            let stem = word.slice(0, -3) + 'f';
            for (let list in wordData) {
                if (wordData[list].hasOwnProperty(stem)) {
                    return { word: stem, list, definition: wordData[list][stem] };
                }
            }
        }

        return null;
    }

    function isWordInData(word) {
        return matchWordInLists(word) !== null;
    }

    // Style word
    function styleWord(word) {
        const match = word.match(/^([\w'-]+)([^\w'-]*)$/);
        if (!match) return word;

        const [, cleanWord, punctuation] = match;
        const wordLower = cleanWord.toLowerCase();
        const isHighlighted = isWordInData(cleanWord);

        let boldLength = Math.ceil(cleanWord.length / 2);
        let formattedWord = `<b>${cleanWord.slice(0, boldLength)}</b>${cleanWord.slice(boldLength)}`;

        if (isHighlighted) {
            formattedWord = `<span class="cogni-highlight" data-word="${wordLower}">${formattedWord}</span>`;
        } else {
            formattedWord = `<span class="cogni-word">${formattedWord}</span>`;
        }

        return formattedWord + punctuation;
    }

    // Handle text node
    function handleTextNode(textNode) {
        const words = textNode.textContent.split(/(\s+)/);
        const formattedText = words.map(word => word.trim() ? styleWord(word) : word).join('');
        const span = document.createElement('span');
        span.innerHTML = formattedText;
        textNode.replaceWith(span);
    }

    // Traverse and style text
    function traverseAndStyleText(node) {
        if (node.nodeType === Node.TEXT_NODE) {
            if (node.textContent.trim().length > 0) {
                handleTextNode(node);
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            if (!['SCRIPT', 'STYLE', 'TEXTAREA'].includes(node.tagName)) {
                Array.from(node.childNodes).forEach(traverseAndStyleText);
            }
        }
    }

    // Initialize tooltip
    function initTooltip() {
        let tooltip = document.createElement('div');
        tooltip.id = 'cogniread-tooltip';
        tooltip.style.cssText = `
            position: fixed;
            background: #ffffff;
            border: none;
            border-radius: 8px;
            padding: 12px 16px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
            display: none;
            z-index: 9999;
            max-width: 300px;
            font-size: 14px;
            line-height: 1.6;
            color: #333333;
            pointer-events: none;
            transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
            opacity: 0;
            transform: translateY(10px);
        `;
        document.body.appendChild(tooltip);

        return tooltip;
    }

    // Display tooltip
    function displayTooltip(word, event) {
        const tooltip = document.getElementById('cogniread-tooltip');
        if (!tooltip) return;

        const matchedWord = matchWordInLists(word);
        if (matchedWord) {
            const { word: matchedWordText, list: sourceList, definition } = matchedWord;

            tooltip.innerHTML = `
                <strong>${word}</strong> ${word !== matchedWordText ? `(${matchedWordText})` : ''}<br>
                ${definition}<br>
                <small>Source: ${sourceList}</small>
            `;
            tooltip.style.display = 'block';

            requestAnimationFrame(() => {
                adjustTooltipPosition(tooltip, event);
                tooltip.style.opacity = '1';
                tooltip.style.transform = 'translateY(0)';
            });
        }
    }

    function removeTooltip() {
        const tooltip = document.getElementById('cogniread-tooltip');
        if (tooltip) {
            tooltip.style.opacity = '0';
            tooltip.style.transform = 'translateY(10px)';
            setTimeout(() => {
                if (tooltip.style.opacity === '0') {
                    tooltip.style.display = 'none';
                }
            }, 200);
        }
    }

    function adjustTooltipPosition(tooltip, event) {
        const tooltipRect = tooltip.getBoundingClientRect();
        let left = event.clientX + 10;
        let top = event.clientY + 10;

        if ((left + tooltipRect.width) > window.innerWidth) {
            left = event.clientX - tooltipRect.width - 10;
        }
        if ((top + tooltipRect.height) > window.innerHeight) {
            top = event.clientY - tooltipRect.height - 10;
        }

        tooltip.style.left = `${left}px`;
        tooltip.style.top = `${top}px`;
    }

    const updateTooltipPositionDebounced = debounce((tooltip, event) => {
        adjustTooltipPosition(tooltip, event);
    }, 10);

    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // Inject CSS styles
    function addStyles() {
        const style = document.createElement('style');
        style.innerHTML = `
            .cogni-word b { color: #000080; }
            .cogni-highlight { background-color: yellow; cursor: pointer; }
            #cogniread-tooltip { font-family: Arial, sans-serif; }
        `;
        document.head.appendChild(style);
    }

    // Enable CogniRead
    async function enableCogniRead() {
        console.log("Applying CogniRead...");
        await fetchWordLists();
        addStyles();
        traverseAndStyleText(document.body);
        initTooltip();
        console.log("CogniRead is now active.");
    }

    // Toggle CogniRead
    function toggleCogniRead() {
        cogniEnabled = !cogniEnabled;
        if (cogniEnabled) {
            enableCogniRead();
        } else {
            location.reload();
        }
    }

    // Set keyboard shortcut
    const isMacOS = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
    document.addEventListener('keydown', function(e) {
        if ((isMacOS && e.ctrlKey && e.key.toLowerCase() === 'b') || (!isMacOS && e.altKey && e.key.toLowerCase() === 'b')) {
            e.preventDefault();
            toggleCogniRead();
        }
    });

    // Event delegation for tooltip display
    document.body.addEventListener('mouseover', function(e) {
        const target = e.target.closest('.cogni-highlight');
        if (target) {
            const word = target.getAttribute('data-word');
            displayTooltip(word, e);
        }
    });

    document.body.addEventListener('mousemove', function(e) {
        const tooltip = document.getElementById('cogniread-tooltip');
        if (tooltip && tooltip.style.display !== 'none') {
            updateTooltipPositionDebounced(tooltip, e);
        }
    });

    document.body.addEventListener('mouseout', function(e) {
        if (!e.relatedTarget || !e.relatedTarget.closest('.cogni-highlight')) {
            removeTooltip();
        }
    });

    window.addEventListener('scroll', debounce(function() {
        const tooltip = document.getElementById('cogniread-tooltip');
        if (tooltip && tooltip.style.display !== 'none') {
            removeTooltip();
        }
    }, 100));

    console.log("CogniRead script loaded. Use Ctrl+B (Mac) or Alt+B (Windows/Linux) to toggle.");
})();