skip to Main Content

I have a userscript that includes a GM_registerMenuCommand('Render LaTeX', () => { (and so forth) that makes part of the userscript only run when manually triggered by selecting the ViolentMonkey addon from my browser bar, then clicking the button labeled "Render LaTeX".

I want to add a button to the webpage this userscript is set to match, which when clicked does the above. I’m just at a loss for how to do so. The only information even regarding buttons I’ve yet been able to find was here.

I’ve tried tinkering with the code provided in the second answer to the linked question. I was able to adjust the text and positioning, but I couldn’t figure out how to make the button do what I want or change its appearance to better fit into the website it’s going on.

// ==UserScript==
// @name        TeX Button
// @namespace   all
// @match     https://mail.google.com/mail/*
// @version     1
// @grant       none
// ==/UserScript==

(function(){
    'use strict'

  window.addEventListener('load', () => {
    addButton('TeX', selectReadFn)
    })

    function addButton(text, onclick, cssObj) {
        cssObj = cssObj || {position: 'absolute', bottom: '92.375%', left:'19.35%', 'z-index': 3}
        let button = document.createElement('button'), btnStyle = button.style
        document.body.appendChild(button)
        button.innerHTML = text
        button.onclick = onclick
        Object.keys(cssObj).forEach(key => btnStyle[key] = cssObj[key])
        return button
    }

    function selectReadFn() {
        [...document.getElementsByClassName('MN')].filter(isRead).forEach(element => element.click())
    }

    function isRead(element) {
        childs = element.parentElement.parentElement.parentElement.getElementsByClassName('G3')
        return ![...childs].some(e => e.innerText.search(/unread/i)!==-1)
    }

}())

Below is the code with the GM_registerMenuCommand I mentioned. I don’t know if this is actually needed for reference, but I figured it couldn’t hurt to share.

// ==UserScript==
// @name        LaTeX for Gmail v2.1
// @namespace   Violentmonkey Scripts
// @match       https://mail.google.com/mail/*
// @grant       GM_registerMenuCommand
// @grant       GM_addElement
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js
// @version     2.1
// @author      /u/MistralMireille & /u/LoganJFisher
// @description Adds support for TeXTheWorld delimiters to Gmail, and an register menu to activate rendering using traditional LaTeX delimiters
// ==/UserScript==

GM_addElement('link', {
  rel: "stylesheet",
  src: "https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.css"
});

function renderLatex() {
  document.querySelectorAll("#\:1 > .nH .aHU.hx > [role='list'] > [role='listitem'][aria-expanded='true']").forEach(message => {
let subportion = message.querySelector("[data-message-id]"); // need to select a subportion of [role='listitem'] to stop rendering latex in the textinput
if(subportion) {
  message = subportion;
}
message.innerHTML = message.innerHTML.replace(/[;(.+?);]/g, (match, p1) => {
  return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: false });
});
message.innerHTML = message.innerHTML.replace(/[(;(.+?);)]/g, (match, p1) => {
  return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: true });
});
  });
}

GM_registerMenuCommand('Render LaTeX', () => {
  document.querySelectorAll("#\:1 > .nH .aHU.hx > [role='list'] > [role='listitem'][aria-expanded='true']").forEach(message => {
    let subportion = message.querySelector("[data-message-id]"); // need to select a subportion of [role='listitem'] to stop rendering latex in the textinput
    if(subportion) {
      message = subportion;
    }
    message.innerHTML = message.innerHTML.replace(/\[(.+?)\]/g, (match, p1) => {
      return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: true });
    });
    message.innerHTML = message.innerHTML.replace(/$$(.+?)$$/g, (match, p1) => {
      return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: true });
    });
    //message.innerHTML = message.innerHTML.replace(/\begin{displaymath}(.+?)\end{displaymath}/g, (match, p1) => {
      //return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: true });
    //});
    //message.innerHTML = message.innerHTML.replace(/\begin{equation}(.+?)\end{equation}/g, (match, p1) => {
      //return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: true });
    //});
    message.innerHTML = message.innerHTML.replace(/\((.+?)\)/g, (match, p1) => {
      return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: false });
    });
    message.innerHTML = message.innerHTML.replace(/$(.+?)$/g, (match, p1) => {
      return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: false });
    });
    message.innerHTML = message.innerHTML.replace(/\begin{math}(.+?)\end{math}/g, (match, p1) => {
      return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: false });
    });
  });
});

