User:JJPMaster/nomviewer.js

From Uncyclopedia, the content-free encyclopedia
Jump to navigation Jump to search

Note: After saving, you have to bypass your browser's cache to see the changes.

  • Internet Explorer: hold down the Ctrl key and click the Refresh or Reload button, or press Ctrl+F5.
  • Firefox: hold down the Shift key while clicking Reload; alternatively press Ctrl+F5 or Ctrl-Shift-R.
  • Opera, Konqueror and Safari users can just click the Reload button.
  • Chrome: press Ctrl+F5 or Shift+F5
// <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:JJPMaster/sandbox': 'nominations',

      'Uncyclopedia:VFH': 'nominations',

      'Uncyclopedia:VFP': 'pictures',

      'Uncyclopedia:Pee_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(')');

    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');

    // 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.classList &&
        nomNextSibling.classList.contains('printfooter')
      )
    ) {
      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 $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();

      // 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&#10;Total edits: ${
        revisions.length
      }&#10;Format: &quot;editor: \
number of edits&quot;:&#10;&#10;${usersArray2.join('&#10;')}`,
    });
  }

  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);
  }

  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 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):&#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.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.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 };
  }

  init();
});
// </nowiki>