MSPFA extras

Adds custom quality of life features to MSPFA.

当前为 2020-08-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         MSPFA extras
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Adds custom quality of life features to MSPFA.
// @author       seymour schlong
// @icon         https://raw.githubusercontent.com/GrantGryczan/MSPFA/master/www/images/ico.svg
// @icon64       https://raw.githubusercontent.com/GrantGryczan/MSPFA/master/www/images/ico.svg
// @match        https://mspfa.com/
// @match        https://mspfa.com/*/
// @match        https://mspfa.com/*/?*
// @match        https://mspfa.com/?s=*
// @match        https://mspfa.com/my/*
// @match        https://mspfa.com/random*
// @grant        none
// ==/UserScript==


(function() {
    'use strict';

    const currentVersion = "1.6";
    console.log(`MSPFA extras script v${currentVersion} by seymour schlong`);

    /**
    * https://github.com/GrantGryczan/MSPFA/projects/1?fullscreen=true
    * Github to-do completion list
    *
    * https://github.com/GrantGryczan/MSPFA/issues/26 - Dropdown menu                   - February 23rd, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/18 - MSPFA themes                    - February 23rd, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/32 - Adventure creation dates        - February 23rd, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/32 - User creation dates             - February 23rd, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/40 - Turn certain buttons into links - July 21st, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/41 - Word and character count        - July 21st, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/57 - Default spoiler values          - August 7th, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/62 - Buttonless spoilers             - August 7th, 2020
    * https://github.com/GrantGryczan/MSPFA/issues/52 - Hash URLs                       - August 8th, 2020
    *                                                 - Page drafts                     - August 8th, 2020
    *                                                 - Edit pages button               - August 8th, 2020
    *
    * Extension to-do... maybe...
    *
    * If trying to save a page and any other save button is not disabled, ask the user if they would rather Save All instead, or prompt to disable update notifications.
    */

    // A general function that allows for waiting until a certain element appears on the page.
    const pageLoad = (fn, length) => {
        const interval = setInterval(() => {
            if (fn()) clearInterval(interval);
        }, length ? length*1000 : 500);
    };

    // Saves the options data for the script.
    const saveData = (data) => {
        localStorage.mspfaextra = JSON.stringify(data);
    };

    // Encases an element within a link
    const addLink = (elm, url) => {
        const link = document.createElement('a');
        link.href = url;
        elm.parentNode.insertBefore(link, elm);
        link.appendChild(elm);
    };

    // Returns true if version 2 is newer
    const compareVer = (ver1, ver2) => {
        ver1 = ver1.split(/\./); // current version
        ver2 = ver2.split(/\./); // new version
        ver1.push(0);
        ver2.push(0);
        if (parseInt(ver2[0]) > parseInt(ver1[0])) { // 1.x.x
            return true;
        } else if (parseInt(ver2[1]) > parseInt(ver1[1])) { // x.1.x
            return true;
        } else if (parseInt(ver2[2]) > parseInt(ver1[2])) { // x.x.1
            return true;
        }
        return false;
    }

    // Easy br element
    const newBr = () => {
        return document.createElement('br');
    }

    const settings = {};
    const defaultSettings = {
        autospoiler: false,
        style: 0,
        styleURL: "",
        night: false,
        auto502: true,
        textFix: false,
        pixelFix: false,
        intro: false,
        autoUpdate: false,
        version: currentVersion,
        spoilerValues: {},
        drafts: {}
    }

    // Load any previous settings from localStorage
    if (localStorage.mspfaextra) {
        Object.assign(settings, JSON.parse(localStorage.mspfaextra));
    }

    // If any settings are undefined, re-set to their default state. (For older users when new things get stored)
    const defaultSettingsKeys = Object.keys(defaultSettings);
    for (let i = 0; i < defaultSettingsKeys.length; i++) {
        if (typeof settings[defaultSettingsKeys[i]] === "undefined") {
            settings[defaultSettingsKeys[i]] = defaultSettings[defaultSettingsKeys[i]];
        }
    }
    saveData(settings);

    // Update saved version to the version used in the script to prevent unnecessary notifications
    if (compareVer(settings.version, currentVersion)) {
        settings.version = currentVersion;
        saveData(settings);
    }

    // Functions to get/change data from the console
    window.MSPFAe = {
        getSettings: () => {
            return settings;
        },
        getSettingsString: () => {
            console.log(JSON.stringify(settings, null, 4));
        },
        changeSettings: (newSettings) => {
            console.log('Settings updated');
            console.log(settings);
            Object.assign(settings, newSettings);
            saveData(settings);
        }
    }

    const styleOptions = ["Standard", "Low Contrast", "Light", "Dark", "Felt", "Trickster", "Custom"];
    const styleUrls = ['', '/css/theme1.css', '/css/theme2.css', '/css/?s=36237', '/css/theme4.css', '/css/theme5.css'];

    // Dropdown menu
    const myLink = document.querySelector('nav a[href="/my/"]');
    if (myLink) {
        const dropDiv = document.createElement('div');
        dropDiv.className = 'dropdown';
        Object.assign(dropDiv.style, {
            position: 'relative',
            display: 'inline-block',
            backgroundColor: 'inherit'
        });

        const dropContent = document.createElement('div');
        dropContent.className = 'dropdown-content';
        Object.assign(dropContent.style, {
            display: 'none',
            backgroundColor: 'inherit',
            position: 'absolute',
            textAlign: 'left',
            minWidth: '100px',
            marginLeft: '-5px',
            padding: '2px',
            zIndex: '1',
            borderRadius: '0 0 5px 5px'
        });

        dropDiv.addEventListener('mouseenter', evt => {
            dropContent.style.display = 'block';
            dropContent.style.color = getComputedStyle(myLink).color;
        });
        dropDiv.addEventListener('mouseleave', evt => {
            dropContent.style.display = 'none';
        });

        myLink.parentNode.insertBefore(dropDiv, myLink);
        dropDiv.appendChild(myLink);
        dropDiv.appendChild(dropContent);

        const dLinks = [];
        dLinks[0] = [ 'Messages', 'My Adventures', 'Settings' ];
        dLinks[1] = [ '/my/messages/', '/my/stories/', '/my/settings/' ];

        for (let i = 0; i < dLinks[0].length; i++) {
            const newLink = document.createElement('a');
            newLink.textContent = dLinks[0][i];
            newLink.href = dLinks[1][i];
            dropContent.appendChild(newLink);
        }

        // Append "My Profile" to the dropdown list if you're signed in
        pageLoad(() => {
            if (window.MSPFA) {
                if (window.MSPFA.me.n) {
                    const newLink = document.createElement('a');
                    newLink.textContent = "My Profile";
                    newLink.href = `/user/?u=${window.MSPFA.me.i}`;
                    dropContent.appendChild(newLink);
                    return true;
                }
            }
        });
    }

    const hashSearch = location.href.replace(location.origin + location.pathname, '').replace(location.search, '');
    if (hashSearch !== '') {
        pageLoad(() => {
            const idElement = document.querySelector(hashSearch);
            if (idElement) {
                document.querySelector(hashSearch).scrollIntoView();
                return true;
            }
        }, 1);
    }

    // Error reloading
    window.addEventListener("load", () => {
        // Reload the page if 502 CloudFlare error page appears
        if (settings.auto502 && document.querySelector('.cf-error-overview')) {
            window.location.reload();
        }

        // Wait five seconds, then refresh the page
        if (document.body.textContent === "Your client is sending data to MSPFA too quickly. Wait a moment before continuing.") {
            setTimeout(() => {
                window.location.reload();
            }, 5000);
        }
    });

    // Message that shows when you first get the script
    const showIntroDialog = () => {
        const msg = window.MSPFA.parseBBCode('Hi! Thanks for installing this script!\n\nBe sure to check the [url=https://greasyfork.org/en/scripts/396798-mspfa-extras#additional-info]GreasyFork[/url] page to see a full list of features, and don\'t forget to check out your [url=https://mspfa.com/my/settings/#extraSettings]settings[/url] page to tweak things to how you want.\n\nIf you have any suggestions, or you find a bug, please be sure to let me know on Discord at [url=discord://discordapp.com/users/277928549866799125]@seymour schlong#3669[/url].\n\n[size=12]This dialog will only appear once. To view it again, click "View Script Message" at the bottom of the site.[/size]');
        window.MSPFA.dialog("MSPFA extras message", msg, ["Okay"]);
    }

    // Check for updates by comparing currentVersion to text data from an adventure that has update text and info
    const checkForUpdates = (evt) => {
        window.MSPFA.request(0, {
            do: "story",
            s: "36596"
        }, story => {
            if (typeof story !== "undefined") {
                const ver = settings.version.split(/\./);
                const newVer = story.p[1].c.split(/\./);
                // compare versions
                if (compareVer(settings.version, story.p[1].c) || (evt && evt.type === 'click')) {
                    const msg = window.MSPFA.parseBBCode(story.p[1].b);
                    settings.version = story.p[1].c;
                    saveData(settings);
                    window.MSPFA.dialog(`MSPFA extras update! (${story.p[1].c})`, msg, ["Opt-out", "Dismiss", "Update"], (output, form) => {
                        if (output === "Update") {
                            window.open('https://greasyfork.org/en/scripts/396798-mspfa-extras', '_blank').focus();
                        } else if (output === "Opt-out") {
                            settings.autoUpdate = false;
                            saveData(settings);
                        }
                    });
                }
            }
        });
    };


    // Check for updates and show intro dialog if needed
    pageLoad(() => {
        if (window.MSPFA) {
            if (settings.autoUpdate) {
                checkForUpdates();
            }

            if (!settings.intro) {
                showIntroDialog();
                settings.intro = true;
                saveData(settings);
            }
            return true;
        }
    });

    const linkColour = document.createElement('a');
    linkColour.href = "/";
    linkColour.id = "linkColour";
    document.body.appendChild(linkColour);
    const details = document.querySelector('#details');
    // Add 'link' at the bottom to show the intro dialog again
    const introLink = document.createElement('a');
    introLink.textContent = 'View Script Message';
    introLink.style = 'cursor: pointer; text-decoration: underline;';
    introLink.style.color = getComputedStyle(linkColour).color;
    introLink.className = 'intro-link';
    introLink.addEventListener('click', showIntroDialog);
    details.appendChild(introLink);

    // vbar!!!!
    const vbar = document.createElement('span');
    Object.assign(vbar, {className: 'vbar', style: 'padding: 0 5px', textContent: '|'});
    details.appendChild(vbar);

    // Add 'link' at the bottom to show the update dialog again
    const updateLink = document.createElement('a');
    updateLink.textContent = 'View Update';
    updateLink.style = 'cursor: pointer; text-decoration: underline;';
    updateLink.style.color = getComputedStyle(linkColour).color;
    updateLink.className = 'intro-link';
    updateLink.addEventListener('click', checkForUpdates);
    details.appendChild(updateLink);

    // vbar 2!!!!
    const vbar2 = document.createElement('span');
    Object.assign(vbar2, {className: 'vbar', style: 'padding: 0 5px', textContent: '|'});
    details.appendChild(vbar2);

    // if you really enjoy the script and has some extra moneys 🥺
    const donateLink = document.createElement('a');
    donateLink.textContent = 'Donate';
    donateLink.href = 'https://ko-fi.com/ironbean';
    donateLink.target = "blank";
    details.appendChild(donateLink);

    // Theme stuff
    const theme = document.createElement('link');
    Object.assign(theme, { id: 'theme', type: 'text/css', rel: 'stylesheet' });
    const updateTheme = (src) => {
        theme.href = src;
        setTimeout(() => {
            introLink.style.color = getComputedStyle(linkColour).color;
            updateLink.style.color = getComputedStyle(linkColour).color;
        }, 1500);
    }
    if (!document.querySelector('#theme') && !/^\/css\/|^\/js\//.test(location.pathname)) {
        document.querySelector('head').appendChild(theme);
        if (settings.night) {
            updateTheme('/css/?s=36237');
        } else {
            updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
        }
    }

    // Dropdown menu and pixelated scaling
    const dropStyle = document.createElement('style');
    const pixelFixText = 'img, .mspfalogo, .major, .arrow, #flashytitle, .heart, .fav, .notify, .edit, .rss, input, #loading { image-rendering: pixelated !important; }';
    const dropStyleText = `#notification { z-index: 2; } .dropdown-content a { color: inherit; padding: 2px; text-decoration: underline; display: block;}`;
    if (!document.querySelector('#dropdown-style')) {
        dropStyle.id = 'dropdown-style';
        dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');
        //dropdownStyle.textContent = '#notification {    z-index: 2;}.dropdown:hover .dropdown-content {	display: block;}.dropdown {    position: relative;    display: inline-block;    background-color: inherit;}.dropdown-content {    display: none;    position: absolute;    text-align: left;    background-color: inherit;    min-width: 100px;    margin-left: -5px;    padding: 2px;    z-index: 1;    border-radius: 0 0 5px 5px;}.dropdown-content a {    color: #fffa36;    padding: 2px 2px;    text-decoration: underline;    display: block;}';

        document.querySelector('head').appendChild(dropStyle);
    }

    // Remove the current theme if the adventure has CSS (to prevent conflicts);
    pageLoad(() => {
        if (window.MSPFA) {
            if (window.MSPFA.story && window.MSPFA.story.y && window.MSPFA.story.y.length > 0) {
                updateTheme('');
            }
            return true;
        }
    });

    // Enabling night mode.
    document.querySelector('footer .mspfalogo').addEventListener('click', evt => {
        if (evt.button === 0) {
            settings.night = !settings.night;
            saveData(settings);

            dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '') + '';
            dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '') + ' *{transition:1s}';

            if (settings.night) {
                updateTheme('/css/?s=36237');
            } else {
                updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
            }

            setTimeout(() => {
                dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');
            }, 1000);

            console.log(`Night mode turned ${settings.night ? 'on' : 'off'}.`);
        }
    });

    if (location.pathname === "/" || location.pathname === "/preview/") {
        // Automatic spoiler opening
        if (settings.autospoiler) {
            window.MSPFA.slide.push((p) => {
                document.querySelectorAll('#slide .spoiler:not(.open) > div:first-child > input').forEach(sb => sb.click());
            });
        }

        if (location.search) {
            // Show creation date
            pageLoad(() => {
                if (document.querySelector('#infobox tr td:nth-child(2)')) {
                    document.querySelector('#infobox tr td:nth-child(2)').appendChild(document.createTextNode('Creation date: ' + new Date(window.MSPFA.story.d).toString().split(' ').splice(1, 3).join(' ')));
                    return true;
                }
            });

            // Hash scrolling and opening infobox or commmentbox
            if (['#infobox', '#commentbox', '#latestpages'].indexOf(hashSearch) !== -1) {
                pageLoad(() => {
                    if (document.querySelector(hashSearch)) {
                        if (hashSearch === '#infobox') {
                            document.querySelector('input[data-open="Show Adventure Info"]').click();
                        } else if (hashSearch === '#commentbox') {
                            document.querySelector('input[data-open="Show Comments"]').click();
                        } else if (hashSearch === '#latestpages') {
                            document.querySelector('input[data-open="Show Adventure Info"]').click();
                            document.querySelector('input[data-open="Show Latest Pages"]').click();
                        }
                        return true;
                    }
                });
            }

            // Attempt to fix text errors
            if (settings.textFix) {
                pageLoad(() => {
                    if (window.MSPFA.story && window.MSPFA.story.p) {
                        // russian/bulgarian is not possible =(
                        const currentPage = parseInt(/^\?s(?:.*?)&p=([\d]*)$/.exec(location.search)[1]);
                        const library = [
                            ["&acirc;��", "'"],
                            ["&Atilde;�", "Ñ"],
                            ["&Atilde;&plusmn;", "ñ"],
                            ["&Atilde;&sup3;", "ó"],
                            ["&Atilde;&iexcl;", "á"],
                            ["&Atilde;&shy;", "í"],
                            ["&Atilde;&ordm;", "ú"],
                            ["&Atilde;&copy;", "é"],
                            ["&Acirc;&iexcl;", "¡"],
                            ["&Acirc;&iquest;", "¿"],
                            ["N&Acirc;&ordm;", "#"]
                        ];
                        // https://mspfa.com/?s=5280&p=51 -- unknown error

                        const replaceTerms = (p) => {
                            library.forEach(term => {
                                if (window.MSPFA.story.p[p]) {
                                    window.MSPFA.story.p[p].c = window.MSPFA.story.p[p].c.replace(new RegExp(term[0], 'g'), term[1]);
                                    window.MSPFA.story.p[p].b = window.MSPFA.story.p[p].b.replace(new RegExp(term[0], 'g'), term[1]);
                                }
                            });
                        };

                        replaceTerms(currentPage-1);

                        window.MSPFA.slide.push(p => {
                            replaceTerms(p);
                            replaceTerms(p-2);
                        });
                        window.MSPFA.page(currentPage);
                        return true;
                    }
                });
            }

            // Turn buttons into links
            const pageButton = document.createElement('button');
            const pageLink = document.createElement('a');
            const searchContent = location.search.split('&p=');
            pageLink.href = `/my/stories/pages/${searchContent[0]}#p${searchContent[1].split('#')[0]}`;
            pageButton.className = 'pages edit major';
            pageButton.type = 'button';
            pageButton.title = 'Edit Pages';
            pageButton.style.marginRight = '10px';
            pageButton.style.backgroundImage = 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAL1JREFUOI1jYKAQMCJzugoL/+NSePPhQ4a569YxoouzoAuU9vUx3AoLY1BbtYqBgYGB4d21a8jS/9ENwTBg/ezZDNpQGh2oy8szJAcFoRiCYUBgairDrd27Gdz9/Bh+vH2LInfn+nUMQ3G6YOemTRiKsQEMA9z9/Bie7N7NYG9tjaGYKBfs3LSJQZuBgeHg0aPkucDe2prhDSUuOHj06Ih3wdw5cxgYZGQYGObMId0FNx8+JKhBQlGRKIOJBgBpTFVYzRG//QAAAABJRU5ErkJggg==")';
            pageLink.appendChild(pageButton);

            pageLoad(() => {
                const infoButton = document.querySelector('.edit.major');
                if (infoButton) {
                    pageLoad(() => {
                        if (window.MSPFA.me.i) {
                            infoButton.title = "Edit Info";
                            infoButton.parentNode.insertBefore(pageLink, infoButton);
                            addLink(infoButton, `/my/stories/info/${searchContent[0]}`);
                            pageButton.style.display = document.querySelector('.edit.major:not(.pages)').style.display;
                            return true;
                        }
                    });
                    addLink(document.querySelector('.rss.major'), `/rss/${searchContent[0]}`);
                    return true;
                }
            });
            window.addEventListener('load', evt => {
                const favButton = document.querySelector('.fav');
            });

            // Add "Reply" button to comment gear
            document.body.addEventListener('click', evt => {
                if (evt.toElement.classList.contains('gear')) {
                    const userID = evt.path[2].classList[2].replace('u', '');
                    const reportButton = document.querySelector('#dialog button[data-value="Report"]');
                    const replyButton = document.createElement('button');
                    replyButton.classList.add('major');
                    replyButton.type = 'submit';
                    replyButton.setAttribute('data-value', 'Reply');
                    replyButton.textContent = 'Reply';
                    replyButton.style = 'margin-right: 9px';
                    reportButton.parentNode.insertBefore(replyButton, reportButton);

                    replyButton.addEventListener('click', evt => {
                        document.querySelector('#dialog button[data-value="Cancel"]').click();
                        const commentBox = document.querySelector('#commentbox textarea');
                        commentBox.value = `[user]${userID}[/user], ${commentBox.value}`;
                        commentBox.focus();
                        // Weird bug where if you have JS console open it opens debugger?
                    });
                } else return;
            });/**/
        }
    }
    else if (location.pathname === "/my/settings/") { // Custom settings
        const saveBtn = document.querySelector('#savesettings');

        const table = document.querySelector("#editsettings tbody");
        let saveTr = table.querySelectorAll("tr");
        saveTr = saveTr[saveTr.length - 1];

        const headerTr = document.createElement('tr');
        const header = document.createElement('th');
        Object.assign(header, { id: 'extraSettings', textContent: 'Extra Settings' });
        headerTr.appendChild(header);

        const moreTr = document.createElement('tr');
        const more = document.createElement('td');
        more.textContent = "* This only applies to a select few older adventures that have had their text corrupted. Some punctuation is fixed, as well as regular characters with accents. Currently only some spanish/french is fixable. Russian/Bulgarian is not possible.";
        moreTr.appendChild(more);

        const settingsTr = document.createElement('tr');
        const localMsg = document.createElement('span');
        const settingsTd = document.createElement('td');
        localMsg.innerHTML = "Because this is an extension, any data saved is only <b>locally</b> on this device.<br>Don't forget to <b>save</b> when you've finished making changes!";
        const plusTable = document.createElement('table');
        const plusTbody = document.createElement('tbody');
        plusTable.appendChild(plusTbody);
        settingsTd.appendChild(localMsg);
        settingsTd.appendChild(newBr());
        settingsTd.appendChild(newBr());
        settingsTd.appendChild(plusTable);
        settingsTr.appendChild(settingsTd);

        plusTable.style = "text-align: center;";

        // Create checkbox (soooo much better)
        const createCheckbox = (text, checked) => {
            const optionTr = plusTbody.insertRow(plusTbody.childNodes.length);
            const optionTextTd = optionTr.insertCell(0);
            const optionInputTd = optionTr.insertCell(1);
            const optionInput = document.createElement('input');
            optionInputTd.appendChild(optionInput);

            optionTextTd.textContent = text;
            optionInput.type = "checkbox";
            optionInput.checked = checked;

            return optionInput;
        }

        const spoilerInput = createCheckbox("Automatically open spoilers:", settings.autospoiler);
        const errorInput = createCheckbox("Automatically reload Cloudflare 502 error pages:", settings.auto502);
        const updateInput = createCheckbox("Automatically check for updates:", settings.autoUpdate);
        const pixelFixInput = createCheckbox("Change pixel scaling to nearest neighbour:", settings.pixelFix);
        const textFixInput = createCheckbox("Attempt to fix text errors (experimental)*:", settings.textFix);

        const cssTr = plusTbody.insertRow(plusTbody.childNodes.length);
        const cssTextTd = cssTr.insertCell(0);
        const cssSelectTd = cssTr.insertCell(1);
        const cssSelect = document.createElement('select');
        cssSelectTd.appendChild(cssSelect);

        cssTextTd.textContent = "Change style:";

        const customTr = plusTbody.insertRow(plusTbody.childNodes.length);
        const customTextTd = customTr.insertCell(0);
        const customCssTd = customTr.insertCell(1);
        const customCssInput = document.createElement('input');
        customCssTd.appendChild(customCssInput);

        customTextTd.textContent = "Custom CSS URL:";
        customCssInput.style.width = "99px";
        customCssInput.value = settings.styleURL;

        styleOptions.forEach(o => cssSelect.appendChild(new Option(o, o)));

        saveTr.parentNode.insertBefore(headerTr, saveTr);
        saveTr.parentNode.insertBefore(settingsTr, saveTr);
        saveTr.parentNode.insertBefore(moreTr, saveTr);
        cssSelect.selectedIndex = settings.style;

        const buttonSpan = document.createElement('span');
        const draftButton = document.createElement('input');
        const spoilerButton = document.createElement('input');
        draftButton.style = 'margin: 0 9px;';
        draftButton.value = 'Clear Drafts';
        draftButton.className = 'major';
        draftButton.type = 'button';
        spoilerButton.value = 'Clear Spoiler Values';
        spoilerButton.className = 'major';
        spoilerButton.type = 'button';
        buttonSpan.appendChild(draftButton);
        buttonSpan.appendChild(spoilerButton);
        settingsTd.appendChild(buttonSpan);

        draftButton.addEventListener('click', () => {
            window.MSPFA.dialog('Delete all Drafts?', window.MSPFA.parseBBCode(`Doing this will delete all drafts saved for [b]${Object.keys(settings.drafts).length}[/b] adventure(s).\nAre you sure? This action is irreversible.`), ["Yes", "No"], (output, form) => {
                if (output === "Yes") {
                    setTimeout(() => {
                        window.MSPFA.dialog('Delete all Drafts?', document.createTextNode('Are you really sure?'), ["No", "Yes"], (output, form) => {
                            if (output === "Yes") {
                                settings.drafts = {};
                                saveData(settings);
                            }
                        });
                    }, 1);
                }
            });
        });

        spoilerButton.addEventListener('click', () => {
            window.MSPFA.dialog('Delete all Spoiler Values?', window.MSPFA.parseBBCode(`Doing this will delete all spoiler values saved for [b]${Object.keys(settings.spoilerValues).length}[/b] adventure(s).\nAre you sure? This action is irreversible.`), ["Yes", "No"], (output, form) => {
                if (output === "Yes") {
                    settings.drafts = {};
                    saveData(settings);
                }
            });
        });

        // Add event listeners
        plusTbody.querySelectorAll('input, select').forEach(elm => {
            elm.addEventListener("change", () => {
                saveBtn.disabled = false;
            });
        });

        saveBtn.addEventListener('mouseup', () => {
            settings.autospoiler = spoilerInput.checked;
            settings.style = cssSelect.selectedIndex;
            settings.styleURL = customCssInput.value;
            settings.auto502 = errorInput.checked;
            settings.textFix = textFixInput.checked;
            settings.pixelFix = pixelFixInput.checked;
            settings.autoUpdate = updateInput.checked;
            settings.night = false;
            console.log(settings);
            saveData(settings);

            updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);

            dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');

            dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '') + ' *{transition:1s}';
            setTimeout(() => {
                dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');
            }, 1000);
        });
    }
    else if (location.pathname === "/my/messages/") { // New buttons
        const btnStyle = "margin: 10px 5px;";

        // Select all read messages button.
        const selRead = document.createElement('input');
        selRead.style = btnStyle;
        selRead.value = "Select Read";
        selRead.id = "selectread";
        selRead.classList.add("major");
        selRead.type = "button";

        // On click, select all messages with the style attribute indicating it as read.
        selRead.addEventListener('mouseup', () => {
            document.querySelectorAll('td[style="border-left: 8px solid rgb(221, 221, 221);"] > input').forEach((m) => m.click());
        });

        // Select duplicate message (multiple update notifications).
        const selDupe = document.createElement('input');
        selDupe.style = btnStyle;
        selDupe.value = "Select Same";
        selDupe.id = "selectdupe";
        selDupe.classList.add("major");
        selDupe.type = "button";

        selDupe.addEventListener('mouseup', evt => {
            const temp = document.querySelectorAll('#messages > tr');
            const msgs = [];
            for (let i = temp.length - 1; i >= 0; i--) {
                msgs.push(temp[i]);
            }
            const titles = [];
            msgs.forEach((msg) => {
                let title = msg.querySelector('a.major').textContent;
                if (/^New update: /.test(title)) { // Select only adventure updates
                    if (titles.indexOf(title) === -1) {
                        if (msg.querySelector('td').style.cssText !== "border-left: 8px solid rgb(221, 221, 221);") {
                            titles.push(title);
                        }
                    } else {
                        msg.querySelector('input').click();
                    }
                }
            });
        });

        // Add buttons to the page.
        const del = document.querySelector('#deletemsgs');
        del.parentNode.appendChild(newBr());
        del.parentNode.appendChild(selRead);
        del.parentNode.appendChild(selDupe);
    }
    else if (location.pathname === "/my/messages/new/" && location.search) { // Auto-fill user when linked from a user page
        const recipientInput = document.querySelector('#addrecipient');
        recipientInput.value = location.search.replace('?u=', '');
        pageLoad(() => {
            const recipientButton = document.querySelector('#addrecipientbtn');
            if (recipientButton) {
                recipientButton.click();
                if (recipientInput.value === "") { // If the button press doesn't work
                    return true;
                }
            }
        });
    }
    else if (location.pathname === "/my/stories/") {
        // Add links to buttons
        pageLoad(() => {
            const adventures = document.querySelectorAll('#stories tr');
            if (adventures.length > 0) {
                adventures.forEach(story => {
                    const buttons = story.querySelectorAll('input.major');
                    const id = story.querySelector('a').href.replace('https://mspfa.com/', '').replace('&p=1', '');
                    if (id) {
                        addLink(buttons[0], `/my/stories/info/${id}`);
                        addLink(buttons[1], `/my/stories/pages/${id}`);
                    }
                });
                return true;
            }
        });

        // Add user guides
        const guides = ["A Guide To Uploading Your Comic To MSPFA", "MSPFA Etiquette", "Fanventure Guide for Dummies", "CSS Guide", "HTML and CSS Things", ];
        const links = ["https://docs.google.com/document/d/17QI6Cv_BMbr8l06RrRzysoRjASJ-ruWioEtVZfzvBzU/edit?usp=sharing", "/?s=27631", "/?s=29299", "/?s=21099", "/?s=23711"];
        const authors = ["Farfrom Tile", "Radical Dude 42", "nzar", "MadCreativity", "seymour schlong"];

        const parentTd = document.querySelector('.container > tbody > tr:last-child > td');
        const unofficial = parentTd.querySelector('span');
        unofficial.textContent = "Unofficial Guides";
        const guideTable = document.createElement('table');
        const guideTbody = document.createElement('tbody');
        guideTable.style.width = "100%";
        guideTable.style.textAlign = "center";

        guideTable.appendChild(guideTbody);
        parentTd.appendChild(guideTable);

        for (let i = 0; i < guides.length; i++) {
            const guideTr = guideTbody.insertRow(i);
            const guideTd = guideTr.insertCell(0);
            const guideLink = document.createElement('a');
            guideLink.href = links[i];
            guideLink.textContent = guides[i];
            guideLink.className = "major";
            guideTd.appendChild(guideLink);
            guideTd.appendChild(newBr());
            guideTd.appendChild(document.createTextNode('by '+authors[i]));
            guideTd.appendChild(newBr());
            guideTd.appendChild(newBr());
        }
    }
    else if (location.pathname === "/my/stories/info/" && location.search) {
        // Button links
        addLink(document.querySelector('#userfavs'), `/readers/${location.search}`);
        addLink(document.querySelector('#editpages'), `/my/stories/pages/${location.search}`);
    }
    else if (location.pathname === "/my/stories/pages/" && location.search) {
        const adventureID = /\?s=(\d{5})/.exec(location.search)[1];

        if (!settings.drafts[adventureID]) {
            settings.drafts[adventureID] = {}
        }

        // Button links
        addLink(document.querySelector('#editinfo'), `/my/stories/info/?s=${adventureID}`);

        // Default spoiler values
        const replaceButton = document.querySelector('#replaceall');
        const spoilerButton = document.createElement('input');
        spoilerButton.classList.add('major');
        spoilerButton.value = 'Default Spoiler Values';
        spoilerButton.type = 'button';
        replaceButton.parentNode.insertBefore(spoilerButton, replaceButton);
        replaceButton.parentNode.insertBefore(newBr(), replaceButton);
        replaceButton.parentNode.insertBefore(newBr(), replaceButton);

        const spoilerSpan = document.createElement('span');
        const spoilerOpen = document.createElement('input');
        const spoilerClose = document.createElement('input');
        spoilerSpan.appendChild(document.createTextNode('Open button text:'));
        spoilerSpan.appendChild(newBr());
        spoilerSpan.appendChild(spoilerOpen);
        spoilerSpan.appendChild(newBr());
        spoilerSpan.appendChild(newBr());
        spoilerSpan.appendChild(document.createTextNode('Close button text:'));
        spoilerSpan.appendChild(newBr());
        spoilerSpan.appendChild(spoilerClose);

        if (!settings.spoilerValues[adventureID]) {
            settings.spoilerValues[adventureID] = {
                open: 'Show',
                close: 'Hide'
            }
        }

        spoilerOpen.value = settings.spoilerValues[adventureID].open;
        spoilerClose.value = settings.spoilerValues[adventureID].close;

        spoilerButton.addEventListener('click', evt => {
            window.MSPFA.dialog('Default Spoiler Values', spoilerSpan, ['Save', 'Cancel'], (output, form) => {
                if (output === 'Save') {
                    settings.spoilerValues[adventureID].open = spoilerOpen.value === '' ? 'Show' : spoilerOpen.value;
                    settings.spoilerValues[adventureID].close = spoilerClose.value === '' ? 'Hide' : spoilerClose.value;
                    if (settings.spoilerValues[adventureID].open === 'Show' && settings.spoilerValues[adventureID].close === 'Hide') {
                        delete settings.spoilerValues[adventureID];
                    }
                    saveData(settings);
                }
            });
        });

        document.querySelector('input[title="Spoiler"]').addEventListener('click', evt => {
            document.querySelector('#dialog input[name="open"]').value = settings.spoilerValues[adventureID].open;
            document.querySelector('#dialog input[name="close"]').value = settings.spoilerValues[adventureID].close;
            document.querySelector('#dialog input[name="open"]').placeholder = settings.spoilerValues[adventureID].open;
            document.querySelector('#dialog input[name="close"]').placeholder = settings.spoilerValues[adventureID].close;
        });

        // Buttonless spoilers
        const flashButton = document.querySelector('input[title="Flash');
        const newSpoilerButton = document.createElement('input');
        newSpoilerButton.setAttribute('data-tag', 'Buttonless Spoiler');
        newSpoilerButton.title = 'Buttonless Spoiler';
        newSpoilerButton.type = 'button';
        newSpoilerButton.style = 'background-position: -66px -88px; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAAXNSR0IArs4c6QAAARNJREFUOI3V1LFqhEAQBuDfPaxcLkFwsbhgJ2STWlhQG4twvT7IWecZ8iB3L5In2BfIbaVNglaipMkQPAiBMAnkr2Zg52OZYoD/lg0AKKVMkiQmiqK7russ9cMwvPm+v03T9MHzvKtlWUD1OI5nrXVzOUO9AICyLA9cPzXGfFp1XR+54JWllDJcMFkCAIQQr1wwWQIAiqJouWCyBADM8/zCBa+sMAy3XDBZAgCqqnrkgsmiVVxzwSsrjuNbLpgsAQB5nt9zwWSJj77hgskSAOCcO3GpnNbX+Y0jJL57+NNsAEBKuTPGtFrrxlp7yrLsYIxp+74/B0Fws9/vn6SUu2maPKqdc891XR8vZwDAWvsHe+bOOy65ZXyuZ2jeAAAAAElFTkSuQmCC");';

        newSpoilerButton.addEventListener('click', evt => {
            const bbe = document.querySelector('#bbtoolbar').parentNode.querySelector('textarea');
            if (bbe) {
                bbe.focus();
                const start = bbe.selectionStart;
                const end = bbe.selectionEnd;
                bbe.value = bbe.value.slice(0, start) + '<div class="spoiler"><div>' + bbe.value.slice(start, end) + '</div></div>' + bbe.value.slice(end);
                bbe.selectionStart = start + 26;
                bbe.selectionEnd = end + 26;
            }
        });

        flashButton.parentNode.insertBefore(newSpoilerButton, flashButton);

        // Open preview in new tab with middle mouse
        document.body.addEventListener('mouseup', evt => {
            if (evt.toElement.value === "Preview" && evt.button === 1) {
                evt.toElement.click(); // TODO: Find a way to prevent the middle mouse scroll after clicking there.
                evt.preventDefault();
                return false;
            }
        });

        // Drafts
        const accessDraftsButton = document.createElement('input');
        accessDraftsButton.classList.add('major');
        accessDraftsButton.value = 'Saved Drafts';
        accessDraftsButton.type = 'button';
        replaceButton.parentNode.insertBefore(accessDraftsButton, replaceButton);
        accessDraftsButton.parentNode.insertBefore(newBr(), replaceButton);
        accessDraftsButton.parentNode.insertBefore(newBr(), replaceButton);

        accessDraftsButton.addEventListener('click', () => {
            const draftDialog = window.MSPFA.parseBBCode('Use the textbox below to copy out the data and save to a file somewhere else.\nYou can also paste in data to replace the current drafts to ones stored there.');
            const draftInputTextarea = document.createElement('textarea');
            draftInputTextarea.placeholder = 'Paste your draft data here';
            draftInputTextarea.style = 'width: 100%; box-sizing: border-box; resize: vertical;';
            draftInputTextarea.rows = 8;
            draftDialog.appendChild(newBr());
            draftDialog.appendChild(newBr());
            draftDialog.appendChild(draftInputTextarea);
            setTimeout(() => {
                draftInputTextarea.focus();
                draftInputTextarea.selectionStart = 0;
                draftInputTextarea.selectionEnd = 0;
                draftInputTextarea.scrollTop = 0;
            }, 1);

            draftInputTextarea.value = JSON.stringify(settings.drafts[adventureID], null, 4);

            window.MSPFA.dialog('Saved Drafts', draftDialog, ['Load Draft', 'Cancel'], (output, form) => {
                if (output === "Load Draft") {
                    if (draftInputTextarea.value === '') {
                        setTimeout(() => {
                            window.MSPFA.dialog('Saved Drafts', window.MSPFA.parseBBCode('Are you sure you want to delete this adventure\'s draft data?\nMake sure you have it saved somewhere!'), ["Delete", "Cancel"], (output, form) => {
                                if (output === "Delete") {
                                    settings.drafts[adventureID] = {};
                                    saveData(settings);
                                }
                            });
                        }, 1);
                    } else if (draftInputTextarea.value !== JSON.stringify(settings.drafts[adventureID], null, 4)) {
                        setTimeout(() => {
                            window.MSPFA.dialog('Saved Drafts', window.MSPFA.parseBBCode('Are you sure you want to load this draft data?\nAll previous draft data for this adventure will be lost!'), ["Load", "Cancel"], (output, form) => {
                                if (output === "Load") {
                                    let newData = {};
                                    try { // Just in case the data given is invalid.
                                        newData = JSON.parse(draftInputTextarea.value);
                                    } catch (err) {
                                        console.error(err);
                                        setTimeout(() => {
                                            window.MSPFA.dialog('Error', window.MSPFA.parseBBCode('The entered data is invalid.'), ["Okay"]);
                                        }, 1);
                                        return;
                                    }

                                    settings.drafts[adventureID] = newData;
                                    saveData(settings);
                                }
                            });
                        }, 1);
                    }
                }
            });
        });

        const msg = document.createElement('span');
        msg.appendChild(document.createTextNode('Command:'));
        msg.appendChild(document.createElement('br'));

        const commandInput = document.createElement('input');
        commandInput.style = 'width: 100%; box-sizing: border-box;';
        commandInput.readOnly = true;
        commandInput.value = 'yes';

        msg.appendChild(commandInput);
        msg.appendChild(document.createElement('br'));
        msg.appendChild(document.createElement('br'));

        msg.appendChild(document.createTextNode('Body:'));

        const bodyInput = document.createElement('textarea');
        bodyInput.style = 'width: 100%; box-sizing: border-box; resize: vertical;';
        bodyInput.readOnly = true;
        bodyInput.rows = 8;
        bodyInput.textContent = '';

        msg.appendChild(bodyInput);

        const showDraftDialog = (pageNum) => {
            const pageElement = document.querySelector(`#p${pageNum}`);

            let shownMessage = msg;
            let optionButtons = [];

            const commandElement = pageElement.querySelector('input[name="cmd"]');
            const pageContentElement = pageElement.querySelector('textarea[name="body"]');

            if (typeof settings.drafts[adventureID][pageNum] === "undefined") {
                shownMessage = document.createTextNode('There is no draft saved for this page.');
                optionButtons = ["Save New", "Close"];
            } else {
                commandInput.value = settings.drafts[adventureID][pageNum].command;
                bodyInput.textContent = settings.drafts[adventureID][pageNum].pageContent;
                optionButtons = ["Save New", "Load", "Delete", "Close"];
            }

            window.MSPFA.dialog(`Page ${pageNum} Draft`, shownMessage, optionButtons, (output, form) => {
                if (output === "Save New") {
                    if (typeof settings.drafts[adventureID][pageNum] === "undefined") {
                        settings.drafts[adventureID][pageNum] = {
                            command: commandElement.value,
                            pageContent: pageContentElement.value
                        }
                        saveData(settings);
                    } else {
                        setTimeout(() => {
                            window.MSPFA.dialog('Overwrite current draft?', document.createTextNode('Doing this will overwrite your current draft with what is currently written in the page box. Are you sure?'), ["Yes", "No"], (output, form) => {
                                if (output === "Yes") {
                                    settings.drafts[adventureID][pageNum] = {
                                        command: commandElement.value,
                                        pageContent: pageContentElement.value
                                    }
                                    saveData(settings);
                                }
                            });
                        }, 1);
                    }
                } else if (output === "Load") {
                    if (pageContentElement.value === '' && (commandElement.value === '' || commandElement.value === document.querySelector('#defaultcmd').value)) {
                        commandElement.value = settings.drafts[adventureID][pageNum].command;
                        pageContentElement.value = settings.drafts[adventureID][pageNum].pageContent;
                        pageElement.querySelector('input[value="Save"]').disabled = false;
                    } else {
                        setTimeout(() => {
                            window.MSPFA.dialog('Overwrite current page?', document.createTextNode('Doing this will overwrite the page\'s content with what is currently written in the draft. Are you sure?'), ["Yes", "No"], (output, form) => {
                                if (output === "Yes") {
                                    commandElement.value = settings.drafts[adventureID][pageNum].command;
                                    pageContentElement.value = settings.drafts[adventureID][pageNum].pageContent;
                                    pageElement.querySelector('input[value="Save"]').disabled = false;
                                }
                            });
                        }, 1);
                    }
                } else if (output === "Delete") {
                    setTimeout(() => {
                        window.MSPFA.dialog('Delete this draft?', document.createTextNode('This action is unreversable! Are you sure?'), ["Yes", "No"], (output, form) => {
                            if (output === "Yes") {
                                delete settings.drafts[adventureID][pageNum];
                                saveData(settings);
                            }
                        });
                    }, 1);
                }
            });
        }

        const createDraftButton = (form) => {
            const draftButton = document.createElement('input');
            draftButton.className = 'major draft';
            draftButton.type = 'button';
            draftButton.value = 'Draft';
            draftButton.style = 'margin-right: 9px;';
            draftButton.addEventListener('click', () => {
                showDraftDialog(form.id.replace('p', ''));
            });
            return draftButton;
        }

        pageLoad(() => {
            let allPages = document.querySelectorAll('#storypages form:not(#newpage)');
            if (allPages.length !== 0) {
                allPages.forEach(form => {
                    const prevButton = form.querySelector('input[name="preview"]');
                    prevButton.parentNode.insertBefore(createDraftButton(form), prevButton);
                });
                document.querySelector('input[value="Add"]').addEventListener('click', () => {
                    allPages = document.querySelectorAll('#storypages form:not(#newpage)');
                    const form = document.querySelector(`#p${allPages.length}`);
                    const prevButton = form.querySelector('input[name="preview"]');
                    prevButton.parentNode.insertBefore(createDraftButton(form), prevButton);
                });
                return true;
            }
        });

        if (hashSearch) {
            pageLoad(() => {
                const element = document.querySelector(hashSearch);
                if (element) {
                    if (element.style.display === "none") {
                        element.style = '';
                    }
                    return true;
                }
            });
        }
    }
    else if (location.pathname === "/user/") {
        const id = location.search.slice(3);
        const statAdd = [];
        // Button links
        pageLoad(() => {
            const msgButton = document.querySelector('#sendmsg');
            if (msgButton) {
                addLink(msgButton, `/my/messages/new/${location.search}`); // note: doesn't input the desired user's id
                addLink(document.querySelector('#favstories'), `/favs/${location.search}`);
                return true;
            }
        });

        // Add extra user stats
        pageLoad(() => {
            if (window.MSPFA) {
                const stats = document.querySelector('#userinfo table');

                const joinTr = stats.insertRow(1);
                const joinTextTd = joinTr.insertCell(0);
                joinTextTd.appendChild(document.createTextNode("Account created:"));
                const joinDate = joinTr.insertCell(1);
                const joinTime = document.createElement('b');
                joinTime.textContent = "Loading...";
                joinDate.appendChild(joinTime);

                const advCountTr = stats.insertRow(2);
                const advTextTd = advCountTr.insertCell(0);
                advTextTd.appendChild(document.createTextNode("Adventures created:"));
                const advCount = advCountTr.insertCell(1);
                const advCountText = document.createElement('b');
                advCountText.textContent = "Loading...";
                advCount.appendChild(advCountText);

                if (statAdd.indexOf('date') === -1) {
                    window.MSPFA.request(0, {
                        do: "user",
                        u: id
                    }, user => {
                        if (typeof user !== "undefined") {
                            statAdd.push('date');
                            joinTime.textContent = new Date(user.d).toString().split(' ').splice(1, 4).join(' ');
                        }
                    });
                }

                if (statAdd.indexOf('made') === -1) {
                    window.MSPFA.request(0, {
                        do: "editor",
                        u: id
                    }, s => {
                        if (typeof s !== "undefined") {
                            statAdd.push('made');
                            advCountText.textContent = s.length;
                        }
                    });
                }

                if (document.querySelector('#favstories').style.display !== 'none' && statAdd.indexOf('fav') === -1) {
                    statAdd.push('fav');
                    const favCountTr = stats.insertRow(3);
                    const favTextTd = favCountTr.insertCell(0);
                    favTextTd.appendChild(document.createTextNode("Adventures favorited:"));
                    const favCount = favCountTr.insertCell(1);
                    const favCountText = document.createElement('b');
                    favCountText.textContent = "Loading...";
                    window.MSPFA.request(0, {
                        do: "favs",
                        u: id
                    }, s => {
                        if (typeof s !== "undefined") {
                            favCountText.textContent = s.length;
                        }
                    });
                    favCount.appendChild(favCountText);
                }

                return true;
            }
        });
    }
    else if (location.pathname === "/favs/" && location.search) {
        // Button links
        pageLoad(() => {
            const stories = document.querySelectorAll('#stories tr');
            let favCount = 0;

            if (stories.length > 0) {
                stories.forEach(story => {
                    favCount++;
                    const id = story.querySelector('a').href.replace('https://mspfa.com/', '');
                    pageLoad(() => {
                        if (window.MSPFA.me.i) {
                            addLink(story.querySelector('.edit.major'), `/my/stories/info/${id}`);
                            return true;
                        }
                    });
                    addLink(story.querySelector('.rss.major'), `/rss/${id}`);
                });

                // Fav count
                const username = document.querySelector('#username');
                username.parentNode.appendChild(newBr());
                username.parentNode.appendChild(newBr());
                username.parentNode.appendChild(document.createTextNode(`Favorited adventures: ${favCount}`));

                return true;
            }
        });
    }
    else if (location.pathname === "/search/" && location.search) {
        // Character and word statistics
        const statTable = document.createElement('table');
        const statTbody = document.createElement('tbody');
        const statTr = statTbody.insertRow(0);
        const charCount = statTr.insertCell(0);
        const wordCount = statTr.insertCell(0);
        const statParentTr = document.querySelector('#pages').parentNode.parentNode.insertRow(2);
        const statParentTd = statParentTr.insertCell(0);

        const statHeaderTr = statTbody.insertRow(0);
        const statHeader = document.createElement('th');
        statHeader.colSpan = '2';

        statHeaderTr.appendChild(statHeader);
        statHeader.textContent = 'Statistics may not be entirely accurate.';

        statTable.style.width = "100%";

        charCount.textContent = "Character count: loading...";
        wordCount.textContent = "Word count: loading...";

        statTable.appendChild(statTbody);
        statParentTd.appendChild(statTable);

        pageLoad(() => {
            if (document.querySelector('#pages br')) {
                const bbc = window.MSPFA.BBC.slice();
                bbc.splice(0, 3);

                window.MSPFA.request(0, {
                    do: "story",
                    s: location.search.replace('?s=', '')
                }, story => {
                    if (typeof story !== "undefined") {
                        const pageContent = [];
                        story.p.forEach(p => {
                            pageContent.push(p.c);
                            pageContent.push(p.b);
                        });

                        const storyText = pageContent.join(' ')
                        .replace(/\n/g, ' ')
                        .replace(bbc[0][0], '$1')
                        .replace(bbc[1][0], '$1')
                        .replace(bbc[2][0], '$1')
                        .replace(bbc[3][0], '$1')
                        .replace(bbc[4][0], '$2')
                        .replace(bbc[5][0], '$3')
                        .replace(bbc[6][0], '$3')
                        .replace(bbc[7][0], '$3')
                        .replace(bbc[8][0], '$3')
                        .replace(bbc[9][0], '$3')
                        .replace(bbc[10][0], '$2')
                        .replace(bbc[11][0], '$1')
                        .replace(bbc[12][0], '$3')
                        .replace(bbc[13][0], '$3')
                        .replace(bbc[14][0], '')
                        .replace(bbc[16][0], '$1')
                        .replace(bbc[17][0], '$2 $4 $5')
                        .replace(bbc[18][0], '$2 $4 $5')
                        .replace(bbc[19][0], '')
                        .replace(bbc[20][0], '')
                        .replace(/<(.*?)>/g, '');

                        wordCount.textContent = `Word count: ${storyText.split(/ +/g).length}`;
                        charCount.textContent = `Character count: ${storyText.replace(/ +/g, '').length}`;
                    }
                });
                return true;
            }
        });
    }
})();