User:Tokenzero/tinfoboxJournal.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/tinfoboxJournal. |
// <nowiki>
/**
* @module tinfoboxJournal
* The meat of the infoboxJournal.js user script.
*/
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';
import { TemplateChoice, HelperData } from '/w/index.php?title=User:Tokenzero/tinfoboxHelperData.js&action=raw&ctype=text%2Fjavascript';
/** Called at the end of this module. */
function main() {
if (mw.config.get('wgIsProbablyEditable')) {
mw.util.addPortletLink(
'p-cactions',
'#',
'Infobox journal',
'ca-infobox-journal',
'Add or normalize infobox-journals.'
);
$('#ca-infobox-journal').click(onClick);
}
/** If we have been redirected, session stores HelperData from which we make the widget. */
if (sessionStorage.getItem('tinfoboxHelperData')) {
const helperData = HelperData.fromJSONString(
sessionStorage.getItem('tinfoboxHelperData')
);
sessionStorage.removeItem('tinfoboxHelperData');
console.log(helperData);
helperData.buildWidget().insertBefore($('#wikiDiff, .mw-editform')[0]);
}
}
/**
* Executed on portletLink click.
*
* @param {JQuery.ClickEvent} event
*/
async function onClick(event) {
event.preventDefault();
// let util = await import('./tinfoboxUtil.js');
// let TinfoboxJournalModule = await import('./tinfoboxJournal.js');
// let fixInfoboxJournals = TinfoboxJournalModule.fixInfoboxJournals;
if (mw.config.get('wgAction') === 'edit' || mw.config.get('wgAction') === 'submit') {
const wikitext = /** @type {string} */($('#wpTextbox1').val());
const [newWikitext, summary, helperData] = await fixInfoboxJournals(wikitext);
$('#wpTextbox1').val(newWikitext);
if (!$('#wpSummary').val())
$('#wpSummary').val(summary);
helperData.buildWidget().insertBefore($('#wikiDiff, .mw-editform')[0]);
} else {
mw.notify('Standarizing infobox journals...', { type: 'info' });
const wikitext = await util.getWikitext(mw.config.get('wgPageName'));
const [newWikitext, summary, helperData] = await fixInfoboxJournals(wikitext);
sessionStorage.setItem('tinfoboxHelperData', helperData.toJSONString());
await util.redirectToPreviewDiff(newWikitext, summary);
}
}
/**
* Normalize or add infobox-journal templates.
*
* Reorders and reformats existing infoboxes, or adds a new pre-formatted;
* also tries to pre-fill some values; removes redundant '{{italic title}}'.
*
* @param {string} wikitext
* @returns {Promise<[string, string, HelperData]>} Modified wikitext, edit summary, HelperData.
*/
export async function fixInfoboxJournals(wikitext) {
// Alternative names of {{infobox journal}}:
const ijNames = ['infobox journal', 'infobox academic journal', 'journal',
'journal infobox', 'infobox serial publication', 'infobox journal series'];
// const _imNames = ['infobox magazine', 'infobox periodical', 'infobox publication',
// 'infobox pulps'];
const templateData = await getInfoboxJournalTemplateData();
// Alternative names of {{italic title}}
const italicNames = ['italic title', 'ital', 'italic', 'italic title infobox',
'italics', 'italics title', 'italicstitle', 'italictitle', 'title italic',
'italicised title', 'italicisedtitle', 'italicize title',
'italicized title', 'italicizedtitle', 'italicizetitle'];
let result = wikitext;
let summary = 'with ([[User:Tokenzero/infoboxJournal|infoboxJournal.js]])';
const helperData = new HelperData();
const ijTemplates = [];
const templates = window.extraJs.parseTemplates(wikitext, false); // Non-recursive.
for (const t of templates) {
const tName = t.name.trim().toLowerCase();
if (ijNames.includes(tName)) {
ijTemplates.push(t);
} else if (italicNames.includes(tName)) {
// Remove the 'italic' template together with whitespace/newline after it.
const regex = new RegExp(mw.util.escapeRegExp(t.wikitext) + '\\ *\\n?');
result = result.replace(regex, '');
}
}
if (ijTemplates.length === 0) {
summary = 'Adding infobox journal ' + summary;
const ijt = new window.extraJs.Template('{{Infobox journal}}');
ijt.setName('Infobox journal');
const [infobox, templateChoice] =
await rebuildInfoboxJournal(ijt, wikitext, templateData);
helperData.templateChoices.push(templateChoice);
// Place after any initial template-only lines.
const index = result.match(/^( *({{.*}} *)*\n)*/m)[0].length;
result = result.slice(0, index) + infobox + '\n' + result.slice(index);
} else {
summary = 'Standardizing infobox journal ' + summary;
for (const t of ijTemplates) {
const [infobox, templateChoice] =
await rebuildInfoboxJournal(t, wikitext, templateData);
helperData.templateChoices.push(templateChoice);
// Remove whitespace with at most one new line and always add one newline.
const regex = new RegExp(mw.util.escapeRegExp(t.wikitext) + '\\ *\\n?');
result = result.replace(regex, infobox + '\n');
}
if (ijTemplates.length > 1) {
helperData.messages.push({
type: 'warning',
message: 'More than one infobox found, use at your own risk.'
});
}
}
return [result, summary, helperData];
}
/**
* Rebuild wikicode for given infobox-journal Template.
*
* @param {ExtraJs.Template} ijt - Template object for current infobox
* @param {string} wikitext - wikitext of whole page (used for prefilling params)
* @param {TemplateData} templateData - the TemplateData object for infobox-journal.
* @returns {Promise<[string, TemplateChoice]>} new wikicode for the infobox, from '{{' to '}}'
*/
async function rebuildInfoboxJournal(ijt, wikitext, templateData) {
const templateChoice = new TemplateChoice(templateData);
// For 'suggested' parameters, propose their 'autovalue'.
// (Don't suggest the 'default', which is the value assumed when none is given).
for (const param of templateData.params.values()) {
if (param.suggested || param.weaklySuggested) {
// Suggest with empty string if no 'autovalue' given (instead of suggesting deletion).
templateChoice.param(param.key).proposedValue = param.autovalue || '';
// Prefer original value if weaklySuggested and prefer suggested value otherwise
// (unless anything appears in ijt.parameters later).
templateChoice.param(param.key).preferOriginal = param.weaklySuggested;
}
}
// Load original parameters.
for (const p of ijt.parameters) {
const canonicalKey = templateData.toCanonicalKey(p.name);
// Report duplicate parameters.
if (templateChoice.param(canonicalKey).originalKey) {
templateChoice.param(canonicalKey).messages.push({
type: 'warning',
message: `Deleted duplicate of "${canonicalKey}" parameter:` +
` "|${p.name}=${p.value}".`
});
continue;
}
templateChoice.param(canonicalKey).originalKey = p.name;
templateChoice.param(canonicalKey).originalValue = p.value;
templateChoice.param(canonicalKey).preferOriginal = true;
// Report unexpected parameters.
if (!templateData.params.has(canonicalKey)) {
// Ignore and skip empty unnamed parameter, someone just wrote one '|' too many.
if ((!canonicalKey || typeof canonicalKey === 'number') &&
util.isTrivialString(p.value))
continue;
templateChoice.param(canonicalKey).messages.push({
type: 'notice',
message: 'No TemplateData for this param.'
});
}
}
// Prefills overwrite autovalues, but mostly keep preferOriginal true.
const notification = mw.notify('Querying categories (this might take a few seconds)...',
{ type: 'info', autoHideSeconds: 30 });
await prefillParameters(templateChoice, wikitext);
// Notify that time-consuming ajax requests are finished.
notification.then((n) => n.close());
if (mw.config.get('wgAction') === 'edit' || mw.config.get('wgAction') === 'submit')
mw.notify('Done.', { type: 'info' });
else
mw.notify('Redirecting...', { type: 'info', autoHideSeconds: 10 });
for (const p of templateChoice.paramChoices.values()) {
if (p.proposedValue === p.templateData.default)
p.proposedValue = '';
}
for (const p of ijt.parameters) {
const canonicalKey = templateData.toCanonicalKey(p.name);
// If original value was absent, empty, equal to the default, or the param is deprecated,
// then always prefer the proposed value (prefill, autovalue or '' if only suggested),
// even if proposed is null (which will delete the parameter).
// TODO evaluate template substitutions in autovalue.
if (util.isTrivialString(p.value) ||
p.value.trim() === templateData.param(canonicalKey).default ||
p.value.trim() === templateData.param(canonicalKey).autovalue ||
templateData.param(canonicalKey).deprecated) {
templateChoice.param(canonicalKey).preferOriginal = false;
// Except if original value was not absent and proposed value is trivial.
// E.g. empty params won't be replaced with default comments.
if (p.value !== null && templateChoice.param(canonicalKey).isProposedValueTrivial())
templateChoice.param(canonicalKey).preferOriginal = true;
// Same if original value was a comment (other than autovalue), but notify.
} else if (!p.value.replace(/<!--[^>]*-->/g, '').trim()) {
/* templateChoice.param(canonicalKey).messages.push({
type: 'notice',
message: 'Replacing unexpected comment.'
}); */
templateChoice.param(canonicalKey).preferOriginal = false;
}
// Otherwise we prefer the original by default.
// Some prefills might have been strong enough to immediately prefer themselves, though.
}
// Format the template wikicode.
const finalMap = new Map();
for (const [canonicalKey, paramChoice] of templateChoice.paramChoices.entries()) {
const key = paramChoice.originalKey || canonicalKey; // Prefer preserving original key.
const value = paramChoice.preferOriginal
? paramChoice.originalValue
: paramChoice.proposedValue;
if (value != null)
finalMap.set(canonicalKey, [key, value]);
}
const result = templateData.build(finalMap, ijt.name.trim());
return [result, templateChoice];
}
/**
* Try to pre-fill some parameters e.g. based on categories.
*
* @param {TemplateChoice} data
* @param {string} wikitext
*/
async function prefillParameters(data, wikitext) {
// Title
data.param('title').proposedValue = mw.config.get('wgTitle').replace(/\s+\(.+/, '');
// Prefills based on categories.
let categories = [];
if (mw.config.get('wgAction') === 'view')
categories = mw.config.get('wgCategories'); // Includes categories from templates.
else
categories = util.parseCategories(wikitext); // Does not include categories from templates.
// Another option is await (new mw.Api()).getCategories(mw.config.get('wgPageName'));
// But this does not include current editor changes.
let historyStart = '';
let historyEnd = 'present';
for (const category of categories) {
// Language
let match = category.match(/(.+)-language (journal|magazine)/);
if (match) {
let language = data.param('language').proposedValue;
if (language)
language += ', ' + match[1];
else
language = match[1];
data.param('language').proposedValue = language;
}
// Frequency
const frequencyMap = new Map([
['continuous', 'Continuous'],
['weekly', 'Weekly'],
['biweekly', 'Biweekly'],
['bi-weekly', 'Biweekly'],
['fortnightly', 'Fortnightly'],
['monthly', 'Monthly'],
['bimonthly', 'Bimonthly'],
['bi-monthly', 'Bimonthly'],
['semimonthly', 'Semimonthly'],
['semi-monthly', 'Semimonthly'],
['annual', 'Annually'],
['biannual', 'Biannually'],
['bi-annual', 'Biannually'],
['triannual', 'Triannually'],
['tri-annual', 'Triannually'],
['quarterly', 'Quarterly'],
['irregular', 'Irregular'],
['irregularly published', 'Irregular'],
['8 times per year', '8/year'],
['eight times annually', '8/year'],
['nine times annually', '9/year'],
['ten times annually', '10/year'],
['10 times per year', '10/year'],
['36 times per year', '36/year']
]);
for (const [pattern, value] of frequencyMap) {
const regex = new RegExp(
'(\\s|^)' + mw.util.escapeRegExp(pattern) + '\\s(journals|magazines)',
'i'
);
if (regex.test(category))
data.param('frequency').proposedValue = value;
}
// Publisher
match = category.match(/^(.+) academic journals$/);
if (match) {
const ancestor = /^Academic journals by publisher/;
if (await util.isCategoryChildOf(category, ancestor, /journal/, 9)) {
const newPublisher = '[[' + match[1] + ']]';
let publisher = data.param('publisher').proposedValue;
if (data.param('publisher').isProposedValueTrivial())
publisher = newPublisher;
else
publisher += ', ' + newPublisher;
data.param('publisher').proposedValue = publisher;
}
}
// Discipline
match = category.match(/^(.+) journals$/);
if (match) {
const ancestor = /^Academic journals by subject area/;
if (await util.isCategoryChildOf(category, ancestor, /journal/, 9)) {
let newDiscipline = match[1];
if (await util.pageExists(newDiscipline))
newDiscipline = '[[' + newDiscipline + ']]';
let discipline = data.param('discipline').proposedValue;
if (data.param('discipline').isProposedValueTrivial())
discipline = newDiscipline;
else
discipline += ', ' + newDiscipline;
data.param('discipline').proposedValue = discipline;
}
}
// History
match = category.match(/^(Publications|Magazines|Academic journals|Newspapers) established in ([0-9]+)$/);
if (match)
historyStart = match[2];
match = category.match(/^(Publications|Magazines|Academic journals|Newspapers) disestablished in ([0-9]+)$/);
if (match)
historyEnd = match[2];
if (historyEnd === 'present' && /^Defunct journals/.test(category))
historyEnd = '?';
// Open access
const ancestor = /^Open access journals/;
if (category === 'Delayed open access journals')
data.param('openaccess').proposedValue = '[[Delayed open access journal|Delayed]]';
else if (category === 'Hybrid open access journals')
data.param('openaccess').proposedValue = '[[Hybrid open access journal|Hybrid]]';
else if (!data.param('openaccess').proposedValue &&
!category.includes('Commons') &&
category.endsWith('journals') &&
await util.isCategoryChildOf(category, ancestor, /journals/, 1))
data.param('openaccess').proposedValue = 'Yes';
}
if (historyStart || historyEnd !== 'present')
data.param('history').proposedValue = historyStart + '–' + historyEnd;
// TODO avoid 'present' if in Category:Defunct journals/periodicals.
// Prefills based on templates.
const templates = window.extraJs.parseTemplates(wikitext, false); // Non-recursive.
for (const template of templates) {
// Website
const tNames = ['companywebsite', 'homepage', 'mainwebsite', 'official', 'officialhomepage',
'offficialwebsite', 'officialwebsite', 'officialwebpage', 'officialsite'];
if (tNames.includes(template.name.toLowerCase().replace(/\s/g, ''))) {
let url = template.getParam('1') ||
template.getParam('url') ||
template.getParam('URL');
if (!url)
continue; // TODO use wikidata "official website" Property (P856).
url = url.value.trim();
if (!/\/\//.test(url))
url = 'http://' + url;
data.param('website').proposedValue = url;
}
// ISSN, eISSN
if (template.name.toLowerCase() === 'issn') {
for (const param of template.parameters) {
if (typeof param.name === 'number') {
if (!data.param('ISSN').proposedValue) {
data.param('ISSN').proposedValue = param.value;
} else if (data.param('ISSN').proposedValue !== param.value) {
data.param('ISSN').messages.push({
type: 'warning',
message: `Found another: ${param.value}`
});
}
}
}
}
if (template.name.toLowerCase() === 'eissn') {
for (const param of template.parameters) {
if (typeof param.name === 'number') {
if (!data.param('eISSN').proposedValue) {
data.param('eISSN').proposedValue = param.value;
} else if (data.param('eISSN').proposedValue !== param.value) {
data.param('eISSN').messages.push({
type: 'warning',
message: `Found another: ${param.value}.`
});
}
}
}
}
}
// History dashes - always change to ndash.
if (!util.isTrivialString(data.param('history').originalValue)) {
let value = data.param('history').originalValue;
value = value.replace(/\s*(-|—|‑|‒|–|—|-|—|—|to)\s*/g, '–');
if (!data.param('history').isProposedValueTrivial() &&
data.param('history').proposedValue !== value) {
data.param('history').messages.push({
type: 'notice',
message: `Categories suggest "${data.param('history').proposedValue}" instead.`
});
}
data.param('history').proposedValue = value;
data.param('history').preferOriginal = false;
}
}
/**
* Get config of how the infobox should be rebuilt.
*
* @returns {Promise<TemplateData>}
*/
async function getInfoboxJournalTemplateData() {
const templateData = await TemplateData.fetch('Template:Infobox journal');
console.log('Fetched TemplateData:', templateData);
templateData.paramOrder = [
'title',
'italic title',
'image', 'image_size', 'alt',
'caption',
'former_name',
'abbreviation',
'bluebook',
'mathscinet',
'nlm',
'bypass-rcheck',
'discipline',
'peer-reviewed',
'language',
'editor',
'publisher',
'country',
'history',
'frequency',
'openaccess', 'license',
'impact', 'impact-year',
'ISSNlabel', 'ISSN', 'eISSN', 'CODEN', 'JSTOR', 'LCCN', 'OCLC',
'ISSN2label', 'ISSN2', 'eISSN2', 'CODEN2', 'JSTOR2', 'LCCN2', 'OCLC2', // Added
'ISSN3label', 'ISSN3', 'eISSN3', 'CODEN3', 'JSTOR3', 'LCCN3', 'OCLC3', // Added
'ISSN4label', 'ISSN4', 'eISSN4', 'CODEN4', 'JSTOR4', 'LCCN4', 'OCLC4', // Added
'ISSN5label', 'ISSN5', 'eISSN5', 'CODEN5', 'JSTOR5', 'LCCN5', 'OCLC5', // Added
'ISSN6label', 'ISSN6', 'eISSN6', 'CODEN6', 'JSTOR6', 'LCCN6', 'OCLC6', // Added
'ISSN7label', 'ISSN7', 'eISSN7', 'CODEN7', 'JSTOR7', 'LCCN7', 'OCLC7', // Added
'ISSN8label', 'ISSN8', 'eISSN8', 'CODEN8', 'JSTOR8', 'LCCN8', 'OCLC8', // Added
'ISSN9label', 'ISSN9', 'eISSN9', 'CODEN9', 'JSTOR9', 'LCCN9', 'OCLC9', // Added
'website',
'link1', 'link1-name',
'link2', 'link2-name',
'link3', 'link3-name', // Added
'link4', 'link4-name', // Added
'link5', 'link5-name', // Added
'boxwidth', 'RSS', 'atom'
];
const addSuggested = ['ISSNlabel', 'link2', 'link2-name'];
for (const paramName of addSuggested)
templateData.param(paramName).suggested = true;
const weaklySuggested = ['bluebook', 'mathscinet', 'nlm',
'peer-reviewed', 'image_size', 'alt', 'caption', 'ISSNlabel'];
for (const paramName of weaklySuggested) {
templateData.param(paramName).suggested = false;
templateData.param(paramName).weaklySuggested = true;
}
return templateData;
}
main();
// </nowiki>