Transliteration of Georgian

Adds transliteration to all text nodes containing Georgian letters. Press a button in the bottom right corner of the page, or use a command in the Tampermonkey menu. Cyrillic transliteration is supported in addition to Latin.

目前為 2023-02-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Transliteration of Georgian
// @namespace    https://greasyfork.org/users/1029228
// @version      0.2
// @description  Adds transliteration to all text nodes containing Georgian letters. Press a button in the bottom right corner of the page, or use a command in the Tampermonkey menu. Cyrillic transliteration is supported in addition to Latin.
// @author       watxum
// @match        http*://*/*
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAbJJREFUOE+tk7GrQXEUx7+39CwoYcAiC4MMMiiDTcog3YRdyiClTK9elrsYFOIPMJmklJEiJZMkA1kMBrPkiu7rd8RzS1ev985y657z+/y+53t+h5Mk6QvAJ4AP/C7OAAROkiTx1eHxeAybzQar1aqEPTOA9KoiEAggk8kgkUgo6vo/wPF4RL1ex2q1QiQSQblcRjqdlimoVCqUY63d46GAJVg3qVQK3W4X7XabgM8txONxGAwGNBoNOeBwOECn02G/38NoNOJyucBisaBarcoAu90Ofr8f2WwW+XyeIKRAFEV4PB5MJhNotVpK3AuZAtaW3W6HSqXCer2G1+vFaDSC2+2+AZSmEAqFoNfrcTqdoFarqTQcDiMYDCKXy70HRKNRAkynU7hcLlyvV7q5WCwiFovdAL1eD61WC81m8yHm+R0w90ulEt26XC5hMpnQ6XSoJQLUajUMBgNy/h4+nw+FQgE8z9Ov7XaL2WwGs9lMHnAc92PicDgEGxEzSKPR0DSYafP5nL5K8TCRPRrmLHO/3+8jmUxCEIS36yVbpsVigc1mA6fTCYfD8fYwAFqmP63zNzNe4MPEsD5EAAAAAElFTkSuQmCC
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    function isInline(node) {
        if (node.nodeType !== Node.ELEMENT_NODE) {
            return null;
        }

        if (inlineTags.includes(node.tagName)) {
            return true;
        } else if (blockTags.includes(node.tagName)) {
            return false;
        }

        return null;
    }

    function doesBlockHaveMoreText(node) {
        for (let currentNode = node.parentNode; currentNode; currentNode = currentNode.parentNode) {
            if (currentNode.textContent.trim() !== node.textContent.trim()) {
                return true;
            }
            if (!isInline(currentNode)) {
                return false;
            }
        }
        return false;
    }

    function getAllTextNodes() {
        let treeWalker = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT);
        let nodes = [];
        let node;
        while ((node = treeWalker.nextNode())) {
            nodes.push(node);
        }
        return nodes.filter((node) => !wrapper.contains(node));
    }

    function initSettings() {
        if (!GM_getValue('target')) {
            GM_setValue('target', 'latin');
            GM_setValue('separator', 'auto');
            GM_setValue('showLink', 'always');
            GM_setValue('runOnLoad', false);
        }
    }

    function removeTransliteration() {
        [...document.querySelectorAll('.transliterationOfGeorgian-transliteration')]
            .forEach((el) => {
                el.remove();
            });
    }

    function convert() {
        removeTransliteration();
        getAllTextNodes()
            .filter((node) => node.textContent.match(georgianRegexp))
            .forEach((node) => {
                let newNode = document.createElement('span');
                newNode.className = 'transliterationOfGeorgian-transliteration';

                let prefix = '';
                let postfix = '';
                if (
                    GM_getValue('separator') === 'br'
                    || (
                        GM_getValue('separator') === 'auto'
                        && !doesBlockHaveMoreText(node)
                        // && node.parentNode.tagName !== 'A'
                    )
               ) {
                    prefix = document.createElement('br');
                } else {
                    prefix = ' [';
                    postfix = ']';
                }

                newNode.append(
                    prefix,
                    convertString(node.textContent.trim(), GM_getValue('target')),
                    postfix
                );

                // Add a space if the converted node ends with spaces.
                if (postfix && node.textContent.match(/\s+$/)) {
                    newNode.append(' ');
                }

                node.after(newNode);
            });
    }

    function toggleTransliteration() {
        if (document.querySelector('.transliterationOfGeorgian-transliteration')) {
            removeTransliteration();
        } else {
            convert();
        }
    }

    function saveSettings() {
        [...document.querySelectorAll('.transliterationOfGeorgian-setting')].forEach((input) => {
            if (input.type === 'checkbox' || input.checked) {
                GM_setValue(
                    input.name.split('-').slice(-1)[0],
                    input.type === 'checkbox' ? input.checked : input.value
                );
            }
        });
        if (document.querySelector('.transliterationOfGeorgian-transliteration')) {
            convert();
        }
    }

    let blockTags = [
        'BLOCKQUOTE', 'DD', 'DIV', 'DL', 'DT', 'FIGURE', 'FIGCAPTION', 'FORM', 'H1', 'H2', 'H3',
        'H4', 'H5', 'H6', 'HR', 'INPUT', 'LI', 'OL', 'P', 'PRE', 'TABLE', 'TBODY', 'TR', 'TH', 'TD',
        'UL',
    ];
    let inlineTags = [
        'A', 'ABBR', 'B', 'BIG', 'BR', 'CENTER', 'CITE', 'CODE', 'DEL', 'EM', 'FONT', 'I', 'IMG',
        'INS', 'KBD', 'Q', 'S', 'SAMP', 'SMALL', 'SPAN', 'STRIKE', 'STRONG', 'SUB', 'SUP', 'TIME',
        'TT', 'U', 'VAR',

        'OPTION', // 'OPTION' is a block element, but it doesn't support line breaks
    ];

    let conversions = {
        latin: {
            "ა": "a",
            "ბ": "b",
            "გ": "g",
            "დ": "d",
            "ე": "e",
            "ვ": "v",
            "ზ": "z",
            "თ": "t",
            "ი": "i",
            "კ": "k'",
            "ლ": "l",
            "მ": "m",
            "ნ": "n",
            "ო": "o",
            "პ": "p'",
            "ჟ": "zh",
            "რ": "r",
            "ს": "s",
            "ტ": "t'",
            "უ": "u",
            "ფ": "p",
            "ქ": "k",
            "ღ": "gh",
            "ყ": "q'",
            "შ": "sh",
            "ჩ": "ch",
            "ც": "ts",
            "ძ": "dz",
            "წ": "ts'",
            "ჭ": "ch'",
            "ხ": "kh",
            "ჯ": "j",
            "ჰ": "h",
        },
        cyrillic: {
            "ა": "а",
            "ბ": "б",
            "გ": "г",
            "დ": "д",
            "ე": "э",
            "ვ": "в",
            "ზ": "з",
            "თ": "т",
            "ი": "и",
            "კ": "к'",
            "ლ": "л",
            "მ": "м",
            "ნ": "н",
            "ო": "o",
            "პ": "п'",
            "ჟ": "ж",
            "რ": "р",
            "ს": "с",
            "ტ": "т'",
            "უ": "у",
            "ფ": "п",
            "ქ": "к",
            "ღ": "гх",
            "ყ": "q'",
            "შ": "ш",
            "ჩ": "ч",
            "ც": "ц",
            "ძ": "дз",
            "წ": "ц'",
            "ჭ": "ч'",
            "ხ": "х",
            "ჯ": "дж",
            "ჰ": "h",
        },
    };

    let convertString = (georgian, target) => (
        georgian
            .split('')
            .map((letter) => conversions[target][letter] || letter)
            .join('')
    );
    let georgianRegexp = new RegExp('[' + Object.keys(conversions.latin).join('') + ']');

    // Always add menu items, even if there is no Georgian text in sight - it might be loaded
    // asynchronically.
    GM_registerMenuCommand('Toggle transliteration', () => {
        toggleTransliteration();
    }, 'a');
    GM_registerMenuCommand('Toggle settings', () => {
        toggleSettings();
    }, 'a');

    if (!document.body.innerHTML.match(georgianRegexp)) return;

    initSettings();

    function toggleSettings() {
        settings.style.display = settings.style.display === '' ? 'none' : '';
    }

    let settings = document.createElement('div');
    settings.className = 'transliterationOfGeorgian-settings';
    toggleSettings();

    settings.innerHTML = `
<div class="transliterationOfGeorgian-settings-group">
  Target script<br>
  <input
    type="radio"
    id="transliterationOfGeorgian-setting-target-latin"
    value="latin"
    name="transliterationOfGeorgian-setting-target"
    class="transliterationOfGeorgian-setting"
  > <label for="transliterationOfGeorgian-setting-target-latin">Latin</label><br>
  <input
    type="radio"
    id="transliterationOfGeorgian-setting-target-cyrillic"
    value="cyrillic"
    name="transliterationOfGeorgian-setting-target"
    class="transliterationOfGeorgian-setting"
  > <label for="transliterationOfGeorgian-setting-target-cyrillic">Cyrillic (but Ჰ = h and ყ = q')</label><br>
  <div class="transliterationOfGeorgian-setting-helpText">Note that ejective consonants have ' (for example, ტ = t') while aspirated consonants have no ' (for example, თ = t). See <a href="https://www.georgian-alphabet.com/en/lesson10.php" target="_blank">the table</a> for their difference.</div>
</div>
<div class="transliterationOfGeorgian-settings-group">
  Transliteration separator<br>
  <input
    type="radio"
    id="transliterationOfGeorgian-setting-separator-br"
    value="br"
    name="transliterationOfGeorgian-setting-separator"
    class="transliterationOfGeorgian-setting"
  > <label for="transliterationOfGeorgian-setting-separator-br">Line break</label><br>
  <input
    type="radio"
    id="transliterationOfGeorgian-setting-separator-brackets"
    value="brackets"
    name="transliterationOfGeorgian-setting-separator"
    class="transliterationOfGeorgian-setting"
  > <label for="transliterationOfGeorgian-setting-separator-brackets">Brackets []</label><br>
  <input
    type="radio"
    id="transliterationOfGeorgian-setting-separator-auto"
    value="auto"
    name="transliterationOfGeorgian-setting-separator"
    class="transliterationOfGeorgian-setting"
  > <label for="transliterationOfGeorgian-setting-separator-auto">Line break for blocks of text, brackets for in-line elements</label><br>
</div>
<div class="transliterationOfGeorgian-settings-group">
  Show the "Toggle transliteration" link<br>
  <input
    type="radio"
    id="transliterationOfGeorgian-setting-showLink-always"
    value="always"
    name="transliterationOfGeorgian-setting-showLink"
    class="transliterationOfGeorgian-setting"
  > <label for="transliterationOfGeorgian-setting-showLink-always">On all sites with Georgian letters</label><br>
  <input
    type="radio"
    id="transliterationOfGeorgian-setting-showLink-dotGe"
    value="dotGe"
    name="transliterationOfGeorgian-setting-showLink"
    class="transliterationOfGeorgian-setting"
  > <label for="transliterationOfGeorgian-setting-showLink-dotGe">On .ge sites</label><br>
  <input
    type="radio"
    id="transliterationOfGeorgian-setting-showLink-never"
    value="never"
    name="transliterationOfGeorgian-setting-showLink"
    class="transliterationOfGeorgian-setting"
  > <label for="transliterationOfGeorgian-setting-showLink-never">Nowhere</label><br>
  <div class="transliterationOfGeorgian-setting-helpText">You can always use a command in the Tampermonkey menu.</div>
</div>
<div class="transliterationOfGeorgian-settings-group">
  <input
    type="checkbox"
    id="transliterationOfGeorgian-setting-runOnLoad"
    value="runOnLoad"
    name="transliterationOfGeorgian-setting-runOnLoad"
    class="transliterationOfGeorgian-setting"
  > <label for="transliterationOfGeorgian-setting-runOnLoad">Add transliteration on page load</label><br>
  <div class="transliterationOfGeorgian-setting-helpText">If the link is hidden, transliteration will not be added.</div>
</div>
`;

    let addLink = document.createElement('a');
    addLink.textContent = 'Toggle transliteration';
    addLink.className = 'transliterationOfGeorgian-link';
    addLink.onclick = toggleTransliteration;

    let settingsLink = document.createElement('a');
    settingsLink.textContent = '⚙️';
    settingsLink.className = 'transliterationOfGeorgian-link';
    settingsLink.onclick = toggleSettings;

    let links;
    if (
        GM_getValue('showLink') === 'always' ||
        (GM_getValue('showLink') === 'dotGe' && location.hostname.endsWith('.ge'))
    ) {
        links = document.createElement('div');
        links.className = 'transliterationOfGeorgian-links';
        links.append(addLink, settingsLink);
    }

    let wrapper = document.createElement('div');
    wrapper.id = 'transliterationOfGeorgian-wrapper';
    wrapper.append(settings);
    if (links) {
        wrapper.append(links);
    }
    document.body.append(wrapper);

    ['target', 'separator', 'showLink', 'runOnLoad'].forEach((setting) => {
        [...document.querySelectorAll(`[name="transliterationOfGeorgian-setting-${setting}"]`)]
            .forEach((input) => {
                if (
                    (input.type === 'radio' && GM_getValue(setting) === input.value)
                    || (input.type === 'checkbox' && GM_getValue(setting))
                ) {
                    input.checked = true;
                }
            });
    });

    [...document.querySelectorAll('.transliterationOfGeorgian-setting')].forEach((input) => {
        input.onclick = saveSettings;
    });

    if (GM_getValue('runOnLoad') && links) {
        convert();
    }

    GM_addStyle(`
#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper {
    all: revert;
    position: fixed;
    z-index: 1000;
    bottom: 0.5em;
    right: 0.5em;
    font-size: 14px;
    line-height: normal !important;
    font-family: sans-serif !important;
    text-align: left;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper * {
    all: revert;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-settings {
    width: 400px;
    color: #222;
    background-color: #f8f8f8;
    padding: 0.75em 1em;
    border: 1px solid #ccc;
    border-radius: 3px;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-settings-group {
    margin: 0.5em 0;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-settings-group:first-child {
    margin-top: 0;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-settings-group:last-child {
    margin-bottom: 0;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-setting-helpText {
    font-size: 85%;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-links {
    width: max-content;
    margin: 0 0 0 auto;
    border: 1px solid #ccc;
    background-color: #f4f4f4;
    opacity: 0.67;
    border-radius: 3px;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-links:hover {
    opacity: 1;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-settings + .transliterationOfGeorgian-links {
    margin-top: 0.5em;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper a {
    cursor: pointer;
    font-family: sans-serif !important;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-link {
    display: inline-block;
    padding: 0.25em 0.5em;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-link + .transliterationOfGeorgian-link {
    padding-left: 0;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-link {
    color: #666;
}

#transliterationOfGeorgian-wrapper#transliterationOfGeorgian-wrapper .transliterationOfGeorgian-link:hover {
    color: #222;
}
`);
})();