// <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',
'lastedit',
'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(')');
return $headings
.first()
.next()
.prevUntil('h2')
.last()
.prev()
.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 = 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');
const useParentDiv = $editLinks.length === 0;
const parentDiv = $h3.parent();
const $editLinks2 = parentDiv.find('.mw-editsection a');
const $editLinksOption = useParentDiv ? $editLinks2 : $editLinks;
// There are no edit links.
if ($editLinksOption.length === 0) {
return;
}
const titleRegex = /[&?]title=(.*?)(?:&|$)/;
// Find the edit link that matches our regex.
const $filteredEditLinks = $editLinksOption.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);
const nodeToReplace = useParentDiv ? parentDiv : $h3;
// Create the nomination's title line.
const newNode = createNewNode({
oldNode: nodeToReplace,
showHideLink,
newSpan,
index,
});
// Create the actual nomination
const nomDiv = generateNomination(index, newNode, nodeToReplace);
// Replace this nomination with the new one we created.
nodeToReplace.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 parentNode = $h3[0].parentNode;
let sectionStart = parentNode.classList.contains('mw-heading3') ? parentNode : $h3[0];
let nomNextSibling = sectionStart.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 or mw-heading2"
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')
) &&
!(
nomNextSibling.classList &&
nomNextSibling.classList.contains('mw-heading2')
) &&
!(
nomNextSibling.classList &&
nomNextSibling.classList.contains('mw-heading3')
)
) {
const nomNextSiblingTemporary = nomNextSibling.nextSibling;
// Move the node, if it isn't a text node
if (nomNextSibling.nodeType !== 3) {
// eslint-disable-next-line unicorn/prefer-node-append
hiddenNode.appendChild(nomNextSibling);
}
nomNextSibling = nomNextSiblingTemporary;
}
// Insert hidden content
return sectionStart.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();
// eslint-disable-next-line unicorn/prefer-string-slice
const newIndex = $link
.attr('id')
.substring(
$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,
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 Total edits: ${
revisions.length
} Format: "editor: \
number of edits": ${usersArray2.join(' ')}`,
});
}
function allRevisionsCallback(object) {
const vars = formatJSON(object);
if (!vars) {
return;
}
// Participants
addParticipants(vars.revisions, vars.pageName, object['query-continue']);
// Nomination age
addAge(vars.firstRevision, vars.pageName);
// Last edit
addLastEdit(vars.lastRevision, vars.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): ${images.join(
' '
)}`,
});
}
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): ${allNominators.join(' ')}`,
});
}
/**
* 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 vars = formatJSON(object);
if (!vars) {
return;
}
const content = vars.firstRevision ? vars.firstRevision['*'] : null;
if (!content) {
return;
}
// 'images'
addImagesCount(content, vars.pageName);
// 'nominators'
addNominators(content, vars.pageName);
// 'votes'
addVotes(content, vars.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): ${then}`,
});
}
function addLastEdit(lastRevision, pageName) {
if (!dataIsEnabled('lastedit') || !lastRevision) {
return;
}
const { timeAgo, then } = getActivity(lastRevision.timestamp);
addNewNomData({
pageName,
data: timeAgo,
id: 'lastedit',
hoverText: `Last edit date (local time): ${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.some((data) => dataName === data);
}
// 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 vars = [];
vars.pages = object.query.pages;
vars.page = Object.keys(vars.pages).map((page) => page);
if (vars.page.length !== 1) {
return false;
}
vars.page = object.query.pages[vars.page[0]];
vars.pageName = vars.page.title.replace(/\s/g, '_');
if (!vars.page.revisions) {
return false;
}
[vars.firstRevision] = vars.page.revisions;
[vars.lastRevision] = vars.page.revisions.slice(-1);
vars.revisions = vars.page.revisions;
return vars;
}
/**
* 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 };
}
function getActivity(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 = `<b>Inactive for ${yearsAgo} ${pluralize('year', yearsAgo)}</b>`;
} else if (monthsAgo >= 3) {
timeAgo = `<b>Inactive for ${monthsAgo} ${pluralize('month', monthsAgo)}</b>`;
} else if (weeksAgo >= 1) {
timeAgo = `<b>Inactive for ${weeksAgo} ${pluralize('week', weeksAgo)}</b>`;
} else {
timeAgo = `Active ${daysAgo} ${pluralize('day', daysAgo)} ago`;
}
} else {
timeAgo = 'Active today';
}
return { timeAgo, then };
}
init();
});
// </nowiki>