User:Tokenzero/tinfoboxTemplateData.js

Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <nowiki>
/**
 * @module tinfoboxTemplateData
 * Classes wrapping TemplateData.
 * Usage: see User:Tokenzero/infoboxJournal.js for how to load a module.
 *  import { TemplateData, TemplateDataParam } from '/w/index.php?title=User:Tokenzero/tinfoboxTemplateData.js&action=raw&ctype=text%2Fjavascript';
 *  (async function() {
 *      templateData = await m.TemplateData.fetch('Template:Infobox journal');
 *      console.log(templateData.param('title').deprecated);
 *      wikicode = templateData.build({title: 'Foo'});
 *      console.log(wikicode);
 *  })();
 * For documentation of TemplateData in general (not this wrapper module):
 * - https://www.mediawiki.org/wiki/Extension:TemplateData
 * - https://www.mediawiki.org/wiki/Help:TemplateData
 * - example JSON: https://en.wikipedia.org/w/api.php?action=templatedata&titles=Template:Infobox%20journal&format=jsonfm&formatversion=2&lang=en
 */

/** Configuration of a template parameter, see {@link TemplateData}. */
export class TemplateDataParam {
    /** @param {string} key */
    constructor(key) {
        /** @type {string} - The canonical name of the parameter. */
        this.key = key;
        /** @type {string} - A short human label like "Former name". */
        this.label = '';
        /** @type {string} */
        this.description = '';
        /**
         * @type {string}
         * One of: unknown/number/boolean/string (any text)/line (short label text)/
         *  date (in ISO format e.g. "2014-05-09" or "2014-05-09T16:01:12Z") /
         *  content (wikitext) / unbalanced-wikitext /
         *  wiki-page-name / wiki-file-name (without File:) /
         *  wiki-template-name / wiki-user-name (without User:)
         */
        this.type = 'unknown';
        /** @type {string} - Default value assumed if none is given. */
        this.default = null;
        /** @type {string} - Initially suggested value, often like '2019'. */
        this.autovalue = null;
        /** @type {string} */
        this.example = null;
        /** @type {boolean} */
        this.required = false;
        /** @type {boolean} */
        this.suggested = false;
        /**
         * @type {boolean}
         * Suggested, but not added as empty by default; situational.
         * This is tinfobox-specific. At most one of suggested/weaklySuggested should be true.
         */
        this.weaklySuggested = false;
        /** @type {(boolean|string)} - May be an instruction of what to use in place of it. */
        this.deprecated = false;
        /** @type {Array<string>} - Other names for the parameter. */
        this.aliases = [];
    }

    /**
     * Serialize to simple json object, called by JSON.stringify.
     *
     * @returns {object}
     */
    toJSON() {
        return {
            key: this.key,
            label: this.label,
            description: this.description,
            default: this.default,
            autovalue: this.autovalue,
            example: this.example,
            required: this.required,
            suggested: this.suggested,
            weaklySuggested: this.weaklySuggested,
            deprecated: this.deprecated,
            aliases: this.aliases
        };
    }

    /**
     * Deserialize from simple object returned by JSON.parse or MW API.
     * We assume mediawiki API json formatversion=2 with the lang param set.
     *
     * @param {object} jsonObject
     * @param {string} [key] - canonical key to identify the param, if not already in jsonObject.
     * @returns {TemplateDataParam}
     */
    static fromJSON(jsonObject, key) {
        console.assert(!jsonObject.inherits); // Inherits should be handled by API.
        const canonicalKey = jsonObject.key || key;
        const result = Object.assign(new TemplateDataParam(canonicalKey), jsonObject);
        result.aliases = result.aliases || [];
        return result;
    }
}

/**
 * Configuration of a template.
 * See https://www.mediawiki.org/wiki/Help:TemplateData#Description_and_parameters
 *  or https://www.mediawiki.org/wiki/Extension:TemplateData
 */
export class TemplateData {
    /** @param {object} jsonObject - from mediawiki API json formatversion=2 with lang= set. */
    constructor(jsonObject) {
        /** @type {string} */
        this.title = jsonObject.title; // Added by API.
        /** @type {boolean} */
        this.notemplatedata = jsonObject.notemplatedata; // Added by API.
        /** @type {string} */
        this.description = jsonObject.description;
        /**
         * @type {string}
         * 'inline', 'block', or a string like '\n{{_\n|_______________ = _\n}}\n'.
         */
        this.format = jsonObject.format;
        /**
         * @type {Object<string, Object<string,(string|Array<string>|Array<Array<string>>)>>}
         * Maps names of consumers to maps from consumer-parameters to our-parameters.
         */
        this.maps = jsonObject.maps;
        /**
         * @type {Array<{label: string, params: Array<string>}>}
         * Sets (groups) of parameters. A parameter may be in multiple sets.
         * Labels are short, 20-ish characters.
         */
        this.sets = jsonObject.sets;
        /** @type {Map<string, TemplateDataParam>} */
        this.params = new Map();
        for (const [k, v] of Object.entries(jsonObject.params))
            this.params.set(k, TemplateDataParam.fromJSON(v, k));
        /** @type {!Array<string>} */
        this.paramOrder = jsonObject.paramOrder;
        // Should always be filled by API but apparently it's not.
        if (!this.paramOrder || !this.paramOrder.length)
            this.paramOrder = Object.keys(jsonObject.params);

        /** @private {Map<string, string>} - map from alias key to canonical key. */
        this.canonicalMap_ = new Map();
        for (const [canonicalKey, param] of this.params.entries()) {
            for (const aliasKey of param.aliases) {
                console.assert(!this.canonicalMap_.get(aliasKey));
                this.canonicalMap_.set(aliasKey, canonicalKey);
            }
        }
    }

