User:Gary/nominations viewer.js: Difference between revisions

Content deleted Content added
make fixes for vector 2022 skin
Import a better version of this script.
Tag: Replaced
 
Line 1:
importScript('User:A455bcd9/nominations viewer.js');
// <nowiki>
// Nominations Viewer
//
// Description: Compact nominations for [[WP:FAC]], [[WP:FAR]], [[WP:FLC]],
// [[WP:FLRC]], [[WP:FPC]], and [[WP:PR]].
// Documentation: [[Wikipedia:Nominations Viewer]]
//
// ===
//
// Settings
// ---
//
// Default:
//
// NominationsViewer =
// {
// 'enabledPages': ['Wikipedia:Featured article candidates', ...],
// 'nominationData': ['images', 'age', 'nominators', 'participants', 'votes'],
// }
$(() => {
// Check the URL to determine if this script should be disabled.
if (window.location.href.includes('&disable=nomviewer')) {
return;
}
 
// Check if already ran elsewhere.
if (window.nominationsViewer) {
return;
}
 
window.nominationsViewer = true;
 
const NominationsViewer = window.NominationsViewer || {};
 
if (!NominationsViewer.enabledPages) {
NominationsViewer.enabledPages = {
'User:Gary/sandbox': 'nominations',
 
'Wikipedia:Featured article candidates': 'nominations',
'Wikipedia:Featured article review': 'reviews',
 
'Wikipedia:Featured list candidates': 'nominations',
'Wikipedia:Featured list removal candidates': 'reviews',
 
'Wikipedia:Featured picture candidates': 'pictures',
 
'Wikipedia:Peer review': 'peer reviews',
};
}
 
if (!NominationsViewer.nominationData) {
NominationsViewer.nominationData = [
'images',
'age',
'nominators',
'participants',
'votes',
];
}
 
/**
* Add empty nomination data holders for a nomination.
*
* @param {string} pageName Name of the nomination page.
* @param {jQuery} $parentNode Parent node containing the entire nomination.
* @param {Array} ids The ID names to create.
* @returns {jQuery} The new node we added.
*/
function addNominationData(pageName, $parentNode, ids) {
return ids.map((id) => {
const $span = $(`<span id="${id}-${simplifyPageName(pageName)}"></span>`);
 
return $parentNode.children().last().before($span);
});
}
 
function addAllNomInfo($headings) {
const data = { allH3Length: $headings.length };
 
const $expandAllLink = $(
'<a href="#" id="expand-all-link">expand all</a>'
).on('click', data, expandAllNoms);
const $collapseAllLink = $(
'<a href="#" id="collapse-all-link">collapse all</a>'
).on('click', data, collapseAllNoms);
const $info = $('<span class="overall-controls"></span>')
.append(' (')
.append($expandAllLink)
.append(' / ')
.append($collapseAllLink)
.append(')');
 
$headings
.first()
// Get the next node. This is needed, so that `.prevUntil` doesn't return
// an empty array.
.next()
.prevUntil('.mw-heading2')
.last()
.prev()
.find('h2')
.first()
.append($info);
}
 
/**
* Call the Wikipedia API with params then run a function on the return data.
*
* @param {Object} params The params to pass to the Wikipedia API.
* @param {Function} callback The function to run with the return data.
* @returns {undefined}
*/
function addNomData(params, callback) {
$.getJSON(mw.util.wikiScript('api'), {
format: 'json',
...params,
})
.done(callback)
.fail(() => {});
}
 
/**
* Add all data to a nomination.
*
* @param {string} pageName The page name.
* @returns {undefined}
*/
function addAllNomData(pageName) {
// Participants, age. Get all the edits for this nomination.
addNomData(
{
action: 'query',
prop: 'revisions',
rvdir: 'newer',
rvlimit: 500,
titles: pageName,
},
allRevisionsCallback
);
 
// Images, nominators, votes. Get the contents of the latest version of this
// nomination.
addNomData(
{
action: 'query',
prop: 'revisions',
rvdir: 'older',
rvlimit: 1,
rvprop: 'content',
titles: pageName,
},
currentRevisionCallback
);
}
 
/**
* Add data to a nomination.
*
* @param {Object} options Options
* @param {string} options.pageName The page name to which to add this data.
* @param {string} options.data The data to add.
* @param {string} options.id The ID of the field to add to.
* @param {string} options.hoverText Data that appears on hover.
* @returns {undefined}
*/
function addNewNomData({ pageName, data, id, hoverText }) {
if (!data) {
return;
}
 
// Select the element we want to add values to.
const $id = $(`#${id}-${simplifyPageName(pageName)}`);
const $newChild = $('<span class="nomv-data"></span>');
const $abbr = $(`<abbr title="${hoverText}">${data}</abbr>`);
 
$newChild.append($abbr);
$id.append($newChild);
}
 
/**
* Create the data that appears next to the nomination's listing.
*
* @param {string} pageName Page name of the nomination page.
* @returns {jQuery} The new node we added.
*/
function createData(pageName) {
const $newSpan = $('<span class="nomination-data"></span>').append(
'<span>(<span>'
);
const matchArchiveNumber = pageName.match(/(\d+)$/);
 
const conditions = matchArchiveNumber && matchArchiveNumber[1] > 1;
const matchArchiveNumberPrint = (() => {
if (conditions) {
const number = Number.parseInt(matchArchiveNumber[1], 10);
 
const ordinalSuffix = (() => {
switch (number) {
case 2:
return 'nd';
case 3:
return 'rd';
default:
return 'th';
}
})();
 
return `: ${number}${ordinalSuffix}`;
}
 
return '';
})();
 
const $viewLink = $(
`<span><a href="${mw.util.getUrl(pageName)}">nomination</a>\
${matchArchiveNumberPrint}</span>`
);
 
return $newSpan.append($viewLink).append('<span>)<span>');
}
 
function createNewNode({ oldNode, showHideLink, newSpan, index }) {
const $newNode = $(`<div id="nom-title-${index}"></div>`).append(
oldNode.clone(true)
);
const $heading = $newNode.children().first();
 
$heading
.prepend(`<span class="nomination-order">${index + 1}.</span> `)
.append(' ')
.append(showHideLink)
.append(newSpan);
 
return $newNode;
}
 
/**
* Replace a nomination with a new and improved one.
*
* @param {Object} options Options
* @param {jQuery} options.$h3 The h3 heading of the nomination.
* @param {number} options.index The index of the nomination among the
* others.
* @returns {undefined}
*/
function createNomination({ $h3, index }) {
// Get edit links. It has to be an edit link, and not an article link,
// because it has to point to the nomination page, not the article.
const $editLinks = $h3.find('.mw-editsection a');
 
// There are no edit links.
if ($editLinks.length === 0) {
return;
}
 
const titleRegex = /[&?]title=(.*?)(?:&|$)/;
 
// Find the edit link that matches our regex.
const $filteredEditLinks = $editLinks.filter((elementIndex, element) =>
$(element).attr('href').match(titleRegex)
);
 
// Only continue if there are filtered edit links. They won't appear when a
// Peer Review is "too long" and therefore is replaced with a message to go
// to the review page directly. So, skip this nomination.
if (
$filteredEditLinks.length === 0 ||
!$filteredEditLinks.eq(0).attr('href') ||
!$filteredEditLinks.eq(0).attr('href').match(titleRegex)
) {
return;
}
 
// Get the name of the nomination page.
const pageName = decodeURIComponent(
$filteredEditLinks.eq(0).attr('href').match(titleRegex)[1]
);
 
// Create the [show] / [hide] link.
const showHideLink = createShowHideLink(index);
 
// Create the spot to put the data that we will retrieve via the Wikipedia
// API.
const newSpan = createData(pageName);
 
// Move the nomination into a hidden node.
hideNomination($h3, index);
 
// Add placeholders for the data that we will retrieve for the API.
addNominationData(pageName, newSpan, NominationsViewer.nominationData);
 
// Create the nomination's title line.
const newNode = createNewNode({
oldNode: $h3,
showHideLink,
newSpan,
index,
});
 
// Create the actual nomination
const nomDiv = generateNomination(index, newNode, $h3);
 
// Replace this nomination with the new one we created.
$h3.replaceWith(nomDiv);
 
// Ask the API to add data to our placeholders.
addAllNomData(pageName);
}
 
function createShowHideLink(index) {
const span = $('<span class="nomv-show-hide"></span>');
const link = $(`<a href="#" id="nom-button-${index}">show</a>`).on(
'click',
{ index },
toggleNomClick
);
 
return span.append('[').append(link).append(']');
}
 
function generateNomination(index, newNode, oldNode) {
return $(`<div class="nomination" id="nom-${index}"></div>`)
.append(newNode.clone(true))
.append($(oldNode[0].nextSibling).clone(true));
}
 
// This function MUST stay in JavaScript, rather than switch to jQuery, for
// optmization reasons.
//
// The jQuery version slowed the page down by about 28%. This version slows
// the page down by about 11%, so it is about 17% faster.
function hideNomination($h3, index) {
// Re-create all nodes between this H3 node, and the next one, then place it
// into a new node.
const hiddenNode = document.createElement('div');
 
hiddenNode.className = 'nomination-body';
hiddenNode.id = `nom-data-${index}`;
hiddenNode.style.display = 'none';
 
let nomNextSibling = $h3[0].nextSibling;
 
// Continue to the next node, as long as the next node still exists, it
// isn't an H2 or H3, and it doesn't have the class "printfooter"
while (
nomNextSibling &&
!(
['H2', 'H3'].includes(nomNextSibling.nodeName) ||
(nomNextSibling.childNodes &&
nomNextSibling.childNodes.length > 1 &&
['H2', 'H3'].includes(nomNextSibling.childNodes[1].nodeName))
) &&
!(
nomNextSibling.classList &&
nomNextSibling.classList.contains('printfooter')
)
) {
const nomNextSiblingTemporary = nomNextSibling.nextSibling;
 
// Move the node, if it isn't a text node
if (nomNextSibling.nodeType !== 3) {
hiddenNode.append(nomNextSibling);
}
 
nomNextSibling = nomNextSiblingTemporary;
}
 
// Insert hidden content
return $h3.after(hiddenNode);
}
 
/**
* The main function, to run the script.
*
* @returns {undefined}
*/
function init() {
let currentPageIsASubpage;
let currentPageIsEnabled;
const pageName = mw.config.get('wgPageName');
 
// Check if enabled on this page
Object.keys(NominationsViewer.enabledPages).forEach((page) => {
if (pageName === page.replace(/\s/g, '_')) {
currentPageIsEnabled = true;
} else if (pageName.startsWith(page.replace(/\s/g, '_'))) {
currentPageIsASubpage = true;
}
});
 
if (
!currentPageIsEnabled ||
mw.config.get('wgAction') !== 'view' ||
window.location.href.includes('&oldid=') ||
currentPageIsASubpage
) {
return;
}
 
// Append the CSS now, since we're definitely running the script on this
// page.
addCss();
 
const $parentNode = $('.mw-content-ltr');
const $h3s = $parentNode.find('h3');
 
addAllNomInfo($h3s);
 
// Loop through each nomination
$h3s.each((index, element) =>
createNomination({
$h3: $(element),
index,
})
);
 
// Fix any conflicts with collapsed comments (using the special template).
$('.collapseButton').each((index, element) => {
const $link = $(element).children().first();
 
const newIndex = $link
.attr('id')
.slice(
$link.attr('id').indexOf('collapseButton') + 'collapseButton'.length,
$link.attr('id').length
);
 
$link.attr('href', '#').on('click', { newIndex }, collapseTable);
});
}
 
// Helpers
function collapseTable(event) {
event.preventDefault();
 
const tableIndex = event.data.index;
const collapseCaption = 'hide';
const expandCaption = 'show';
 
const $button = $(`#collapseButton${tableIndex}`);
const $table = $(`#collapsibleTable${tableIndex}`);
 
if ($table.length === 0 || $button.length === 0) {
return false;
}
 
const $rows = $table.find('> tbody > tr');
 
if ($button.text() === collapseCaption) {
$rows.each((index, element) => {
if (index === 0) {
return true;
}
 
return $(element).hide();
});
 
return $button.text(expandCaption);
}
 
$rows.each((index, element) => {
if (index === 0) {
return true;
}
 
return $(element).show();
});
 
return $button.text(collapseCaption);
}
 
// Add CSS to the page, to use for this script. This is a separate function,
// so that it's more easy to disable it when necessary.
function addCss() {
mw.util.addCSS(`
#content .nomination h3 {
margin-bottom: 0;
padding-top: 0;
}
 
.nomination-data,
.nomination-order,
.overall-controls {
font-size: 75%;
font-weight: normal;
}
 
.nomination-order {
display: inline-block;
width: 25px;
}
 
.nomv-show-hide {
display: inline-block;
font-size: 13px;
font-weight: normal;
margin-right: 2.5px;
width: 40px;
}
 
.nomv-show-hide a {
display: inline-block;
text-align: center;
width: 31px;
}
 
.nomv-data::before {
content: " · ";
}
 
.nomv-data abbr {
white-space: nowrap;
}
`);
}
 
function expandAllNoms(event) {
return toggleAllNoms(event, 'expand');
}
 
function collapseAllNoms(event) {
return toggleAllNoms(event, 'collapse');
}
 
function toggleAllNoms(event, actionParam) {
let action = actionParam;
 
if (!action) {
action = 'expand';
}
 
event.preventDefault();
 
const { allH3Length } = event.data;
 
new Array(allH3Length).fill().forEach((value, index) => {
toggleNom(index, action);
});
}
 
function toggleNom(id, actionParam) {
let action = actionParam;
 
if (!action) {
action = '';
}
 
const toggleHideNom = ($node, $nomButton) => {
$node.hide();
 
return $nomButton.text('show');
};
 
const toggleShowNom = ($node, $nomButton) => {
$node.show();
 
return $nomButton.text('hide');
};
 
const $node = $(`#nom-data-${id}`);
const $nomButton = $(`#nom-button-${id}`);
 
// These are actions that override the status for all nominations.
if (action === 'collapse') {
return toggleHideNom($node, $nomButton);
}
 
if (action === 'expand') {
return toggleShowNom($node, $nomButton);
}
 
// These have to be separate from the above because they have a lower
// priority.
if ($node.is(':visible')) {
return toggleHideNom($node, $nomButton);
}
 
if ($node.is(':hidden')) {
return toggleShowNom($node, $nomButton);
}
 
return null;
}
 
function toggleNomClick(event) {
event.preventDefault();
 
const { index } = event.data;
 
return toggleNom(index);
}
 
// Callbacks
function addParticipants(revisions, pageName, queryContinue) {
if (!dataIsEnabled('participants') || !revisions) {
return;
}
 
const users = {};
let userCount = 0;
 
revisions.forEach((revision) => {
if (!revision.user) {
return;
}
 
if (users[revision.user]) {
users[revision.user] += 1;
} else {
users[revision.user] = 1;
userCount += 1;
}
});
 
const moreThan = queryContinue ? 'more than ' : '';
const usersArray = Object.keys(users).map((user) => [
user,
Number.parseInt(users[user], 10),
]);
 
const usersArray2 = [...usersArray]
.sort((a, b) => {
if (a[1] < b[1]) {
return 1;
}
 
if (a[1] > b[1]) {
return -1;
}
 
return 0;
})
.map((user) => `${user[0]}: ${user[1]}`);
 
addNewNomData({
pageName,
data: `${moreThan + userCount} ${pluralize('participant', userCount)}`,
id: 'participants',
hoverText: `Sorted from most to least edits&#10;Total edits: ${
revisions.length
}&#10;Format: &quot;editor: \
number of edits&quot;:&#10;&#10;${usersArray2.join('&#10;')}`,
});
}
 
function allRevisionsCallback(object) {
const variables = formatJSON(object);
 
if (!variables) {
return;
}
 
// Participants
addParticipants(
variables.revisions,
variables.pageName,
object['query-continue']
);
 
// Nomination age
addAge(variables.firstRevision, variables.pageName);
}
 
function addImagesCount(content, pageName) {
if (!nomType('pictures') || !dataIsEnabled('images')) {
return;
}
 
// Determine number of images in the nomination
const pattern1 = /\[\[(file|image):.*?]]/gi;
const pattern2 = /\n(file|image):.*\|/gi;
const matches1 = content.match(pattern1);
const matches2 = content.match(pattern2);
 
const matches = matches1 || matches2 || [];
const images = matches.map((match) => {
const split = match.split('|');
const filename = $.trim(split[0].replace(/^\[\[/, ''));
 
return filename;
});
 
addNewNomData({
pageName,
data: `${matches.length} ${pluralize('image', matches.length)}`,
id: 'images',
hoverText: `Images (in order of appearance):&#10;&#10;${images.join(
'&#10;'
)}`,
});
}
 
function getNominators(content) {
let nomTypeText = '';
let listOfNominators = {};
 
switch (nomType()) {
case 'nominations':
nomTypeText = 'nominator';
listOfNominators = findNominators(content, /Nominator(\(s\))?:.*/);
 
// No nominators were found, so try once more with a different pattern.
if ($.isEmptyObject(listOfNominators)) {
listOfNominators = findNominators(content, /:<small>''.*/);
}
 
break;
case 'reviews':
nomTypeText = 'notification';
listOfNominators = findNominators(content, /(Notified|Notifying):?.*/);
 
break;
case 'pictures':
nomTypeText = 'nominator';
listOfNominators = findNominators(
content,
/\* '''Support as nominator''' – .*/
);
 
break;
default:
}
 
return { listOfNominators, nomTypeText };
}
 
function addNominators(content, pageName) {
if (!dataIsEnabled('nominators') || nomType('peer reviews')) {
return;
}
 
const { listOfNominators, nomTypeText } = getNominators(content);
let allNominators = Object.keys(listOfNominators)
.map((n) => n)
.sort();
 
let data;
 
if (allNominators.length > 0) {
data = `${allNominators.length} ${pluralize(
nomTypeText,
allNominators.length
)}`;
 
// We couldn't identify any nominators.
} else {
// Use the first username on the page to determine the nominator.
const matches = content.match(/\[\[User:(.*?)[\]|]/);
 
if (nomType('nominations') && matches) {
allNominators = [matches[1]];
data = `${allNominators.length} ${pluralize(
nomTypeText,
allNominators.length
)}`;
 
// This is not a nomination-type, and we couldn't find any relevant
// users, so we have to assume that there are none.
} else {
data = `0 ${pluralize(nomTypeText, 0)}`;
}
}
 
addNewNomData({
pageName,
data,
id: 'nominators',
hoverText: `${pluralize(
capitalize(nomTypeText),
allNominators.length
)} (sorted alphabetically):&#10;&#10;${allNominators.join('&#10;')}`,
});
}
 
/**
* Generate the patterns used to find vote text.
*
* @returns {Object} The patterns.
*/
function getVoteTextAndPatterns() {
// Look for text that is enclosed within bold text, or level-4 (or greater)
// headings.
const wrapPattern = "('''|====)";
 
// The amount of characters allowed between the vote text, and the wrapping
// patterns.
const voteBuffer = 25;
const textPattern = `(.{0,${voteBuffer}})?`;
 
let openPattern = `${wrapPattern}${textPattern}`;
let closePattern = `${textPattern}${wrapPattern}`;
 
let supportText = 'support';
let opposeText = 'oppose';
 
// Use different words for review pages.
if (nomType('reviews')) {
supportText = 'keep';
opposeText = 'delist';
 
// Pictures has their own specific method of declaring votes.
} else if (nomType('pictures')) {
openPattern = "\\*(\\s)?'''.*?";
closePattern = ".*?'''";
}
 
const createPattern = (text) =>
new RegExp(
`(${openPattern}${text}${closePattern}|^;${textPattern}${text})`,
'gim'
);
 
return {
supportText,
supportPattern: createPattern(supportText),
opposeText,
opposePattern: createPattern(opposeText),
};
}
 
function shouldShowVotes() {
const showOpposesForNominations = false;
const showOpposesForReviews = true;
 
return (
((nomType('nominations') || nomType('pictures')) &&
showOpposesForNominations) ||
(nomType('reviews') && showOpposesForReviews)
);
}
 
/**
* Add votes data to a nomination.
*
* @param {string} content The nomination's content.
* @param {string} pageName The page name.
* @returns {undefined}
*/
function addVotes(content, pageName) {
if (!dataIsEnabled('votes') || nomType('peer reviews')) {
return;
}
 
const { supportText, supportPattern, opposeText, opposePattern } =
getVoteTextAndPatterns();
 
const supportMatches = content.match(supportPattern) || [];
const opposeMatches = content.match(opposePattern) || [];
 
const supports = `${supportMatches.length} ${pluralize(
supportText,
supportMatches.length
)}`;
const opposes = `, ${opposeMatches.length} ${pluralize(
opposeText,
opposeMatches.length
)}`;
 
addNewNomData({
pageName,
data: shouldShowVotes() ? supports + opposes : supports,
id: 'votes',
hoverText: supports + opposes,
});
}
 
function currentRevisionCallback(object) {
const variables = formatJSON(object);
 
if (!variables) {
return;
}
 
const content = variables.firstRevision
? variables.firstRevision['*']
: null;
 
if (!content) {
return;
}
 
// 'images'
addImagesCount(content, variables.pageName);
 
// 'nominators'
addNominators(content, variables.pageName);
 
// 'votes'
addVotes(content, variables.pageName);
}
 
function addAge(firstRevision, pageName) {
if (!dataIsEnabled('age') || !firstRevision) {
return;
}
 
const { timeAgo, then } = getTimeAgo(firstRevision.timestamp);
 
addNewNomData({
pageName,
data: timeAgo,
id: 'age',
hoverText: `Creation date (local time):&#10;&#10;${then}`,
});
}
 
// Callback helpers
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
 
/**
* Check if the data field is enabled.
*
* @param {string} dataName The name of the data field to look up.
* @returns {boolean} The data field is enabled, so we want to use it.
*/
function dataIsEnabled(dataName) {
return NominationsViewer.nominationData.includes(dataName);
}
 
// Given `content`, find nominators with the `pattern`. Returns an Object, so
// that we exclude duplicates.
function findNominators(content, pattern) {
const nominatorMatches = content.match(pattern);
const listOfNominators = {};
 
if (!nominatorMatches) {
return listOfNominators;
}
 
// Find nominator usernames.
// [[User:Example|Example]], [[Wikipedia talk:WikiProject Example]]
let nominators = nominatorMatches[0].match(
/\[\[(user|wikipedia|wp|wt)([ _]talk)?:.*?]]/gi
);
 
if (nominators) {
nominators.forEach((nominator) => {
// Strip unneeded characters from the nominator's URL.
let username = nominator
// Strip the start of the username link.
.replace(/\[\[(user|wikipedia|wp|wt)([ _]talk)?:/i, '')
// Strip the displayed portion of the username link.
.replace(/\|.*/, '')
// Strip the ending portion of the username link.
.replace(']]', '')
// Strip URL anchors.
.replace(/#.*?$/, '');
 
// Does 'username' have a '/' that we have to strip?
if (username.includes('/')) {
username = username.slice(0, Math.max(0, username.indexOf('/')));
}
 
listOfNominators[username] += 1;
});
}
 
// {{user|Example}} and similar variants
const userTemplatePattern = /{{user.*?\|(.*?)}}/gi;
 
nominators = nominatorMatches[0].match(userTemplatePattern);
 
if (nominators) {
nominators.forEach((singleNominator) => {
listOfNominators[
singleNominator.replace(userTemplatePattern, '$1')
] += 1;
});
}
 
return listOfNominators;
}
 
function formatJSON(object) {
if (!object.query || !object.query.pages) {
return false;
}
 
const variables = [];
 
variables.pages = object.query.pages;
variables.page = Object.keys(variables.pages).map((page) => page);
 
if (variables.page.length !== 1) {
return false;
}
 
variables.page = object.query.pages[variables.page[0]];
variables.pageName = variables.page.title.replace(/\s/g, '_');
 
if (!variables.page.revisions) {
return false;
}
 
[variables.firstRevision] = variables.page.revisions;
variables.revisions = variables.page.revisions;
 
return variables;
}
 
/**
* Check if the nomination type of the current nomination is the type
* specified. If no type is specified, then return the type of the current
* nomination. Possible types are: `nominations`, `peer reviews`, `pictures`,
* and `reviews`, as specified in `NominationsViewer.enabledPages`.
*
* @param {string} [type] The type to compare the current nomination with.
* @returns {boolean|string} The current nomination matches the type
* specified, or the type of the current nomination.
*/
function nomType(type = null) {
const pageName = mw.config.get('wgPageName').replace(/_/g, ' ');
const pageType = NominationsViewer.enabledPages[pageName];
 
if (type) {
return type === pageType;
}
 
return pageType;
}
 
/**
* Pluralize a word if necessary.
*
* @param {string} string The word to possibly pluralize.
* @param {number} count The number of items there are.
* @returns {string} The pluralized word.
*/
function pluralize(string, count) {
const plural = `${string}s`;
 
if (count === 1) {
return string;
}
 
return plural;
}
 
/**
* Format a page name by remove any non-word characters.
*
* @param {string} pageName The page name to format.
* @returns {string} The formatted page name.
*/
function simplifyPageName(pageName) {
return pageName.replace(/\W/g, '');
}
 
/**
* Given a timestamp, generally calculate the time ago.
*
* @param {string} timestamp A timestamp.
* @returns {Object.<string, string>} The time ago phrase.
*/
function getTimeAgo(timestamp) {
const matches = timestamp.match(
/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z/
);
const now = new Date();
const then = new Date(
Date.UTC(
matches[1],
matches[2] - 1,
matches[3],
matches[4],
matches[5],
matches[6]
)
);
 
const millisecondsAgo = now.getTime() - then.getTime();
const daysAgo = Math.floor(millisecondsAgo / (1000 * 60 * 60 * 24));
let timeAgo = '';
 
if (daysAgo > 0) {
const weeksAgo = Math.round(daysAgo / 7);
const monthsAgo = Math.round(daysAgo / 30);
const yearsAgo = Math.round(daysAgo / 365);
 
if (yearsAgo >= 1) {
timeAgo = `${yearsAgo} ${pluralize('year', yearsAgo)} old`;
} else if (monthsAgo >= 3) {
timeAgo = `${monthsAgo} ${pluralize('month', monthsAgo)} old`;
} else if (weeksAgo >= 1) {
timeAgo = `${weeksAgo} ${pluralize('week', weeksAgo)} old`;
} else {
timeAgo = `${daysAgo} ${pluralize('day', daysAgo)} old`;
}
} else {
timeAgo = 'today';
}
 
return { timeAgo, then };
}
 
init();
});
// </nowiki>