LaTeX Unicode Shortcuts

Highlight text then press [ALT+X] to convert LaTeX commands to their unicode equivalent (ex. \pi → π)

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         LaTeX Unicode Shortcuts
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Highlight text then press [ALT+X] to convert LaTeX commands to their unicode equivalent (ex. \pi → π)
// @author       eyl327
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    var convert;

    var dictLoaded = false;

    /* source url for shortcut file */
    var dictionarySource = "https://raw.githubusercontent.com/eyl327/LaTeX-Gboard-Dictionary/master/dictionary.txt";

    /* fetch text file when requested */
    function loadAsset(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", url, false);
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
                if (xhr.status === 200 || xhr.status === 0) {
                    callback(xhr.responseText);
                }
            }
        }
        xhr.send();
    }

    /* on dictionary loaded callback */
    function loaded(response) {
        console.log("LaTeX Unicode Shortcuts has been loaded.");
        /* generate dictionary from text file */
        var dictArr = response.split("\n").slice(1);
        var dictionary = {};
        for (var i = 0, len = dictArr.length; i < len; ++i) {
            var kvp = dictArr[i].split("\t");
            dictionary[kvp[0]] = kvp[1];
        }
        /* conversion function */
        convert = function (text) {
            var result = text.replace(/{([A-Za-z0-9])}/g, '$1'); // {R} => R
            for (var key in dictionary) {
                var pattern = new RegExp(key.replace(/([[^$.|\\?*+(){}])/g, '\\$1') + "\\b", 'g'); // clean and escape key
                var replaced = result.replace(pattern, dictionary[key]);
                if (replaced.length < result.length) {
                    result = replaced;
                }
            }
            return result;
        };
        dictLoaded = true;
    }

    /* get caret position within input box */
    function getCaretPositionInputBox(el) {
        if ("selectionStart" in el && document.activeElement == el) {
            return {
                start: el.selectionStart,
                end: el.selectionEnd
            };
        }
        else if (el.createTextRange) {
            var sel = document.selection.createRange();
            if (sel.parentElement() === el) {
                var range = el.createTextRange();
                range.moveToBookmark(sel.getBookmark());
                for (var len = 0;
                    range.compareEndPoints("EndToStart", range) > 0;
                    range.moveEnd("character", -1)) {
                    len++;
                }
                range.setEndPoint("StartToStart", el.createTextRange());
                for (var pos = { start: 0, end: len };
                    range.compareEndPoints("EndToStart", range) > 0;
                    range.moveEnd("character", -1)) {
                    pos.start++;
                    pos.end++;
                }
                return pos;
            }
        }
        return -1;
    }

    /* set caret position within input box */
    function setCaretPosition(el, pos) {
        if (el.setSelectionRange) {
            el.focus();
            el.setSelectionRange(pos, pos);
        }
        else if (el.createTextRange) {
            var range = el.createTextRange();
            range.collapse(true);
            range.moveEnd('character', pos);
            range.moveStart('character', pos);
            range.select();
        }
    }

    function overwriteInputBoxText(activeEl, before, convertedText, after) {
        // overwrite text
        activeEl.value = before + convertedText + after;
        // set cursor to be at end of selection
        setCaretPosition(activeEl, before.length + convertedText.length);
    }

    function replaceConversionInElement(activeEl, fullText, start, end) {
        var textToConvert = fullText.substring(start, end);
        var before = fullText.substring(0, start);
        var after = fullText.substring(end, fullText.length);
        // convert selection
        var convertedText = convert(textToConvert);
        if ("value" in activeEl) {
            overwriteInputBoxText(activeEl, before, convertedText, after);
        }
    }

    /* convert hilighted text in active element */
    function convertSelectionInputBox(activeEl) {
        var caretRange = getCaretPositionInputBox(activeEl);
        var selStart = caretRange.start;
        var selEnd = caretRange.end;
        var fullText = activeEl.value;
        /* if selection is empty, find word at caret */
        if (selStart == selEnd) {
            // Find beginning and end of word
            var left = fullText.slice(0, selStart + 1).search(/\S+$/);
            var right = fullText.slice(selStart).search(/(\s|$)/);
            /* convert the word at the caret selection */
            replaceConversionInElement(activeEl, fullText, left, right + selStart)
        }
        /* else convert the selection */
        else {
            replaceConversionInElement(activeEl, fullText, selStart, selEnd);
        }
    }

    /* convert hilighted text in active element */
    function convertSelectionContentEditable(element) {
        var NodeTree = {
            // Used to find all DOM nodes in window.getSelection()
            getInnerNodes: function (anchor, focus) {
                var ancestor = NodeTree.lowestCommonAncestor(anchor, focus);
                var childList = NodeTree.findChildrenList(ancestor);
                var [i, j] = [childList.indexOf(anchor), childList.indexOf(focus)].sort();
                return childList.slice(i, j + 1);
            },
            getNodeChain: function (node) {
                var chain = [];
                chain.push(node);
                while (node.parentNode) {
                    node = node.parentNode;
                    chain.push(node);
                }
                return chain.reverse();
            },
            lowestCommonAncestor: function (anchor, focus) {
                var uChain = NodeTree.getNodeChain(anchor);
                var vChain = NodeTree.getNodeChain(focus);
                var i;
                for (i = 0; i < uChain.length; i++) {
                    if (uChain[i] !== vChain[i]) {
                        break
                    }
                }
                return uChain[i - 1]
            },
            findChildrenList: function (node) {
                var list = []
                var find = function (n) {
                    if (!n) {
                        return;
                    }
                    list.push(n);
                    for (var child of Array.from(n.childNodes || [])) {
                        find(child);
                    }
                }
                find(node);
                return list;
            }
        }

        var sel = element.ownerDocument.getSelection();

        var selAN = sel.anchorNode;
        var selFN = sel.focusNode;

        var nodesBetweenNodes = NodeTree.getInnerNodes(selAN, selFN);

        var startNode = nodesBetweenNodes[0];
        var endNode = nodesBetweenNodes[nodesBetweenNodes.length - 1];

        var selAO = sel.anchorOffset;
        var selFO = sel.focusOffset;

        var [startCursor, endCursor] = (startNode === selAN && selAO <= selFO) ? [selAO, selFO] : [selFO, selAO];

        var cursor;

        for (var node of nodesBetweenNodes) {
            if (node.nodeType === 3) { // 3 = text type
                var selStart = (node === nodesBetweenNodes[0]) ? startCursor : 0;
                var selEnd = (node === nodesBetweenNodes[nodesBetweenNodes.length - 1]) ? endCursor : node.nodeValue.length;
                var text = node.nodeValue;
                selEnd = Math.min(text.length, selEnd);

                var convertStart = selStart;
                var convertEnd = selEnd;

                // cursor is not a hilighted selection
                if (selStart == selEnd) {
                    // Find beginning and end of word
                    convertStart = text.slice(0, selStart + 1).search(/\S+$/);
                    convertEnd = text.slice(selEnd).search(/(\s|$)/) + selStart;
                }

                /* convert the word at the caret selection */
                var textToConvert = text.substring(convertStart, convertEnd);
                var before = text.substring(0, convertStart);
                var after = text.substring(convertEnd, text.length);
                var convertedText = convert(textToConvert);

                // replace in element
                var result = before + convertedText + after;
                cursor = Math.min(result.length, before.length + convertedText.length);
                node.nodeValue = result;
            }
        }
        sel.collapse(endNode, cursor)
    }

    /* detect ALT+X keyboard shortcut */
    function enableLaTeXShortcuts(event) {
        if (event.altKey && event.keyCode == 88) { // ALT+X
            // load dictionary when first pressed
            if (!dictLoaded) {
                loadAsset(dictionarySource, loaded);
            }
            // convert selection
            var activeEl = document.activeElement;
            var activeElTag = activeEl.tagName.toLowerCase();
            if (activeElTag == "textarea" || activeElTag == "input") {
                convertSelectionInputBox(activeEl);
            }
            else if (activeEl.contentEditable) {
                convertSelectionContentEditable(activeEl);
            }
        }
    }

    document.addEventListener('keydown', enableLaTeXShortcuts, true);

})();