MSPFA extras

Adds custom features to MSPFA.

当前为 2020-07-30 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MSPFA extras
// @namespace    http://tampermonkey.net/
// @version      1.3.2.2
// @description  Adds custom features to MSPFA.
// @author       seymour schlong
// @match        https://mspfa.com/
// @match        https://mspfa.com/*/
// @match        https://mspfa.com/*/?*
// @match        https://mspfa.com/?s=*
// @match        https://mspfa.com/my/*
// @grant        none
// ==/UserScript==


(function() {
    'use strict';

    /**
    * https://github.com/GrantGryczan/MSPFA/projects/1?fullscreen=true
    * Github to-do completion list
    *
    * https://github.com/GrantGryczan/MSPFA/issues/26 - Dropdown menu
    * https://github.com/GrantGryczan/MSPFA/issues/18 - MSPFA themes
    * https://github.com/GrantGryczan/MSPFA/issues/32 - Adventure creation dates
    * https://github.com/GrantGryczan/MSPFA/issues/32 - User creation dates
    * https://github.com/GrantGryczan/MSPFA/issues/40 - Turn certain buttons into links
    * https://github.com/GrantGryczan/MSPFA/issues/41 - Word and character count
    *
    * Extension to-do... maybe...
    * https://github.com/GrantGryczan/MSPFA/issues/57 - Default spoiler values  (might be very difficult unless i can detect when any spoiler button on the bbtoolbar is clicked. EACH ONE)
    * https://github.com/GrantGryczan/MSPFA/issues/62 - Buttonless spoilers     (may also be extremely tough, as you have to add a button to each toolbar (or add an option in the regular spoiler))
    */

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

    // Saves the options data for the script.
    const saveData = (data) => {
        localStorage.mspfaextra = JSON.stringify(data);
        //console.log("Saved cookies under mspfaextra.");
    };

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

    let settings = {};

    if (localStorage.mspfaextra) {
        settings = JSON.parse(localStorage.mspfaextra);
    } else {
        settings.autospoiler = false;
        settings.style = 0;
        settings.styleURL = "";
        settings.night = false;
        settings.auto502 = true;
        saveData(settings);
    }
    // If any settings are undefined, re-set to their default state. (For older users when new things get stored)
    if (typeof settings.autospoiler === "undefined") {
        settings.autospoiler = false;
    }
    if (typeof settings.style === "undefined") {
        settings.style = 0;
    }
    if (typeof settings.styleURL === "undefined") {
        settings.styleURL = "";
    }
    if (typeof settings.night === "undefined") {
        settings.night = false;
    }
    if (typeof settings.auto502 === "undefined") {
        settings.auto502 = true;
    }
    if (typeof settings.textFix === "undefined") {
        settings.textFix = false;
    }
    if (typeof settings.pixelFix === "undefined") {
        settings.pixelFix = false;
    }

    //console.log(settings);

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

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

    let 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';
    });

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

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

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

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

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

    let pixelFixText = 'img, .mspfalogo, .major, .arrow, #flashytitle, .heart, .fav, .notify, .edit, .rss, input, #loading { image-rendering: pixelated !important; }'
    let dropStyleText = `#notification { z-index: 2; } .dropdown-content a { color: inherit; padding: 2px; text-decoration: underline; display: block;}`;
    let dropStyle = document.createElement('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;}';

    let theme = document.createElement('link');
    Object.assign(theme, { id: 'theme', type: 'text/css', rel: 'stylesheet' });

    if (!document.querySelector('#theme') && !/^\/css\/|^\/js\//.test(location.pathname)) {
        document.querySelector('head').appendChild(theme);
    }
    if (!document.querySelector('#dropdown-style')) {
        document.querySelector('head').appendChild(dropStyle);
    }

    const updateTheme = (src) => {
        theme.href = src;
    }

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

    pageLoad(() => {
        if (window.MSPFA) {
            if (window.MSPFA.story && window.MSPFA.story.y && window.MSPFA.story.y.length > 0) {
                updateTheme('');
            }
            return true;
        }
    });

    pageLoad(() => {
        if (document.querySelector('footer .mspfalogo')) {
            document.querySelector('footer .mspfalogo').addEventListener('dblclick', evt => {
                if (evt.button === 0) {
                    settings.night = !settings.night;
                    saveData(settings);

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

                    dropStyle.textContent = dropStyleText + '';
                    dropStyle.textContent = dropStyleText + '*{transition:1s}';
                    setTimeout(() => {
                        dropStyle.textContent = dropStyleText;
                    }, 1000);

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

    if (location.pathname === "/" || location.pathname === "/preview/") {
        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) {
            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;
                }
            });
            if (settings.textFix) {
                pageLoad(() => {
                    if (window.MSPFA.story && window.MSPFA.story.p) {
                        // russian/bulgarian is not possible =(
                        let currentPage = parseInt(/^\?s(?:.*?)&p=([\d]*)$/.exec(location.search)[1]);
                        let 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;
                    }
                });
            }
            pageLoad(() => {
                let infoButton = document.querySelector('.edit.major');
                if (infoButton) {
                    pageLoad(() => {
                        if (window.MSPFA.me.i) {
                            addLink(infoButton, `/my/stories/info/${location.search.split('&p=')[0]}`);
                            return;
                        }
                    });
                    addLink(document.querySelector('.rss.major'), `/rss/${location.search.split('&p=')[0]}`);
                    return true;
                }
            });
            /*
            pageLoad(() => {
                let infoButton = document.querySelector('.edit.major');
                if (infoButton) {
                    let editPages = document.createElement('button');
                    Object.assign(editPages, { className: 'editpages major edit', title: 'Edit pages'});
                    //infoButton.parentNode.insertBefore(editPages, infoButton);
                    return true;
                }
            });/**/
        }
    }
    else if (location.pathname === "/my/settings/") { // Custom settings
        let saveBtn = document.querySelector('#savesettings');

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

        let headerTr = document.createElement('tr');
        let header = document.createElement('th');
        header.textContent = "Extra Settings";
        headerTr.appendChild(header);

        let moreTr = document.createElement('tr');
        let 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);

        let settingsTr = document.createElement('tr');
        let localMsg = document.createElement('span');
        let 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!";
        let plusTable = document.createElement('table');
        let plusTbody = document.createElement('tbody');
        plusTable.appendChild(plusTbody);
        settingsTd.appendChild(localMsg);
        settingsTd.appendChild(document.createElement('br'));
        settingsTd.appendChild(document.createElement('br'));
        settingsTd.appendChild(plusTable);
        settingsTr.appendChild(settingsTd);

        let spoilerTr = plusTbody.insertRow(plusTbody.childNodes.length);
        let spoilerTextTd = spoilerTr.insertCell(0);
        let spoilerInputTd = spoilerTr.insertCell(1);
        let spoilerInput = document.createElement('input');
        spoilerInputTd.appendChild(spoilerInput);

        let errorTr = plusTbody.insertRow(plusTbody.childNodes.length);
        let errorTextTd = errorTr.insertCell(0);
        let errorInputTd = errorTr.insertCell(1);
        let errorInput = document.createElement('input');
        errorInputTd.appendChild(errorInput);

        let pixelFixTr = plusTbody.insertRow(plusTbody.childNodes.length);
        let pixelFixTextTd = pixelFixTr.insertCell(0);
        let pixelFixInputTd = pixelFixTr.insertCell(1);
        let pixelFixInput = document.createElement('input');
        pixelFixInputTd.appendChild(pixelFixInput);

        let textFixTr = plusTbody.insertRow(plusTbody.childNodes.length);
        let textFixTextTd = textFixTr.insertCell(0);
        let textFixInputTd = textFixTr.insertCell(1);
        let textFixInput = document.createElement('input');
        textFixInputTd.appendChild(textFixInput);

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

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

        plusTable.style = "text-align: center;";
        spoilerTextTd.textContent = "Automatically open spoilers:";
        spoilerInput.name = "p1";
        spoilerInput.type = "checkbox";
        spoilerInput.checked = settings.autospoiler;

        errorTextTd.textContent = "Automatically reload Cloudflare 502 error pages:";
        errorInput.name = "p2";
        errorInput.type = "checkbox";
        errorInput.checked = settings.auto502;

        pixelFixTextTd.textContent = "Change pixel scaling to nearest neighbour:";
        pixelFixInput.name = "p3";
        pixelFixInput.type = "checkbox";
        pixelFixInput.checked = settings.pixelFix;

        textFixTextTd.textContent = "Attempt to fix text errors (experimental)*:";
        textFixInput.name = "p4";
        textFixInput.type = "checkbox";
        textFixInput.checked = settings.textFix;

        cssTextTd.textContent = "Change style:";

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

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

        // Enable the save button
        spoilerInput.addEventListener("change", () => {
            saveBtn.disabled = false;
        });
        errorInput.addEventListener("change", () => {
            saveBtn.disabled = false;
        });
        pixelFixInput.addEventListener("change", () => {
            saveBtn.disabled = false;
        });
        textFixInput.addEventListener("change", () => {
            saveBtn.disabled = false;
        });
        cssSelect.addEventListener("change", () => {
            saveBtn.disabled = false;
        });
        customCssInput.addEventListener("keydown", () => {
            saveBtn.disabled = false;
        });

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

        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.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
        let 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 => {
            let temp = document.querySelectorAll('#messages > tr');
            let msgs = [];
            for (let i = temp.length - 1; i >= 0; i--) {
                msgs.push(temp[i]);
            }
            let 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.
        let del = document.querySelector('#deletemsgs');
        del.parentNode.appendChild(document.createElement('br'));
        del.parentNode.appendChild(selRead);
        del.parentNode.appendChild(selDupe);
    }
    else if (location.pathname === "/my/stories/") {
        pageLoad(() => {
            let adventures = document.querySelectorAll('#stories tr');
            if (adventures.length > 0) {
                adventures.forEach(story => {
                    let buttons = story.querySelectorAll('input.major');
                    let id = story.querySelector('a').href.replace('https://mspfa.com/', '').replace('&p=1', '');
                    addLink(buttons[0], `/my/stories/info/${id}`);
                    addLink(buttons[1], `/my/stories/pages/${id}`);
                });
                return true;
            }
        });

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

        let parentTd = document.querySelector('.container > tbody > tr:last-child > td');
        let unofficial = parentTd.querySelector('span');
        unofficial.textContent = "Unofficial Guides";
        let guideTable = document.createElement('table');
        let 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++) {
            let guideTr = guideTbody.insertRow(i);
            let guideTd = guideTr.insertCell(0);
            let guideLink = document.createElement('a');
            guideLink.href = links[i];
            guideLink.textContent = guides[i];
            guideLink.className = "major";
            guideTd.appendChild(guideLink);
            guideTd.appendChild(document.createElement('br'));
            guideTd.appendChild(document.createTextNode('by '+authors[i]));
            guideTd.appendChild(document.createElement('br'));
            guideTd.appendChild(document.createElement('br'));
        }
    }
    else if (location.pathname === "/my/stories/info/" && location.search) {
        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) {
        addLink(document.querySelector('#editinfo'), `/my/stories/info/${location.search}`);
    }
    else if (location.pathname === "/user/") {
        pageLoad(() => {
            let msgButton = document.querySelector('#sendmsg');
            if (msgButton) {
                addLink(msgButton, '/my/messages/new/'); // note: doesn't input the desired user's id
                addLink(document.querySelector('#favstories'), `/favs/${location.search}`);
            }
        });

        pageLoad(() => {
            if (window.MSPFA) {
                window.MSPFA.request(0, {
                    do: "user",
                    u: location.search.slice(3)
                }, user => {
                    if (typeof user !== "undefined") {
                        let stats = document.querySelector('#userinfo table');
                        let joinTr = stats.insertRow(1);
                        let joinTextTd = joinTr.insertCell(0);
                        joinTextTd.appendChild(document.createTextNode("Account created:"));
                        let d = new Date(user.d).toString().split(' ').splice(1, 4).join(' ');
                        let joinDate = joinTr.insertCell(1);
                        let joinTime = document.createElement('b');
                        joinTime.appendChild(document.createTextNode(d));
                        joinDate.appendChild(joinTime);
                    }
                }, status => {
                    console.log(status);
                }, true);
                return true;
            }
        });
    }
    else if (location.pathname === "/favs/" && location.search) {
        pageLoad(() => {
            let stories = document.querySelectorAll('#stories tr');

            if (stories.length > 0) {
                stories.forEach(story => {
                    let 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;
                        }
                    });
                    addLink(story.querySelector('.rss.major'), `/rss/${id}`);
                });
                return true;
            }
        });
    }
    else if (location.pathname === "/search/" && location.search) {
        let pages = document.querySelector('#pages');
        let statTable = document.createElement('table');
        let statTbody = document.createElement('tbody');
        let statTr = statTbody.insertRow(0);
        let charCount = statTr.insertCell(0);
        let wordCount = statTr.insertCell(0);
        let statParentTr = pages.parentNode.parentNode.insertRow(2);
        let statParentTd = statParentTr.insertCell(0);

        let statHeaderTr = statTbody.insertRow(0);
        let 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')) {
                let 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") {
                        let pageContent = [];
                        story.p.forEach(p => {
                            pageContent.push(p.c);
                            pageContent.push(p.b);
                        });

                        let 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}`;
                    }
                }, status => {
                    console.log(status);
                }, true);
                return true;
            }
        });
    }
})();