function waitForElement(queryString) {
  let count = 0;
  return new Promise((resolve, reject) => {
let findInterval = setInterval(() => {
  let waitElement = document.querySelector(queryString);
  if(waitElement) {
    clearInterval(findInterval);
    resolve(waitElement);
  } else if(count > 20) {
    clearInterval(findInterval);
    reject(`Couldn't find waitElement: ${queryString}.`);
  } else {
    count += 1;
  }
}, 100);
  });
}

window.addEventListener('load', () => {
  waitForElement("#\:1 > .nH").then(messagesDiv => {
(new MutationObserver((mutationRecords, observerElement) => {
  mutationRecords.forEach(mutationRecord => {
    switch(mutationRecord.type) {
      case "childList":
        mutationRecord.addedNodes.forEach(addedNode => {
          console.log(addedNode);
          if(addedNode.tagName === "DIV" && addedNode.getAttribute("role") === "listitem") {
            renderLatex();
          }
        });
        break;
      case "attributes":
        if(mutationRecord.target.tagName === "DIV" && mutationRecord.target.getAttribute("role") === "listitem" && mutationRecord.attributeName === "aria-expanded") {
          renderLatex();
        }
    }
  });
})).observe(messagesDiv, { childList: true, subtree: true, attributes: true, attributeOldValue: true});
  });
});

2

