Advertisement
Guest User

Untitled

a guest
Sep 20th, 2024
57
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 11.12 KB | Source Code | 0 0
  1. // ==UserScript==
  2. // @name        Render Latex v3
  3. // @namespace   Violentmonkey Scripts
  4. // @match       https://mail.google.com/mail/*
  5. // @grant       GM_registerMenuCommand
  6. // @grant       GM_addElement
  7. // @grant       GM_setValue
  8. // @grant       GM_getValue
  9. // @grant       GM_setValues
  10. // @grant       GM_listValues
  11. // @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js
  12. // @version     3.0
  13. // @author      -
  14. // @description
  15. // ==/UserScript==
  16.  
  17. GM_addElement('link', {
  18.   rel: "stylesheet",
  19.   src: "https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.css"
  20. });
  21.  
  22. function escapeRegExpSpecialCharacters(regexString) {
  23.   return regexString.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
  24. }
  25.  
  26. function renderLatex() {
  27.   document.querySelectorAll("#\\:1 > .nH .aHU.hx > [role='list'] > [role='listitem'][aria-expanded='true']").forEach(message => {
  28.     let subportion = message.querySelector("[data-message-id]"); // need to select a subportion of [role='listitem'] to stop rendering latex in the textinput
  29.     if(subportion) {
  30.       message = subportion;
  31.     }
  32.  
  33.     if(GM_listValues().length < 2) {
  34.       GM_setValues({
  35.         "[;...;]": "off",
  36.         "\\(...\\)": "off",
  37.         "$...$": "off",
  38.         "\\begin{math}...\\end{math}": "off",
  39.         "[(;...;)]": "off",
  40.         "\\[...\\]": "off",
  41.         "$$...$$": "off",
  42.         "\\begin{displaymath}...\\end{displaymath}": "off",
  43.         "\\begin{equation}...\\end{equation}": "off"
  44.       });
  45.     }
  46.  
  47.     // Have to find "$$...$$" before "$...$". This moves it to the front of the array so that it isn't done first.
  48.     let delims = GM_listValues();
  49.     let tempVar = delims[0];
  50.     let doubleDollarLocation = delims.indexOf("$$...$$");
  51.     delims[0] = "$$...$$";
  52.     delims[doubleDollarLocation] = tempVar;
  53.  
  54.     for(delim of delims) {
  55.       if(delim !== "automatic" && GM_getValue(delim, "off") !== "off") {
  56.         let delimRegex = new RegExp(escapeRegExpSpecialCharacters(delim.split("...")[0]) + "(.+?)" + escapeRegExpSpecialCharacters(delim.split("...")[1]), "g");
  57.         message.innerHTML = message.innerHTML.replace(delimRegex, (match, p1) => {
  58.           p1 = p1.replaceAll("<wbr>", "");
  59.           let htmlEntityConversions = {
  60.             "&amp;": "&",
  61.             "&quot;": "\"",
  62.             "&apos;": "'",
  63.             "&lt;": "<",
  64.             "&gt;": ">"
  65.           }
  66.           for(let entity in htmlEntityConversions) {
  67.             p1 = p1.replaceAll(entity, htmlEntityConversions[entity]);
  68.           }
  69.  
  70.           return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: GM_getValue(delim, "off") === "display" });
  71.         });
  72.       }
  73.     }
  74.   });
  75. }
  76.  
  77. GM_registerMenuCommand('Render Latex', () => {
  78.   renderLatex();
  79. });
  80.  
  81. function waitForElement(queryString) {
  82.   let count = 0;
  83.   return new Promise((resolve, reject) => {
  84.     let findInterval = setInterval(() => {
  85.       let waitElement = document.querySelector(queryString);
  86.       if(waitElement) {
  87.         clearInterval(findInterval);
  88.         resolve(waitElement);
  89.       } else if(count > 20) {
  90.         clearInterval(findInterval);
  91.         reject(`Couldn't find waitElement: ${queryString}.`);
  92.      } else {
  93.        count += 1;
  94.      }
  95.    }, 100);
  96.  });
  97. }
  98.  
  99. /**
  100. * Creates a MutationObserver that will watch the element messagesDiv in order to tell when either an email is opened
  101. * or a reply in an email chain is expanded. If either of those things happen, renderLatex() will be called.
  102. * @param Element messagesDiv - the div we get from document.querySelector("#\\:1 > .nH")
  103. * @return MutationObserver - returns the MutationObserver that is watching messagesDiv so that we can call observe or disconnect on it.
  104. */
  105. function createMailObserver(messagesDiv) {
  106.  let mailObserver = new MutationObserver((mutationRecords, observerElement) => {
  107.    mutationRecords.forEach(mutationRecord => {
  108.      switch(mutationRecord.type) {
  109.        case "childList":
  110.          mutationRecord.addedNodes.forEach(addedNode => {
  111.            if(addedNode.tagName === "DIV" && addedNode.getAttribute("role") === "listitem") {
  112.              renderLatex();
  113.            }
  114.          });
  115.          break;
  116.        case "attributes":
  117.          if(mutationRecord.target.tagName === "DIV" && mutationRecord.target.getAttribute("role") === "listitem" && mutationRecord.attributeName === "aria-expanded") {
  118.            renderLatex();
  119.          }
  120.      }
  121.    });
  122.  });
  123.  
  124.  return mailObserver;
  125. }
  126.  
  127. /**
  128. * Creates a popup window that will let the user change settings about how this script will handle delimiters and whether or not the latex
  129. * will attempt to render automatically.
  130. * @param MutationObserver mailObserver - the observer that checks when mail is opened or replies are expanded.
  131. * @param messagesDiv - the div that mailObserver is watching so that we can reconnect it again
  132. * @return Element latexOptionsWindow - a div that represents the window that lets the user change script settings.
  133. */
  134. function createLatexOptionsWindow(mailObserver, messagesDiv) {
  135.  let latexOptionsWindow = document.createElement("DIV");
  136.  latexOptionsWindow.classList.add("latex-window");
  137.  
  138.  latexOptionsWindow.innerHTML = `
  139.    <h2 class="latex-window-header">Inline Delimiters</h2>
  140.    <div>
  141.      <span class="latex-delimiter">[;...;]</span>
  142.      <input data-display-type="inline" type="checkbox">
  143.    </div>
  144.    <div>
  145.      <span class="latex-delimiter">\\(...\\)</span>
  146.      <input data-display-type="inline" type="checkbox">
  147.    </div>
  148.    <div>
  149.      <span class="latex-delimiter">$...$</span>
  150.      <input data-display-type="inline" type="checkbox">
  151.    </div>
  152.    <div>
  153.      <span class="latex-delimiter">\\begin{math}...\\end{math}</span>
  154.      <input data-display-type="inline" type="checkbox">
  155.    </div>
  156.    <h2 class="latex-window-header">Display Delimiters</h2>
  157.    <div>
  158.      <span class="latex-delimiter">[(;...;)]</span>
  159.      <input type="checkbox">
  160.    </div>
  161.    <div>
  162.      <span class="latex-delimiter">\\[...\\]</span>
  163.      <input type="checkbox">
  164.    </div>
  165.    <div>
  166.      <span class="latex-delimiter">$$...$$</span>
  167.      <input type="checkbox">
  168.    </div>
  169.    <div>
  170.      <span class="latex-delimiter">\\begin{displaymath}...\\end{displaymath}</span>
  171.      <input type="checkbox">
  172.    </div>
  173.    <div>
  174.      <span class="latex-delimiter">\\begin{equation}...\\end{equation}</span>
  175.      <input type="checkbox">
  176.    </div>
  177.    <h4>Automatically Attempt to Render Latex</h4>
  178.    <input class="automatic-latex" type="checkbox">
  179.    <div>
  180.      <button class="latex-window-cancel" style="margin-left: auto;">Cancel</button>
  181.      <button class="latex-window-save" style="margin-left: 1em;">Save</button>
  182.    </div>
  183.  `;
  184.  
  185.  latexOptionsWindow.querySelector(".latex-window-cancel").addEventListener('click', () => {
  186.    latexOptionsWindow.style.display = "none";
  187.  });
  188.  
  189.  latexOptionsWindow.querySelector(".latex-window-save").addEventListener('click', () => {
  190.    latexOptionsWindow.querySelectorAll(".latex-delimiter").forEach(delimiter => {
  191.      let delimiterKey = delimiter.innerText;
  192.      let delimiterValue;
  193.      if(delimiter.nextElementSibling.checked) {
  194.        delimiterValue = delimiter.nextElementSibling.hasAttribute("data-display-type") ? "inline" : "display";
  195.      } else {
  196.        delimiterValue = "off";
  197.      }
  198.      GM_setValue(delimiterKey, delimiterValue);
  199.    });
  200.  
  201.    if(latexOptionsWindow.querySelector(".automatic-latex").checked) {
  202.      renderLatex();
  203.      mailObserver.observe(messagesDiv, { childList: true, subtree: true, attributes: true, attributeOldValue: true});
  204.      GM_setValue("automatic", true);
  205.    } else {
  206.      mailObserver.disconnect();
  207.      GM_setValue("automatic", false);
  208.    }
  209.    latexOptionsWindow.style.display = "none";
  210.  });
  211.  
  212.  document.body.prepend(latexOptionsWindow);
  213.  
  214.  return latexOptionsWindow;
  215. }
  216.  
  217. /**
  218. * Creates a button and adds it to the menu in the header. The button will show the latexOptionsWindow when the user clicks on it, and if
  219. * the user clicks on the button while shift is held, it will call renderLatex() to manually attempt to render latex in whatever mail is
  220. * currently open.
  221. * @param Element latexOptionsWindow - a div that represents the window that lets the user change script settings.
  222. */
  223. function createLatexOptionsButton(latexOptionsWindow) {
  224.  waitForElement("header [data-tooltip='Support']").then(supportButton => {
  225.    let latexButton = document.createElement("DIV");
  226.    latexButton.classList.add("zo");
  227.    latexButton.style.cursor = "pointer";
  228.    latexButton.style.backgroundImage = "url('https://www.latex-project.org/about/logos/latex-project-logo_288x288.svg')";
  229.     latexButton.style.backgroundSize = "100%";
  230.     latexButton.setAttribute("data-tooltip", "Latex");
  231.     let supportButtonStyles = getComputedStyle(supportButton);
  232.     latexButton.style.width = supportButtonStyles.width;
  233.     latexButton.style.height = supportButtonStyles.height;
  234.  
  235.     latexButton.addEventListener('click', (e) => {
  236.       if(e.button === 0 && e.shiftKey) {
  237.         renderLatex();
  238.       } else if(e.button === 0) {
  239.         latexOptionsWindow.querySelectorAll(".latex-delimiter").forEach(delimiter => {
  240.           if(GM_getValue(delimiter.innerText, "off") !== "off") {
  241.             delimiter.nextElementSibling.checked = true;
  242.           }
  243.         });
  244.  
  245.         if(GM_getValue("automatic", false)) {
  246.           latexOptionsWindow.querySelector(".automatic-latex").checked = true;
  247.         }
  248.  
  249.         if(latexOptionsWindow.style.display !== "flex") {
  250.           latexOptionsWindow.style.display = "flex";
  251.         } else {
  252.           latexOptionsWindow.style.display = "none";
  253.         }
  254.       }
  255.     });
  256.  
  257.     supportButton.parentElement.prepend(latexButton);
  258.   });
  259. }
  260.  
  261. function addStyles() {
  262.   let latexWindowStyle = `
  263.     .latex-window {
  264.       left: 50%;
  265.       top: 50%;
  266.       transform: translate(-50%, -50%);
  267.       position: absolute;
  268.       background-color: white;
  269.       z-index: 5000;
  270.       display: none;
  271.       flex-direction: column;
  272.       border: 2px solid #555555;
  273.       padding: 50px;
  274.       border-radius: 50px;
  275.     }
  276.  
  277.     .latex-window-header:first-child {
  278.       margin-top: 0;
  279.     }
  280.  
  281.     .latex-window-header {
  282.       margin-bottom: 0;
  283.     }
  284.  
  285.     .latex-window > div {
  286.       display: flex;
  287.     }
  288.  
  289.     .latex-window input {
  290.       margin-left: auto;
  291.     }
  292.  
  293.     .latex-window h4 {
  294.       margin-bottom: 0;
  295.     }
  296.  
  297.     .automatic-latex {
  298.       margin-bottom: 20px;
  299.     }
  300.   `;
  301.   document.head.prepend(Object.assign(document.createElement("STYLE"), { innerHTML: latexWindowStyle }));
  302. }
  303.  
  304. window.addEventListener('load', () => {
  305.   addStyles();
  306.  
  307.   waitForElement("#\\:1 > .nH").then(messagesDiv => {
  308.     let mailObserver = createMailObserver(messagesDiv)
  309.  
  310.     if(GM_listValues().length === 0) {
  311.       GM_setValue("automatic", false);
  312.     }
  313.     if(GM_getValue("automatic", false)) {
  314.       mailObserver.observe(messagesDiv, { childList: true, subtree: true, attributes: true, attributeOldValue: true});
  315.     }
  316.  
  317.     let latexOptionsWindow = createLatexOptionsWindow(mailObserver, messagesDiv);
  318.  
  319.     createLatexOptionsButton(latexOptionsWindow);
  320.   });
  321. });
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement