NitroType Perfect Nitros with SFB Highlighting (QWERTY, DVORAK, COLEMAK)

Highlights the largest words, single-finger bigrams (SFBs), and custom words/phrases for QWERTY, DVORAK, or COLEMAK layouts. Adds custom color coding for hand-specific characters like 'b' and 'y'.

// ==UserScript==
// @name         NitroType Perfect Nitros with SFB Highlighting (QWERTY, DVORAK, COLEMAK)
// @namespace    https://greasyfork.org/users/1331131-tensorflow-dvorak
// @version      2.5.1
// @description  Highlights the largest words, single-finger bigrams (SFBs), and custom words/phrases for QWERTY, DVORAK, or COLEMAK layouts. Adds custom color coding for hand-specific characters like 'b' and 'y'.
// @author       Ray Adams/Nate Dogg, Modified by TensorFlow - Dvorak
// @match        https://www.nitrotype.com/race
// @match        https://www.nitrotype.com/race/*
// @run-at       document-end
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
    // CHANGE THIS to 'QWERTY', 'DVORAK' or 'COLEMAK'
    const keyboardLayout = 'QWERTY';

    const options = {
        highlightColor: '#1b1c25',
        wordHighlightColor: '#5b048a',
        singleFingerBigramColor: '#403dae',
        redForRightHand: 'red',
        blueForLeftHand: 'blue',
        intervalMs: 100
    };

    // Custom wordlist for highlighting (add words or phrases you aim to type differently here)
    const customWords = new Set(['number', "you're"]);

    // SFBs for different keyboard layouts (remove bigrams you don't want to work on)
    const SFBs = new Map([
        ['QWERTY', ['ed', 'de', 'fr', 'rf', 'gt', 'tg', 'bv', 'vb', 'ju', 'uj', 'ki', 'ik', 'nm', 'mn', 'nu', 'un']],
        ['DVORAK', ['pu', 'up', 'ui', 'iu', 'pi', 'ip', 'je', 'ej']],
        ['COLEMAK', ['']]
    ]);

    // Define different layouts.
    const layoutKeys = {
        QWERTY: {
            leftHand: 'qwertasdfgzxcvb',
            rightHand: 'yuiophjklmn',
            targetChars: ['b'] // 'b' is typed by either hand and will be color coded based on which hand should be used. Red = right Blue = Left.
        },
        DVORAK: {
            leftHand: 'aoeuqjkxiyp',
            rightHand: 'dhtsnfgcrlbmwvz',
            targetChars: ['x']
        },
        COLEMAK: {
            leftHand: 'qwfpbjluyarst',
            rightHand: 'neiohjkxvmzcdg',
            targetChars: ['b']
        }
    };

    const client = () => {
        const dashLetters = document.querySelector('.dash-letter');
        if (dashLetters) {
            clearInterval(intervalId);

            // Get all words from the race
            const wordList = [...document.getElementsByClassName('dash-word')].map(word => word.textContent.replace(/\s/g, ''));

            // Find the largest words
            const maxLength = Math.max(...wordList.map(word => word.length));
            const largestWords = wordList.filter(word => word.length === maxLength);

            // Highlight largest words and bigrams
            wordList.forEach((word, index) => {
                const wordElement = document.getElementsByClassName('dash-word')[index];

                // Highlight largest words
                if (largestWords.includes(word)) {
                    wordElement.style.backgroundColor = options.highlightColor;
                }

                // Highlight custom words/phrases
                highlightCustomWords(wordElement);

                // Highlight single-finger bigrams
                highlightSingleFingerBigrams(wordElement);

                // Highlight specific target chars if followed by the same hand's character
                highlightTargetCharsWithSameHand(wordElement, keyboardLayout);
            });
        }
    };

    const highlightCustomWords = (wordElement) => {
        const text = wordElement.textContent.toLowerCase();

        // Loop over each word or phrase in the custom wordlist
        customWords.forEach(word => {
            const startIndex = text.indexOf(word.toLowerCase());
            if (startIndex !== -1) {
                for (let i = 0; i < word.length; i++) {
                    wordElement.querySelector(`.dash-letter:nth-child(${startIndex + i + 1})`).style.color = options.wordHighlightColor;
                }
            }
        });
    };

    const highlightSingleFingerBigrams = (wordElement) => {
        const text = wordElement.textContent;

        // Loop over the text and highlight single-finger bigrams
        for (let i = 0; i < text.length - 1; i++) {
            const bigram = text[i] + text[i + 1];
            if (SFBs.get(keyboardLayout).includes(bigram)) {
                wordElement.querySelector(`.dash-letter:nth-child(${i + 1})`).style.color = options.singleFingerBigramColor;
                wordElement.querySelector(`.dash-letter:nth-child(${i + 2})`).style.color = options.singleFingerBigramColor;
                i++;
            }
        }
    };

    const highlightTargetCharsWithSameHand = (wordElement, layout) => {
        const { leftHand, rightHand, targetChars } = layoutKeys[layout];
        const text = wordElement.textContent.toLowerCase();

        // Check if target characters are followed by the same hand
        for (let i = 0; i < text.length - 1; i++) {
            const currentChar = text[i];
            const nextChar = text[i + 1];

            if (targetChars.includes(currentChar)) {
                if (leftHand.includes(currentChar) && leftHand.includes(nextChar)) {
                    // Highlight target char to suggest typing with the right hand
                    wordElement.querySelector(`.dash-letter:nth-child(${i + 1})`).style.color = options.redForRightHand;
                } else if (rightHand.includes(currentChar) && rightHand.includes(nextChar)) {
                    // Highlight target char to suggest typing with the left hand
                    wordElement.querySelector(`.dash-letter:nth-child(${i + 1})`).style.color = options.blueForLeftHand;
                }
            }
        }
    };

    const intervalId = setInterval(client, options.intervalMs);
})();