Mathcord Reborn

Typeset equations in Discord messages.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Mathcord Reborn
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Typeset equations in Discord messages.
// @author       Till Hoffmann, hnOsmium0001
// @license      MIT
// @match        https://discordapp.com/*
// @match        https://discord.com/*
// @resource     katexCSS https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js
// @grant        GM_addStyle
// @grant        GM_getResourceText
// ==/UserScript==

(function () {
    'use strict';

    if (!renderMathInElement) throw "Katex did not load correctly!";

    /**
     * Evaluate whether an element has a certain class prefix.
    */
    function hasClassPrefix(element, prefix) {
        if (!element.getAttribute) return false;

        const classes = (element.getAttribute("class") || "").split();
        return classes.some(x => x.startsWith(prefix));
    }

    // Declare rendering options (see https://katex.org/docs/autorender.html#api for details)
    const options = {
        delimiters: [
            { left: "$$", right: "$$", display: true },
            { left: "\\(", right: "\\)", display: false },
            { left: "\\[", right: "\\]", display: true },
            // Needs to come last to prevent over-eager matching of delimiters
            { left: "$", right: "$", display: false },
        ],
    };

    // Align block LaTeX to the left for better viewing experience
    GM_addStyle(`
        .katex-html {
            text-align: left !important;
        }
    `);

    // We need to download the CSS, modify any relative urls to be absolute, and inject the CSS
    const pattern = /url\((.*?)\)/gi;
    const katexCSS = GM_getResourceText("katexCSS").replace(pattern, 'url(https://cdn.jsdelivr.net/npm/[email protected]/dist/$1)');
    GM_addStyle(katexCSS);

    class ChildrenSelector {
        constructor(elm) {
            this.elm = elm;
        }

        andThenTag(tag, alternativeElm) {
            if (!this.elm) {
                this.elm = alternativeElm;
                return this;
            }

            for (const child of this.elm.childNodes) {
                if (child.tagName === tag) {
                    this.elm = child;
                    return this;
                }
            }
            this.elm = alternativeElm;
            return this;
        }

        andThenClass(prefix, alternativeElm) {
            if (!this.elm) {
                this.elm = alternativeElm;
                return this;
            }

            for (const child of this.elm.childNodes) {
                if (hasClassPrefix(child, prefix)) {
                    this.elm = child;
                    return this;
                }
            }
            // Failed to find a matching children
            this.elm = alternativeElm;
            return this;
        }

        accept(successful, failed) {
            if (this.elm) {
                successful(this.elm);
            } else {
                failed();
            }
        }
    }

    function updateFor_chat(added) {
        const chatContent = new ChildrenSelector(added)
            .andThenClass("content")
            .andThenTag("MAIN")
            .elm;
        updateFor_chatContent(chatContent);
    }

    function updateFor_chatContent(added) {
        new ChildrenSelector(added)
            .andThenClass("messagesWrapper")
            .andThenClass("scroller")
            .andThenClass("scrollerContent")
            .andThenClass("scrollerInner")
            .accept(
                scroller => {
                  console.log("here")
                    for (const candidate of scroller.children) {
                        if (hasClassPrefix(candidate, "chat-messages")) {
                            renderMathInElement(candidate, options);
                        }
                    }
                },
                () => {
                    throw "Failed to find 'scrollerInner' element on content change (reloading cached meesages)";
                }
            );
    }

    // Monitor the document for changes and render math as necessary
    const observer = new MutationObserver(function (mutations, observer) {
        for (const mutation of mutations) {
            const target = mutation.target;
            // Respond to newly loaded messages
            if (hasClassPrefix(target, "scrollerInner")) {
                // Iterate over all messages added to the scroller and typeset them
                for (const added of mutation.addedNodes) {
                    if (added.tagName === "LI" && hasClassPrefix(added, "chat-messages")) {
                        renderMathInElement(added, options);
                    }
                }
            }
            // Respond to edited messages
            else if (hasClassPrefix(target, "contents") &&
                hasClassPrefix(target.parentNode, "message")) {
                for (const added of mutation.addedNodes) {
                    // Do not typeset the interactive edit container
                    if (added.tagName === "DIV" && !added.getAttribute("class")) {
                        continue;
                    }
                    // Hack to get around Discord's slight delay between confirm edit and edit displayed
                    setTimeout(_ => renderMathInElement(added, options), 1000);
                }
            }
            // Respond to reloading cached messages
            else if (hasClassPrefix(target, "message")) {
                new ChildrenSelector(target)
                    .andThenClass("contents")
                    .andThenClass("message-content")
                    .accept(
                        messageContent => {
                            renderMathInElement(messageContent, options);
                        },
                        () => {
                            throw "Failed, TODO report bug";
                        }
                    );
            }
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();