User:Tokenzero/tinfoboxHelperData.js

From Wikipedia, the free encyclopedia
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.
/**
 * @module tinfoboxHelperData
 * Helper structures for infoboxJournal.js, storing info about parameter choices, messages, etc.
 */
import * as util from '/w/index.php?title=User:Tokenzero/tinfoboxUtil.js&action=raw&ctype=text%2Fjavascript';
import { TemplateData, TemplateDataParam } from '/w/index.php?title=User:Tokenzero/tinfoboxTemplateData.js&action=raw&ctype=text%2Fjavascript';

/** Structure for parameter values and choices. */
export class ParamChoice {
    /**
     * @param {object} templateData
     */
    constructor(templateData) {
        /**
         * @constant
         * @type {TemplateDataParam}
         * Note this is also included in TemplateChoice.templateData (possibly as a deep copy).
         */
        this.templateData = templateData;
        /** @type {?string} */
        this.originalKey = null;
        /** @type {?string} */
        this.originalValue = null;
        /** @type {?string} */
        this.proposedValue = null;
        /** @type {boolean} */
        this.preferOriginal = true;
        /** @type {Array<{type: string, message: string}>} */
        this.messages = [];
    }

    /**
     * Serialize to simple object to be passed do JSON.stringify.
     *
     * @returns {object}
     */
    toJSON() {
        return {
            templateData: this.templateData, // Recursively toJSON'ed by JSON.stringify.
            originalKey: this.originalKey,
            originalValue: this.originalValue,
            proposedValue: this.proposedValue,
            preferOriginal: this.preferOriginal,
            messages: this.messages
        };
    }

    /**
     * Deserialize from simple object returned by JSON.parse.
     *
     * @param {object} jsonObject
     * @returns {ParamChoice}
     */
    static fromJSON(jsonObject) {
        const templateData = TemplateDataParam.fromJSON(jsonObject.templateData);
        delete jsonObject.templateData;
        return Object.assign(new ParamChoice(templateData), jsonObject);
    }

    /**
     * Return whether proposed value is empty or equal to original, default or autovalue.
     *
     * @returns {boolean}
     */
    isProposedValueTrivial() {
        return (
            (!this.proposedValue) ||
            (this.proposedValue === this.originalValue) ||
            (this.proposedValue === this.templateData.default) ||
            (this.proposedValue === this.templateData.autovalue) ||
            (!this.proposedValue && !this.templateData.autovalue)
        );
    }
}


/** Data to preserve after redirecting: ParamChoice-s and messages. */
export class TemplateChoice {
    /** @param {TemplateData} templateData */
    constructor(templateData) {
        /**
         * @constant
         * @type {TemplateData}
         * Note that templateData for params are also included in ParamChoices,
         * possibly as a deep copy.
         */
        this.templateData = templateData;
        /** @type {Map<string, ParamChoice>} from canonicalKey to its ParamChoice. */
        this.paramChoices = new Map();
        /** @type {Array<{type: string, message: string}>} messages about this template instance. */
        this.messages = [];
    }

    /**
     * Get or create ParamChoice for given canonicalKey.
     *
     * @param {string} canonicalKey
     * @returns {ParamChoice}
     */
    param(canonicalKey) {
        if (!this.paramChoices.has(canonicalKey)) {
            const paramChoice = new ParamChoice(this.templateData.param(canonicalKey));
            this.paramChoices.set(canonicalKey, paramChoice);
        }
        return this.paramChoices.get(canonicalKey);
    }


    /**
     * Serialize to object to be passed do JSON.stringify.
     *
     * @returns {object}
     */
    toJSON() {
        // JSON.stringify will recursively call .toJSON() in each entry.
        return {
            templateData: this.templateData,
            paramChoices: util.objectFromEntries(this.paramChoices.entries()),
            messages: this.messages
        };
    }

    /**
     * Deserialize from object returned by JSON.parse.
     *
     * @param {object} jsonObject
     * @returns {TemplateChoice}
     */
    static fromJSON(jsonObject) {
        const templateData = TemplateData.fromJSON(jsonObject.templateData);
        const result = new TemplateChoice(templateData);
        result.paramChoices = new Map(Object.entries(jsonObject.paramChoices).map(
            ([key, value]) => [key, ParamChoice.fromJSON(value)]
        ));
        result.messages = jsonObject.messages;
        return result;
    }