Answers


  1. Chosen as BEST ANSWER

    Edited GTK's accepted answer

    // ==UserScript==
    // @name        LaTeX for Gmail v2.2
    // @namespace   Violentmonkey Scripts
    // @match       https://mail.google.com/mail/*
    // @grant       GM_registerMenuCommand
    // @grant       GM_addElement
    // @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js
    // @version     2.2
    // @author      Logan J Fisher & various contributors
    // @description Adds support for TeXTheWorld delimiters to Gmail, and a button to activate rendering using traditional LaTeX delimiters
    // @noframes
    // ==/UserScript==
    
    /* globals katex */
    
    GM_addElement('link', {
        rel: "stylesheet",
        src: "https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.css"
    });
    
    function renderLatex() {
        console.log('rendering latex...');
        const messages = document.querySelectorAll("#\:1 > .nH .aHU.hx > [role='list'] > [role='listitem'][aria-expanded='true']");
        messages.forEach(message => {
            let subportion = message.querySelector("[data-message-id]"); // need to select a subportion of [role='listitem'] to stop rendering latex in the textinput
            if(subportion) {
                message = subportion;
            }
            message.innerHTML = message.innerHTML.replace(/[;(.+?);]/g, (match, p1) => {
                return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: false });
            });
            message.innerHTML = message.innerHTML.replace(/[(;(.+?);)]/g, (match, p1) => {
                return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: true });
            });
        });
    }
    
    function ButtonRenderLaTeX() {
        console.log('rendering latex...');
        const messages = document.querySelectorAll("#\:1 > .nH .aHU.hx > [role='list'] > [role='listitem'][aria-expanded='true']");
        messages.forEach(message => {
            let subportion = message.querySelector("[data-message-id]"); // need to select a subportion of [role='listitem'] to stop rendering latex in the textinput
            if(subportion) {
                message = subportion;
            }
            message.innerHTML = message.innerHTML.replace(/\[(.+?)\]/g, (match, p1) => {
                return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: true });
            });
            message.innerHTML = message.innerHTML.replace(/$$(.+?)$$/g, (match, p1) => {
                return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: true });
            });
            message.innerHTML = message.innerHTML.replace(/\((.+?)\)/g, (match, p1) => {
                return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: false });
            });
            message.innerHTML = message.innerHTML.replace(/$(.+?)$/g, (match, p1) => {
                return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: false });
            });
            message.innerHTML = message.innerHTML.replace(/\begin{math}(.+?)\end{math}/g, (match, p1) => {
                return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: false });
            });
        });
    }
    
    function waitForElement(queryString) {
        let count = 0;
        return new Promise((resolve, reject) => {
            let findInterval = setInterval(() => {
                let waitElement = document.querySelector(queryString);
                if(waitElement) {
                    clearInterval(findInterval);
                    resolve(waitElement);
                } else if(count > 20) {
                    clearInterval(findInterval);
                    reject(`Couldn't find waitElement: ${queryString}.`);
                } else {
                    count += 1;
                }
            }, 100);
        });
    }
    
    window.addEventListener('load', () => {
        waitForElement("#\:1 > .nH").then(messagesDiv => {
            addButton(messagesDiv);
    
            (new MutationObserver((mutationRecords, observerElement) => {
                mutationRecords.forEach(mutationRecord => {
                    switch(mutationRecord.type) {
                        case "childList":
                            mutationRecord.addedNodes.forEach(addedNode => {
                                console.log(addedNode);
                                if(addedNode.tagName === "DIV" && addedNode.getAttribute("role") === "listitem") {
                                    renderLatex();
                                }
                            });
                            break;
                        case "attributes":
                            if(mutationRecord.target.tagName === "DIV" && mutationRecord.target.getAttribute("role") === "listitem" && mutationRecord.attributeName === "aria-expanded") {
                                renderLatex();
                            }
                    }
                });
            })).observe(messagesDiv, { childList: true, subtree: true, attributes: true, attributeOldValue: true});
        });
    });
    
    function addButton(parent) {
        GM_registerMenuCommand('Render LaTeX', ButtonRenderLaTeX);
        GM_addElement(parent, 'button', {
            id: 'LatexButton',
            textContent: 'RENDER LATEX'
        });
    
        parent.querySelector('#LatexButton').addEventListener('click', ButtonRenderLaTeX);
    }
    

  2. Here’s your script modified to add a button and a menuCommand that will both call renderLatex:

    // ==UserScript==
    // @name        LaTeX for Gmail v2.1
    // @namespace   Violentmonkey Scripts
    // @match       https://mail.google.com/mail/*
    // @grant       GM_registerMenuCommand
    // @grant       GM_addElement
    // @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js
    // @version     2.1
    // @author      /u/MistralMireille & /u/LoganJFisher
    // @description Adds support for TeXTheWorld delimiters to Gmail, and an register menu to activate rendering using traditional LaTeX delimiters
    // @noframes
    // ==/UserScript==
    
    /* globals katex */
    
    GM_addElement('link', {
        rel: "stylesheet",
        src: "https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.css"
    });
    
    function renderLatex() {
        console.log('rendering latex...');
        const messages = document.querySelectorAll("#\:1 > .nH .aHU.hx > [role='list'] > [role='listitem'][aria-expanded='true']");
        messages.forEach(message => {
            let subportion = message.querySelector("[data-message-id]"); // need to select a subportion of [role='listitem'] to stop rendering latex in the textinput
            if(subportion) {
                message = subportion;
            }
            message.innerHTML = message.innerHTML.replace(/[;(.+?);]/g, (match, p1) => {
                return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: false });
            });
            message.innerHTML = message.innerHTML.replace(/[(;(.+?);)]/g, (match, p1) => {
                return katex.renderToString(p1, { throwOnError: false, output: "mathml", displayMode: true });
            });
        });
    }
    
    function waitForElement(queryString) {
        let count = 0;
        return new Promise((resolve, reject) => {
            let findInterval = setInterval(() => {
                let waitElement = document.querySelector(queryString);
                if(waitElement) {
                    clearInterval(findInterval);
                    resolve(waitElement);
                } else if(count > 20) {
                    clearInterval(findInterval);
                    reject(`Couldn't find waitElement: ${queryString}.`);
                } else {
                    count += 1;
                }
            }, 100);
        });
    }
    
    window.addEventListener('load', () => {
        waitForElement("#\:1 > .nH").then(messagesDiv => {
            addButton(messagesDiv);
    
            (new MutationObserver((mutationRecords, observerElement) => {
                mutationRecords.forEach(mutationRecord => {
                    switch(mutationRecord.type) {
                        case "childList":
                            mutationRecord.addedNodes.forEach(addedNode => {
                                console.log(addedNode);
                                if(addedNode.tagName === "DIV" && addedNode.getAttribute("role") === "listitem") {
                                    renderLatex();
                                }
                            });
                            break;
                        case "attributes":
                            if(mutationRecord.target.tagName === "DIV" && mutationRecord.target.getAttribute("role") === "listitem" && mutationRecord.attributeName === "aria-expanded") {
                                renderLatex();
                            }
                    }
                });
            })).observe(messagesDiv, { childList: true, subtree: true, attributes: true, attributeOldValue: true});
        });
    });
    
    function addButton(parent) {
        GM_registerMenuCommand('Render LaTeX', renderLatex);
        GM_addElement(parent, 'button', {
            id: 'LatexButton',
            textContent: 'RENDER LATEX'
        });
    
        parent.querySelector('#LatexButton').addEventListener('click', renderLatex);
    }
    

    *You can easily add styles to this button using GM_addStyle(css).


    EDIT:

    add this to the script to fix the TrustedHTML error:

    if (window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy) {
        window.trustedTypes.createPolicy('default', {
            createHTML: string => string
        });
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search