Mathcord

Typeset equations in Discord messages.

  1. // ==UserScript==
  2. // @name Mathcord
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description Typeset equations in Discord messages.
  6. // @author Till Hoffmann
  7. // @match https://discordapp.com/*
  8. // @resource katexCSS https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css
  9. // @require https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.js
  10. // @require https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/contrib/auto-render.min.js
  11. // @grant GM_addStyle
  12. // @grant GM_getResourceText
  13. // ==/UserScript==
  14.  
  15. /**
  16. * Evaluate whether an element has a certain class prefix.
  17. */
  18. function hasClassPrefix(element, prefix) {
  19. var classes = (element.getAttribute("class") || "").split();
  20. return classes.some(x => x.startsWith(prefix));
  21. }
  22.  
  23. (function() {
  24. 'use strict';
  25. // Declare rendering options (see https://katex.org/docs/autorender.html#api for details)
  26. const options = {
  27. delimiters: [
  28. {left: "$$", right: "$$", display: true},
  29. {left: "\\(", right: "\\)", display: false},
  30. {left: "\\[", right: "\\]", display: true},
  31. // Needs to come last to prevent over-eager matching of delimiters
  32. {left: "$", right: "$", display: false},
  33. ],
  34. };
  35.  
  36. // We need to download the CSS, modify any relative urls to be absolute, and inject the CSS
  37. var katexCSS = GM_getResourceText("katexCSS");
  38. var pattern = /url\((.*?)\)/gi;
  39. katexCSS = katexCSS.replace(pattern, 'url(https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/$1)');
  40. GM_addStyle(katexCSS);
  41.  
  42. // Monitor the document for changes and render math as necessary
  43. var config = { childList: true, subtree: true };
  44. var observer = new MutationObserver(function(mutations, observer) {
  45. for (let mutation of mutations) {
  46. var target = mutation.target;
  47. // Iterate over all messages added to the scroller and typeset them
  48. if (target.tagName == "DIV" && hasClassPrefix(target, "scroller")) {
  49. for (let added of mutation.addedNodes) {
  50. if (added.tagName == "DIV" && hasClassPrefix(added, "message")) {
  51. renderMathInElement(added, options);
  52. }
  53. }
  54. }
  55. // Respond to edited messages
  56. else if (target.tagName == "DIV" && hasClassPrefix(target, "container") &&
  57. hasClassPrefix(target.parentNode, "message")) {
  58. for (let added of mutation.addedNodes) {
  59. // Do not typeset the interactive edit container
  60. if (added.tagName == "DIV" && !added.getAttribute("class")) {
  61. continue;
  62. }
  63. setTimeout(_ => renderMathInElement(added, options), 1000);
  64. }
  65. }
  66. }
  67. });
  68. observer.observe(document.body, config);
  69. })();