    /**
     * Build table listing parameters with their choices and messages.
     *
     * @returns {JQuery<HTMLElement>|''}
     */
    buildParamTable() {
        const changedList = [];
        const proposedList = [];
        const weaklySuggestedList = [];
        const otherList = [];
        const choices = this.templateData.reorder(this.paramChoices).entries();
        for (const [canonicalKey, pc] of choices) {
            if (pc.proposedValue === pc.originalValue && !pc.messages.length) {
                if (pc.proposedValue && pc.proposedValue.replace(/<!--[^>]*-->/g, '')) {
                    console.log(
                        `Param ${canonicalKey} guessed correctly as "${pc.proposedValue}".`);
                }
                continue;
            }
            if (pc.originalValue === pc.proposedValue)
                pc.preferOriginal = true;

            const row = $('<tr>');
            row.append($(`<td>${pc.originalKey || canonicalKey}=</td>`));
            if (typeof pc.originalValue === 'string')
                row.append($(`<td>${util.escapeHTML(pc.originalValue)}</td>`));
            else
                row.append($('<td>(absent)</td>').addClass('absent'));
            if (pc.preferOriginal)
                row.children().last().addClass('selected');
            if (typeof pc.proposedValue === 'string')
                row.append($(`<td>${util.escapeHTML(pc.proposedValue)}</td>`));
            else if (!pc.preferOriginal)
                row.append($('<td>(deleted)</td>').addClass('absent'));
            else
                row.append($('<td></td>').addClass('absent'));
            if (!pc.preferOriginal)
                row.children().last().addClass('selected');
            if (!pc.isProposedValueTrivial())
                row.children().last().addClass('nontrivial');
            const messageTd = $('<td>');
            const tooltip = $(`<span
                class="ext-tinfobox-tooltip"
                title="${util.escapeHTML(pc.templateData.description)}"
            />`);
            if (pc.templateData.description)
                messageTd.append(tooltip);
            const messageWidget = HelperData.buildMessagesWidget(pc.messages);
            if (messageWidget !== '')
                messageTd.append(messageWidget);
            else
                messageTd.addClass('empty');
            row.append(messageTd);

            if (!pc.preferOriginal)
                changedList.push(row);
            else if (!pc.isProposedValueTrivial())
                proposedList.push(row);
            else if ((pc.templateData.suggested || pc.templateData.weaklySuggested) &&
                     pc.originalValue === null)
                weaklySuggestedList.push(row);
            else if (pc.messages.length)
                otherList.push(row);
            // Else: we prefer original, proposed value is trivial and not suggested as addition,
            // and there are no messages, so we just don't show the param.
        }
        let rows = [];
        rows.push($(`<tr>
            <th></th><th>current value</th><th>new value/suggested</th><th></th>
        </tr>`));
        const makeHeadRow = (t) => $('<tr><th colspan="3">' + t + '</th></tr>');
        if (changedList.length) {
            rows.push(makeHeadRow('<strong>Changed parameters</strong> (please fill empty ones)'));
            rows = rows.concat(changedList);
        } else {
            rows.push(makeHeadRow('No parameters were changed.'));
        }
        if (proposedList.length) {
            rows.push(makeHeadRow('<strong>Suggested changes</strong> (currently unchanged)'));
            rows = rows.concat(proposedList);
        }
        if (weaklySuggestedList.length) {
            rows.push(makeHeadRow('<strong>Additional parameters</strong>' +
                ' (situational, omit by default)'));
            rows = rows.concat(weaklySuggestedList);
        }
        if (otherList.length) {
            rows.push(makeHeadRow('Other warnings'));
            rows = rows.concat(otherList);
        }
        return $('<table>').append(rows);
    }
}

/**
 * Data to pass after redirect, including TemplateChoice-s.
 */
export class HelperData {
    /** Constructor. */
    constructor() {
        /** @type {Array<TemplateChoice>} */
        this.templateChoices = [];
        /** @type {Array<{type: string, message: string}>} global messages. */
        this.messages = [];
    }

    /**
     * Serialize to JSON string.
     *
     * @returns {string}
     */
    toJSONString() {
        return JSON.stringify({
            templateChoices: this.templateChoices,
            messages: this.messages
        });
    }

    /**
     * Deserialize from JSON string to new HelperData object.
     *
     * @param {string} json
     * @returns {HelperData}
     */
    static fromJSONString(json) {
        const result = Object.assign(new HelperData(), JSON.parse(json));
        result.templateChoices = result.templateChoices.map(
            (x) => TemplateChoice.fromJSON(x)
        );
        return result;
    }

    /**
     * Create jQuery object showing a list of messages.
     *
     * @param {Array<{type: string, message: string}>} messages
     * @returns {JQuery|''}
     */
    static buildMessagesWidget(messages) {
        if (!messages || !messages.length)
            return '';
        const result = $('<ul></ul>');
        for (const m of messages) {
            const entry = $('<li>');
            entry.text(m.message);
            entry.prepend(`<b>${m.type}</b>: `);
            result.append(entry);
        }
        return result;
    }

    /**
     * Build a box describing helperData (prefilled parameters and such).
     *
     * @returns {JQuery<HTMLElement>}
     */
    buildWidget() {
        const widget = $(`
            <div class="ext-tinfobox-helper">
                <h2>infoboxJournal.js</h2>
            </div>
        `);
        let globalMessages = this.messages;
        if (this.templateChoices.length === 1)
            globalMessages = globalMessages.concat(this.templateChoices[0].messages);
        widget.append(HelperData.buildMessagesWidget(globalMessages));

        const many = (this.templateChoices.length > 1);
        for (const [tcIndex, tc] of this.templateChoices.entries()) {
            if (many) {
                widget.append($(`<h3>Template #${tcIndex + 1}</h3>`));
                widget.append(HelperData.buildMessagesWidget(tc.messages));
            }
            widget.append(tc.buildParamTable());
        }

        return widget;
    }
}