- // ==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);
-
- })();