    /**
     * Serialize to simple json object, called by JSON.stringify.
     *
     * @returns {object}
     */
    toJSON() {
        // Convert Map to Object (avoid importing polyfills just for three lines).
        const jsonParams = {};
        for (const [key, value] of this.params.entries())
            jsonParams[key] = value;
        return {
            title: this.title,
            notemplatedata: this.notemplatedata,
            description: this.description,
            format: this.format,
            maps: this.maps,
            sets: this.sets,
            params: jsonParams,
            paramOrder: this.paramOrder
        };
    }

    /**
     * Deserialize from simple object returned by JSON.parse or MW API.
     * We assume mediawiki API json formatversion=2 with the lang param set.
     *
     * @param {object} jsonObject
     * @returns {TemplateData}
     */
    static fromJSON(jsonObject) {
        return new TemplateData(jsonObject);
    }

    /**
     * Fetch given template's TemplateData via API.
     *
     * @param {string} name
     * @returns {Promise<TemplateData>}
     */
    static async fetch(name) {
        if (!name.startsWith('Template'))
            name = 'Template:' + name;
        const r = await (new mw.Api()).get({
            action: 'templatedata',
            titles: name,
            redirects: true,
            lang: mw.config.get('wgUserLanguage'),
            formatversion: 2
        });
        return new TemplateData(Object.values(r.pages)[0]);
    }

    /**
     * Map an alias key to the canonical parameter name.
     *
     * @param {string|number} key
     * @returns {string}
     */
    toCanonicalKey(key) {
        return this.canonicalMap_.get(key.toString().trim()) || key.toString().trim();
    }

    /**
     * Give TemplateDataParam for given canonical key or return default.
     *
     * @param {string} canonicalKey
     * @returns {TemplateDataParam}
     */
    param(canonicalKey) {
        // Don't save default param, because we check and report when a param has no TemplateData.
        // Freeze to avoid mistaken attempts to make a new param starting from default values.
        if (!this.params.has(canonicalKey))
            return Object.freeze(new TemplateDataParam(canonicalKey));
        return this.params.get(canonicalKey);
    }

    /** Reorder the keys of a Map according to this.paramOrder.
     * Keys not in this.paramOrder are then appended lexicographically.
     *
     * @param {Map<string, T>} map
     * @returns {Map<string, T>}
     * @template T
     */
    reorder(map) {
        const result = new Map();
        // First add keys in paramOrder.
        for (const key of this.paramOrder) {
            if (map.has(key))
                result.set(key, map.get(key));
        }
        // Then take remaining keys, sort, and add.
        const toBeSorted = [];
        for (const [key, value] of map.entries()) {
            if (!this.paramOrder.includes(key))
                toBeSorted.push([key, value]);
        }
        for (const [key, value] of toBeSorted.sort())
            result.set(key, value);

        return result;
    }

    /**
     * Format given key-value map as wikicode of the template.
     *
     * @param {Map<string, [(number|string), string]>} map - from canonicalKey to final key, value.
     * @param {string=} templateName - alias template name to use (defaults to this.title).
     * @param {boolean=} doReorder - whether to apply this.reorder(map); defaults to true.
     * @returns {string} wikicode, currently from '{{' to '}}' inclusive, no final endline.
     */
    build(map, templateName, doReorder) {
        // TODO Use this.format; make it easy to handle pre and post newlines.
        // As in https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/extensions/TemplateWizard/+/master/resources/ext.TemplateWizard.TemplateFormatter.js
        if (doReorder == null)
            doReorder = true;
        if (doReorder)
            map = this.reorder(map);
        if (!templateName)
            templateName = this.title;
        let result = '{{' + templateName.trim() + '\n';
        for (let [_canonicalKey, [key, value]] of map.entries()) {
            if (typeof key === 'string') {
                key = key.trim();
            } else if (typeof key === 'number') {
                key = key.toString();
            } else {
                console.log(`Error: unexpected key type "${typeof key}"`);
                continue;
            }
            if (typeof value === 'string')
                value = value.trim();
            else if (!value)
                value = '';
            result += '| ' + key.padEnd(14) + '= ' + value + '\n';
        }
        result += '}}';
        return result;
    }
}
// </nowiki>