Script to update enemy infoboxes, en masse. Currently targeting Template:FFVIII Enemy. Depends on Module:Formatter for parsing parameters.

Notes[edit | edit source]

I'm using a couple of really new features here:

  • async/await, which is part of ES2017 and available in most evergreen browsers.
  • Object rest/spread, which will probably be part of ES2018. It's currently available in Chrome and Firefox, but other browsers are working on it too.

...In addition to some general ES2015 stuff. If your browser is out-of-date, the following code probably won't work. FANDOM also validates JavaScript pages (e.g. MediaWiki:Common.js) on the server, using a significantly outdated parser which doesn't support all of this cool new stuff. In other words, if you aren't running it on the console, you're still stuck with jQuery deferreds. :(

Code[edit | edit source]

require(['jquery', 'mw'], async ($, mw) => {
    'use strict';

    await mw.loader.using([
        'mediawiki.api',
        'mediawiki.api.edit'
    ]);

    /**
     * Describe the following function here.
     */
    function gallery(...args) {
        const tmp = args.map(([label, wikitext]) => {
            const [target] = wikitext
                .replace(/^\[\[/u, '')
                .replace(/\]\]$/u, '')
                .split('|');

            const image = target
                ? new mw.Title(target).getMainText()
                : `<!-- ${label} -->`;

            return `${image}|${label}`;
        });

        return ['<gallery>', ...tmp, '</gallery>'].join('\n');
    }

    const api = new mw.Api();
    const templates = [
        {
            title: 'Template:FFVIII Enemy',
            infoboxBuilder(parameters) {
                const enemy = [
                    '{{infobox enemy',
                    '| release = FFVIII',
                    '| name = ' + parameters.name,
                    '| japanese = ' + parameters.japan,
                    '| romaji = ' + parameters.romaji,
                    '| lit = ' + parameters.lit,
                    '| image = ' + parameters.image || gallery(
                        [['1' || parameters['tab 1']], parameters['1 image']],
                        [['2' || parameters['tab 2']], parameters['2 image']],
                        [['3' || parameters['tab 3']], parameters['3 image']],
                        [['4' || parameters['tab 4']], parameters['4 image']],
                    ),
                    '| location = ' + parameters.Location,
                    '| type = ' + parameters.Enemy || 'Enemy',
                    '| formations = ',
                    '| aiscript = ',
                    '}}'
                ].join('\n');

                const stats = [
                    '{{infobox enemy stats FFVIII',
                    '| prev = ' + parameters.prev,
                    '| prev no = ' + parameters['prev no'],
                    '| bestiary = ' + parameters.bestiary,
                    '| next = ' + parameters.prev,
                    '| next no = ' + parameters['next no'],
                    '| low min = ' + parameters.LvL1,
                    '| low max = ' + parameters.LvL2,
                    '| mid min = ' + parameters.LvM1,
                    '| mid max = ' + parameters.LvM2,
                    '| high min = ' + parameters.LvH1,
                    '| high max = ' + parameters.LvH2,
                    '| hp a = ' + parameters.HPf1,
                    '| hp b = ' + parameters.HPf2,
                    '| hp c = ' + parameters.HPf3,
                    '| str a = ' + parameters.STRf1,
                    '| str b = ' + parameters.STRf2,
                    '| str c = ' + parameters.STRf3,
                    '| str d = ' + parameters.STRf4,
                    '| mag a = ' + parameters.MAGf1,
                    '| mag b = ' + parameters.MAGf2,
                    '| mag c = ' + parameters.MAGf3,
                    '| mag d = ' + parameters.MAGf4,
                    '| vit a = ' + parameters.VITf1,
                    '| vit b = ' + parameters.VITf2,
                    '| vit c = ' + parameters.VITf3,
                    '| vit d = ' + parameters.VITf4,
                    '| spr a = ' + parameters.SPRf1,
                    '| spr b = ' + parameters.SPRf2,
                    '| spr c = ' + parameters.SPRf3,
                    '| spr d = ' + parameters.SPRf4,
                    '| spd a = ' + parameters.SPDf1,
                    '| spd b = ' + parameters.SPDf2,
                    '| spd c = ' + parameters.SPDf3,
                    '| spd d = ' + parameters.SPDf4,
                    '| eva a = ' + parameters.EVAf1,
                    '| eva b = ' + parameters.EVAf2,
                    '| eva c = ' + parameters.EVAf3,
                    '| eva d = ' + parameters.EVAf4,
                    '| exp a = ' + parameters.EXPf1,
                    '| exp b = ' + parameters.EXPf2,
                    '| ap = ' + parameters.AP,
                    '| fire = ' + parameters.Fire,
                    '| thunder = ' + parameters.Thunder,
                    '| ice = ' + parameters.Ice,
                    '| water = ' + parameters.Water,
                    '| wind = ' + parameters.Wind,
                    '| earth = ' + parameters.Earth,
                    '| poison = ' + parameters.Poison,
                    '| holy = ' + parameters.Holy,
                    '| undead = ' + parameters.Undead,
                    '| gravity = ' + parameters.Gravity,
                    '| death = ' + parameters.Death,
                    '| poison status = ' + parameters.Poisoned,
                    '| petrify = ' + parameters.Petrify,
                    '| blind = ' + parameters.Blind,
                    '| silence = ' + parameters.Silence,
                    '| berserk = ' + parameters.Beserk,
                    '| zombie = ' + parameters.Zombie,
                    '| sleep = ' + parameters.Sleep,
                    '| haste = ' + parameters.Haste,
                    '| slow = ' + parameters.Slow,
                    '| stop = ' + parameters.Stop,
                    '| regen = ' + parameters.Regen,
                    '| reflect = ' + parameters.Reflect,
                    '| doom = ' + parameters.Doom,
                    '| petrifying = ' + parameters.Petrifying,
                    '| float = ' + parameters.Float,
                    '| drain = ' + parameters.Drain,
                    '| confuse = ' + parameters.Confuse,
                    '| eject = ' + parameters.Eject,
                    '| lvmod = ' + parameters.LVMod,
                    '| the end = ' + parameters['The End'],
                    '| drop rate = ' + parameters['Drop rate'],
                    '| mug rate = ' + parameters['Mug rate'],
                    '| mug 1 = ' + parameters['Mug 1'],
                    '| mug 2 = ' + parameters['Mug 2'],
                    '| mug 3 = ' + parameters['Mug 3'],
                    '| mug 4 = ' + parameters['Mug 4'],
                    '| drop 1 = ' + parameters['Drop 1'],
                    '| drop 2 = ' + parameters['Drop 2'],
                    '| drop 3 = ' + parameters['Drop 3'],
                    '| drop 4 = ' + parameters['Drop 4'],
                    '| draw = ' + parameters.Draw,
                    '| taste = ' + parameters.Taste,
                    '| low mug 1 = ' + parameters['Low Mug 1'],
                    '| low mug 2 = ' + parameters['Low Mug 2'],
                    '| low mug 3 = ' + parameters['Low Mug 3'],
                    '| low mug 4 = ' + parameters['Low Mug 4'],
                    '| low drop 1 = ' + parameters['Low Drop 1'],
                    '| low drop 2 = ' + parameters['Low Drop 2'],
                    '| low drop 3 = ' + parameters['Low Drop 3'],
                    '| low drop 4 = ' + parameters['Low Drop 4'],
                    '| low draw = ' + parameters['Low Draw'],
                    '| low taste = ' + parameters['Low Taste'],
                    '| mid mug 1 = ' + parameters['Mid Mug 1'],
                    '| mid mug 2 = ' + parameters['Mid Mug 2'],
                    '| mid mug 3 = ' + parameters['Mid Mug 3'],
                    '| mid mug 4 = ' + parameters['Mid Mug 4'],
                    '| mid drop 1 = ' + parameters['Mid Drop 1'],
                    '| mid drop 2 = ' + parameters['Mid Drop 2'],
                    '| mid drop 3 = ' + parameters['Mid Drop 3'],
                    '| mid drop 4 = ' + parameters['Mid Drop 4'],
                    '| mid draw = ' + parameters['Mid Draw'],
                    '| mid taste = ' + parameters['Mid Taste'],
                    '| high mug 1 = ' + parameters['High Mug 1'],
                    '| high mug 2 = ' + parameters['High Mug 2'],
                    '| high mug 3 = ' + parameters['High Mug 3'],
                    '| high mug 4 = ' + parameters['High Mug 4'],
                    '| high drop 1 = ' + parameters['High Drop 1'],
                    '| high drop 2 = ' + parameters['High Drop 2'],
                    '| high drop 3 = ' + parameters['High Drop 3'],
                    '| high drop 4 = ' + parameters['High Drop 4'],
                    '| high draw = ' + parameters['High Draw'],
                    '| high taste = ' + parameters['High Taste'],
                    '| card drop = ' + parameters['Card Drop'],
                    '| location = ' + parameters.Location,
                    '| flying = ' + parameters.Flying,
                    '| scan = ' + parameters.Scan,
                    '| card 1 = ' + parameters['Card 1'],
                    '| card 2 = ' + parameters['Card 2'],
                    '| attacks = ' + parameters.Attacks,
                    '| low attacks = ' + parameters['Low Attacks'],
                    '| mid attacks = ' + parameters['Mid Attacks'],
                    '| high attacks = ' + parameters['High Attacks'],
                    '| info = ' + parameters['Other Information'],
                    '| low info = ' + parameters['Low Other Information'],
                    '| mid info = ' + parameters['Mid Other Information'],
                    '| high info = ' + parameters['High Other Information'],
                    '}}'
                ].join('\n');

                return {enemy, stats};
            }
        }
    ];

    /**
     * Gotta match crazy stuff like "{{__tEmPlAtE:__fFIII__Enemies__":
     *
     * The namespace is case-insensitive, and defaults to "Template:".
     * The first letter of the title is case-insensitive.
     * Underscores are converted to spaces.
     * Whitespace is removed from before the namespace, title, and parameters.
     * Consecutive spaces are collapsed.
     */
    function matchTemplateStart(str) {
        function desensitize(char) {
            const upper = char.toUpperCase();
            const lower = char.toLowerCase();

            if (upper === lower) {
                return char;
            }

            return '[' + upper + lower + ']';
        }

        function formatPagename(str) {
            const first = desensitize(str[0]);
            const rest = str.slice(1);

            return (first + rest).replace(/_/ug, '[ _]+');
        }

        const title = new mw.Title(str);
        const tmp = title.getNamespacePrefix()
            .split('')
            .map(desensitize)
            .join('');

        const whitespace = '[\\s_]*?';
        const start = '\\{\\{' + whitespace;
        const namespace = '(?:' + tmp + whitespace + ')?';
        const pagename = formatPagename(title.title);

        return new RegExp(start + namespace + pagename + whitespace, 'ug');
    }

    /**
     * Make successive calls to the API to generate a list of articles (i.e.
     * pages in the main namespace) that transclude a given page.
     */
    async function findTransclusions(title) {
        let pages = [];
        let cont = {};

        while (cont) {
            const response = await api.get({
                action: 'query',
                list: 'embeddedin',
                eititle: title,
                einamespace: 0,
                eilimit: 500,
                ...cont.embeddedin
            });

            pages = [...pages, ...response.query.embeddedin];
            cont = response['query-continue'];
        }

        return pages;
    }

    /**
     * Use [[Module:Formatter]] to build a regular expression that matches a
     * given page. Makes an API call to grab the results.
     */
    async function buildRegex(title) {
        const response = await api.get({
            action: 'parse',
            text: `{{subst:#invoke:Formatter|${title}}}`,
            onlypst: true
        });

        const pattern = response.parse.text['*']
            .replace(/^\| (.+?) = $/ugm, '\\| ($1) = ([\\s\\S]*?)')
            .replace(/([{}])/ug, '\\$1');

        return new RegExp(pattern, 'u');
    }

    /**
     * Make an API call to grab the top section of a given page.
     */
    async function getTopSection(title) {
        const response = await api.get({
            action: 'parse',
            page: title,
            prop: 'wikitext',
            section: 0
        });

        return response.parse.wikitext['*'];
    }

    /**
     * Use [[Module:Formatter]] to reorganize a given blob of wikitext, so that
     * transclusions of a given page have consistent formatting. Makes an API
     * call to grab the results.
     */
    async function reformat(title, wikitext) {
        const pattern = matchTemplateStart(title);
        const replacement = `{{subst:#invoke:Formatter|${title}`;

        // using POST to avoid long URIs
        const response = await api.post({
            action: 'parse',
            text: wikitext.replace(pattern, replacement),
            onlypst: true
        });

        return response.parse.text['*'];
    }

    /**
     * Parse a given blob of wikitext with a given regular expression.
     * Interprets the results as a list of key-value pairs.
     */
    function getParameters(wikitext, regex) {
        return regex.exec(wikitext).reduce((obj, val, i, arr) => {
            // skip arr[0] because we don't care about the full match
            if (i % 2 === 1) {
                obj[val] = arr[i + 1];
            }

            return obj;
        }, {});
    }

    // BEGIN MAIN LOOP
    for (const {title, infoboxBuilder} of templates) {
        const transclusions = await findTransclusions(title);

        if (transclusions.length === 0) {
            continue;
        }

        const regex = await buildRegex(title);

        for (const {title: title2} of transclusions) {
            console.log(`Now editing ${title2}...`);

            const topSection = await getTopSection(title2);
            const wikitext = await reformat(title, topSection);
            const parameters = getParameters(wikitext, regex);
            const {enemy, stats} = infoboxBuilder(parameters);

            // there's no way to do this in one pass, unfortunately :(
            await api.postWithEditToken({
                action: 'edit',
                title: title2,
                section: 1,
                text: '',
                summary: '[[User:DarthKitty/Enemy page updater|<auto>]] Remove [[Template:FFVIII Enemy Stats]]'
            });

            await api.postWithEditToken({
                action: 'edit',
                title: title2,
                section: 0,
                text: `${wikitext.replace(regex, enemy)}\n\n==Stats==\n${stats}`,
                summary: '[[User:DarthKitty/Enemy page updater|<auto>]] Convert enemy infoboxes'
            });
        }
    }
    // END MAIN LOOP

    console.log('Done!');
});
Community content is available under CC-BY-SA unless otherwise noted.