User:Tokenzero/tinfoboxHelperData.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Tokenzero/tinfoboxHelperData. |
/**
* @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;
}
}