Jump to content

User:Tokenzero/tinfoboxHelperData.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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;
    }
}