MediaWiki:Gadget-relativetime.js: Difference between revisions

From RuneRealm Wiki
Jump to navigation Jump to search
Content added Content deleted
No edit summary
Tag: Manual revert
No edit summary
 
Line 1: Line 1:
"use strict";

// Don't load CommentsInLocalTime for namespaces it is disabled for.
// Don't load CommentsInLocalTime for namespaces it is disabled for.
if ( [-1, 0, 8].indexOf(mw.config.get("wgNamespaceNumber")) === -1 ) {
if ([-1, 0, 8].indexOf(mw.config.get("wgNamespaceNumber")) === -1) {
// [[w:en:User:Mxn/CommentsInLocalTime]]
// [[w:en:User:Mxn/CommentsInLocalTime]]
// en.wikipedia.org/wiki/User:Mxn/CommentsInLocalTime.js
// en.wikipedia.org/wiki/User:Mxn/CommentsInLocalTime.js

/**
/**
* Comments in local time
* Comments in local time
* [[User:Mxn/CommentsInLocalTime]]
* [[User:Mxn/CommentsInLocalTime]]
*
*
* Adjust timestamps in comment signatures to use easy-to-understand, relative
* Adjust timestamps in comment signatures to use easy-to-understand, relative
* local time instead of absolute UTC time.
* local time instead of absolute UTC time.
*
*
* Inspired by [[Wikipedia:Comments in Local Time]].
* Inspired by [[Wikipedia:Comments in Local Time]].
*
*
* @author [[User:Mxn]]
* @author [[User:Mxn]]
*/
*/

/**
/**
* Default settings for this gadget.
* Default settings for this gadget.
*/
*/
window.LocalComments = $.extend({
window.LocalComments = $.extend({
// USER OPTIONS ////////////////////////////////////////////////////////////
// USER OPTIONS ////////////////////////////////////////////////////////////

/**
/**
* When false, this gadget does nothing.
* When false, this gadget does nothing.
*/
*/
enabled: true,
enabled: true,
/**
* Formats to display inline for each timestamp, keyed by a few common
/**
* cases.
* Formats to display inline for each timestamp, keyed by a few common
* cases.
*
* If a property of this object is set to a string, the timestamp is
*
* If a property of this object is set to a string, the timestamp is
* formatted according to the documentation at
* <http://momentjs.com/docs/#/displaying/format/>.
* formatted according to the documentation at
*
* <http://momentjs.com/docs/#/displaying/format/>.
* If a property of this object is set to a function, it is called to
*
* retrieve the formatted timestamp string. See
* If a property of this object is set to a function, it is called to
* <http://momentjs.com/docs/#/displaying/> for the various things you can
* retrieve the formatted timestamp string. See
* do with the passed-in moment object.
* <http://momentjs.com/docs/#/displaying/> for the various things you can
*/
* do with the passed-in moment object.
formats: {
*/
/**
formats: {
* Within a day, show a relative time that’s easy to relate to.
/**
*/
* Within a day, show a relative time that’s easy to relate to.
day: function day(then) {
*/
day: function (then) { return then.fromNow(); },
return then.fromNow();
},
/**
/**
* Within a week, show a relative date and specific time, still helpful
* Within a week, show a relative date and specific time, still helpful
* if the user doesn’t remember today’s date. Don’t show just a relative
* if the user doesn’t remember today’s date. Don’t show just a relative
* time, because a discussion may need more context than “Last Friday”
* time, because a discussion may need more context than “Last Friday”
* on every comment.
* on every comment.
*/
*/
week: function (then) { return then.calendar(); },
week: function week(then) {
return then.calendar();
},
/**
/**
* The calendar() method uses an ambiguous “MM/DD/YYYY” format for
* The calendar() method uses an ambiguous “MM/DD/YYYY” format for
* faraway dates; spell things out for this international audience.
* faraway dates; spell things out for this international audience.
*/
*/
other: "LLL",
other: "LLL"
},
},
/**
/**
* Formats to display in each timestamp’s tooltip, one per line.
* Formats to display in each timestamp’s tooltip, one per line.
*
*
* If an element of this array is a string, the timestamp is formatted
* If an element of this array is a string, the timestamp is formatted
* according to the documentation at
* according to the documentation at
* <http://momentjs.com/docs/#/displaying/format/>.
* <http://momentjs.com/docs/#/displaying/format/>.
*
*
* If an element of this array is a function, it is called to retrieve the
* If an element of this array is a function, it is called to retrieve the
* formatted timestamp string. See <http://momentjs.com/docs/#/displaying/>
* formatted timestamp string. See <http://momentjs.com/docs/#/displaying/>
* for the various things you can do with the passed-in moment object.
* for the various things you can do with the passed-in moment object.
*/
*/
tooltipFormats: [
tooltipFormats: [function (then) {
function (then) { return then.fromNow(); },
return then.fromNow();
}, "LLLL", "YYYY-MM-DDTHH:mmZ"],
"LLLL",
/**
"YYYY-MM-DDTHH:mmZ",
* When true, this gadget refreshes timestamps periodically.
],
*/
dynamic: true
/**
}, {
* When true, this gadget refreshes timestamps periodically.
// SITE OPTIONS ////////////////////////////////////////////////////////////
*/

dynamic: true,
/**
}, {
* Numbers of namespaces to completely ignore. See [[Wikipedia:Namespace]].
// SITE OPTIONS ////////////////////////////////////////////////////////////
*/
excludeNamespaces: [-1, 0, 8, 100, 108, 118],
/**
/**
* Numbers of namespaces to completely ignore. See [[Wikipedia:Namespace]].
* Names of tags that often directly contain timestamps.
*/
*
excludeNamespaces: [-1, 0, 8, 100, 108, 118],
* This is merely a performance optimization. This gadget will look at text
* nodes in any tag other than the codeTags, but adding a tag here ensures
/**
* that it gets processed the most efficient way possible.
* Names of tags that often directly contain timestamps.
*/
*
proseTags: ["dd", "li", "p", "td"],
* This is merely a performance optimization. This gadget will look at text
/**
* nodes in any tag other than the codeTags, but adding a tag here ensures
* Names of tags that don’t contain timestamps either directly or
* that it gets processed the most efficient way possible.
* indirectly.
*/
*/
proseTags: ["dd", "li", "p", "td"],
codeTags: ["code", "input", "pre", "textarea"],
/**
/**
* Names of tags that don’t contain timestamps either directly or
* Expected format or formats of the timestamps in existing wikitext. If
* very different formats have been used over the course of the wiki’s
* indirectly.
* history, specify an array of formats.
*/
*
codeTags: ["code", "input", "pre", "textarea"],
* This option expects parsing format strings
* <http://momentjs.com/docs/#/parsing/string-format/>.
/**
*/
* Expected format or formats of the timestamps in existing wikitext. If
parseFormat: "H:m, D MMM YYYY",
* very different formats have been used over the course of the wiki’s
/**
* history, specify an array of formats.
* Regular expression matching all the timestamps inserted by this MediaWiki
*
* installation over the years. This regular expression should more or less
* This option expects parsing format strings
* agree with the parseFormat option.
* <http://momentjs.com/docs/#/parsing/string-format/>.
*/
*
* Until 2005:
parseFormat: "H:m, D MMM YYYY",
* 18:16, 23 Dec 2004 (UTC)
* 2005–present:
/**
* 08:51, 23 November 2015 (UTC)
* Regular expression matching all the timestamps inserted by this MediaWiki
*/
* installation over the years. This regular expression should more or less
parseRegExp: /\d\d:\d\d, \d\d? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w* \d{4} \(UTC\)/,
* agree with the parseFormat option.
/**
*
* UTC offset of the wiki's default local timezone. See
* Until 2005:
* [[mw:Manual:Timezone]].
* 18:16, 23 Dec 2004 (UTC)
*/
* 2005–present:
utcOffset: 0
* 08:51, 23 November 2015 (UTC)
}, window.LocalComments);
*/
$(function () {
parseRegExp: /\d\d:\d\d, \d\d? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w* \d{4} \(UTC\)/,
if (!LocalComments.enabled || LocalComments.excludeNamespaces.indexOf(mw.config.get("wgNamespaceNumber")) !== -1 || ["view", "submit"].indexOf(mw.config.get("wgAction")) === -1 || mw.util.getParamValue("disable") === "loco") {
return;
/**
}
* UTC offset of the wiki's default local timezone. See
var proseTags = LocalComments.proseTags.join("\n").toUpperCase().split("\n");
* [[mw:Manual:Timezone]].
// Exclude <time> to avoid an infinite loop when iterating over text nodes.
*/
var codeTags = $.merge(LocalComments.codeTags, ["time"]).join(", ");
utcOffset: 0,

}, window.LocalComments);
// Look in the content body for DOM text nodes that may contain timestamps.
// The wiki software has already localized other parts of the page.
$(function () {
var root = $("#wikiPreview, #mw-content-text")[0];
if (!LocalComments.enabled
if (!root || !("createNodeIterator" in document)) return;
|| LocalComments.excludeNamespaces.indexOf(mw.config.get("wgNamespaceNumber")) !== -1
var iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, {
|| ["view", "submit"].indexOf(mw.config.get("wgAction")) === -1
acceptNode: function acceptNode(node) {
|| mw.util.getParamValue("disable") === "loco")
// We can’t just check the node’s direct parent, because templates
{
// like [[Template:Talkback]] and [[Template:Resolved]] may place a
return;
// signature inside a nondescript <span>.
}
var isInProse = proseTags.indexOf(node.parentElement.nodeName) !== -1 || !$(node).parents(codeTags).length;
var proseTags = LocalComments.proseTags.join("\n").toUpperCase().split("\n");
var isDateNode = isInProse && LocalComments.parseRegExp.test(node.data);
return isDateNode ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
// Exclude <time> to avoid an infinite loop when iterating over text nodes.
}
var codeTags = $.merge(LocalComments.codeTags, ["time"]).join(", ");
});

// Look in the content body for DOM text nodes that may contain timestamps.
// Mark up each timestamp found.
// The wiki software has already localized other parts of the page.
function wrapTimestamps() {
var root = $("#wikiPreview, #mw-content-text")[0];
var prefixNode;
if (!root || !("createNodeIterator" in document)) return;
while (prefixNode = iter.nextNode()) {
var iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, {
var result = LocalComments.parseRegExp.exec(prefixNode.data);
acceptNode: function (node) {
if (!result) continue;
// We can’t just check the node’s direct parent, because templates

// like [[Template:Talkback]] and [[Template:Resolved]] may place a
// Split out the timestamp into a separate text node.
// signature inside a nondescript <span>.
var dateNode = prefixNode.splitText(result.index);
var isInProse = proseTags.indexOf(node.parentElement.nodeName) !== -1
var suffixNode = dateNode.splitText(result[0].length);
|| !$(node).parents(codeTags).length;

var isDateNode = isInProse && LocalComments.parseRegExp.test(node.data);
// Determine the represented time.
return isDateNode ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
var then = moment.utc(result[0], LocalComments.parseFormat);
},
if (!then.isValid()) {
});
// Many Wikipedias started out with English as the default
// localization, so fall back to English.
// Mark up each timestamp found.
then = moment.utc(result[0], "H:m, D MMM YYYY", "en");
function wrapTimestamps() {
}
var prefixNode;
if (!then.isValid()) continue;
while ((prefixNode = iter.nextNode())) {
then.utcOffset(-LocalComments.utcOffset);
var result = LocalComments.parseRegExp.exec(prefixNode.data);

if (!result) continue;
// Wrap the timestamp inside a <time> element for findability.
var timeElt = $("<time />");
// Split out the timestamp into a separate text node.
// MediaWiki core styles .explain[title] the same way as
var dateNode = prefixNode.splitText(result.index);
// abbr[title], guiding the user to the tooltip.
var suffixNode = dateNode.splitText(result[0].length);
timeElt.addClass("localcomments explain");
timeElt.attr("datetime", then.toISOString());
// Determine the represented time.
$(dateNode).wrap(timeElt);
var then = moment.utc(result[0], LocalComments.parseFormat);
}
if (!then.isValid()) {
}
// Many Wikipedias started out with English as the default

// localization, so fall back to English.
/**
then = moment.utc(result[0], "H:m, D MMM YYYY", "en");
* Returns a formatted string for the given moment object.
}
*
if (!then.isValid()) continue;
* @param {Moment} then The moment object to format.
then.utcOffset(-LocalComments.utcOffset);
* @param {String} fmt A format string or function.
* @returns {String} A formatted string.
// Wrap the timestamp inside a <time> element for findability.
*/
var timeElt = $("<time />");
function formatMoment(then, fmt) {
// MediaWiki core styles .explain[title] the same way as
return fmt instanceof Function ? fmt(then) : then.format(fmt);
// abbr[title], guiding the user to the tooltip.
}
timeElt.addClass("localcomments explain");

timeElt.attr("datetime", then.toISOString());
/**
$(dateNode).wrap(timeElt);
* Reformats a timestamp marked up with the <time> element.
}
*
}
* @param {Number} idx Unused.
* @param {Element} elt The <time> element.
/**
*/
* Returns a formatted string for the given moment object.
function formatTimestamp(idx, elt) {
*
var iso = $(elt).attr("datetime");
* @param {Moment} then The moment object to format.
var then = moment(iso, moment.ISO_8601);
* @param {String} fmt A format string or function.
var now = moment();
* @returns {String} A formatted string.
var withinHours = Math.abs(then.diff(now, "hours", true)) <= moment.relativeTimeThreshold("h");
*/
var formats = LocalComments.formats;
function formatMoment(then, fmt) {
var text;
return (fmt instanceof Function) ? fmt(then) : then.format(fmt);
if (withinHours) {
}
text = formatMoment(then, formats.day || formats.other);
} else {
/**
var dayDiff = then.diff(moment().startOf("day"), "days", true);
* Reformats a timestamp marked up with the <time> element.
if (dayDiff > -6 && dayDiff < 7) {
*
text = formatMoment(then, formats.week || formats.other);
* @param {Number} idx Unused.
} else text = formatMoment(then, formats.other);
* @param {Element} elt The <time> element.
}
*/
$(elt).text(text);
function formatTimestamp(idx, elt) {

var iso = $(elt).attr("datetime");
// Add a tooltip with multiple formats.
var then = moment(iso, moment.ISO_8601);
elt.title = $.map(LocalComments.tooltipFormats, function (fmt, idx) {
var now = moment();
return formatMoment(then, fmt);
var withinHours = Math.abs(then.diff(now, "hours", true))
}).join("\n");
<= moment.relativeTimeThreshold("h");

var formats = LocalComments.formats;
// Register for periodic updates.
var text;
var withinMinutes = withinHours && Math.abs(then.diff(now, "minutes", true)) <= moment.relativeTimeThreshold("m");
if (withinHours) {
var withinSeconds = withinMinutes && Math.abs(then.diff(now, "seconds", true)) <= moment.relativeTimeThreshold("s");
text = formatMoment(then, formats.day || formats.other);
var unit = withinSeconds ? "seconds" : withinMinutes ? "minutes" : withinHours ? "hours" : "days";
}
$(elt).attr("data-localcomments-unit", unit);
else {
}
var dayDiff = then.diff(moment().startOf("day"), "days", true);

if (dayDiff > -6 && dayDiff < 7) {
/**
text = formatMoment(then, formats.week || formats.other);
* Reformat all marked-up timestamps and start updating timestamps on an
}
* interval as necessary.
else text = formatMoment(then, formats.other);
*/
}
function formatTimestamps() {
$(elt).text(text);
wrapTimestamps();
$(".localcomments").each(function (idx, elt) {
// Add a tooltip with multiple formats.
// Update every timestamp at least this once.
elt.title = $.map(LocalComments.tooltipFormats, function (fmt, idx) {
formatTimestamp(idx, elt);
return formatMoment(then, fmt);
if (!LocalComments.dynamic) return;
}).join("\n");

// Update this minute’s timestamps every second.
// Register for periodic updates.
if ($("[data-localcomments-unit='seconds']").length) {
var withinMinutes = withinHours
setInterval(function () {
&& Math.abs(then.diff(now, "minutes", true))
$("[data-localcomments-unit='seconds']").each(formatTimestamp);
<= moment.relativeTimeThreshold("m");
}, 1000 /* ms */);
var withinSeconds = withinMinutes
}
&& Math.abs(then.diff(now, "seconds", true))
// Update this hour’s timestamps every minute.
<= moment.relativeTimeThreshold("s");
setInterval(function () {
var unit = withinSeconds ? "seconds" :
$("[data-localcomments-unit='minutes']").each(formatTimestamp);
(withinMinutes ? "minutes" :
}, 60 /* s */ * 1000 /* ms */);
(withinHours ? "hours" : "days"));
// Update today’s timestamps every hour.
$(elt).attr("data-localcomments-unit", unit);
setInterval(function () {
}
$("[data-localcomments-unit='hours']").each(formatTimestamp);
}, 60 /* min */ * 60 /* s */ * 1000 /* ms */);
/**
});
* Reformat all marked-up timestamps and start updating timestamps on an
}
* interval as necessary.
mw.loader.using("moment", function () {
*/
wrapTimestamps();
function formatTimestamps() {
formatTimestamps();
wrapTimestamps();
});
$(".localcomments").each(function (idx, elt) {
});
// Update every timestamp at least this once.
formatTimestamp(idx, elt);
if (!LocalComments.dynamic) return;
// Update this minute’s timestamps every second.
if ($("[data-localcomments-unit='seconds']").length) {
setInterval(function () {
$("[data-localcomments-unit='seconds']").each(formatTimestamp);
}, 1000 /* ms */);
}
// Update this hour’s timestamps every minute.
setInterval(function () {
$("[data-localcomments-unit='minutes']").each(formatTimestamp);
}, 60 /* s */ * 1000 /* ms */);
// Update today’s timestamps every hour.
setInterval(function () {
$("[data-localcomments-unit='hours']").each(formatTimestamp);
}, 60 /* min */ * 60 /* s */ * 1000 /* ms */);
});
}
mw.loader.using("moment", function () {
wrapTimestamps();
formatTimestamps();
});
});
}
}

Latest revision as of 12:06, 20 October 2024

"use strict";

// Don't load CommentsInLocalTime for namespaces it is disabled for.
if ([-1, 0, 8].indexOf(mw.config.get("wgNamespaceNumber")) === -1) {
  // [[w:en:User:Mxn/CommentsInLocalTime]]
  // en.wikipedia.org/wiki/User:Mxn/CommentsInLocalTime.js

  /**
   * Comments in local time
   * [[User:Mxn/CommentsInLocalTime]]
   * 
   * Adjust timestamps in comment signatures to use easy-to-understand, relative
   * local time instead of absolute UTC time.
   * 
   * Inspired by [[Wikipedia:Comments in Local Time]].
   * 
   * @author [[User:Mxn]]
   */

  /**
   * Default settings for this gadget.
   */
  window.LocalComments = $.extend({
    // USER OPTIONS ////////////////////////////////////////////////////////////

    /**
     * When false, this gadget does nothing.
     */
    enabled: true,
    /**
     * Formats to display inline for each timestamp, keyed by a few common
     * cases.
     * 
     * If a property of this object is set to a string, the timestamp is
     * formatted according to the documentation at
     * <http://momentjs.com/docs/#/displaying/format/>.
     * 
     * If a property of this object is set to a function, it is called to
     * retrieve the formatted timestamp string. See
     * <http://momentjs.com/docs/#/displaying/> for the various things you can
     * do with the passed-in moment object.
     */
    formats: {
      /**
       * Within a day, show a relative time that’s easy to relate to.
       */
      day: function day(then) {
        return then.fromNow();
      },
      /**
       * Within a week, show a relative date and specific time, still helpful
       * if the user doesn’t remember today’s date. Don’t show just a relative
       * time, because a discussion may need more context than “Last Friday”
       * on every comment.
       */
      week: function week(then) {
        return then.calendar();
      },
      /**
       * The calendar() method uses an ambiguous “MM/DD/YYYY” format for
       * faraway dates; spell things out for this international audience.
       */
      other: "LLL"
    },
    /**
     * Formats to display in each timestamp’s tooltip, one per line.
     * 
     * If an element of this array is a string, the timestamp is formatted
     * according to the documentation at
     * <http://momentjs.com/docs/#/displaying/format/>.
     * 
     * If an element of this array is a function, it is called to retrieve the
     * formatted timestamp string. See <http://momentjs.com/docs/#/displaying/>
     * for the various things you can do with the passed-in moment object.
     */
    tooltipFormats: [function (then) {
      return then.fromNow();
    }, "LLLL", "YYYY-MM-DDTHH:mmZ"],
    /**
     * When true, this gadget refreshes timestamps periodically.
     */
    dynamic: true
  }, {
    // SITE OPTIONS ////////////////////////////////////////////////////////////

    /**
     * Numbers of namespaces to completely ignore. See [[Wikipedia:Namespace]].
     */
    excludeNamespaces: [-1, 0, 8, 100, 108, 118],
    /**
     * Names of tags that often directly contain timestamps.
     * 
     * This is merely a performance optimization. This gadget will look at text
     * nodes in any tag other than the codeTags, but adding a tag here ensures
     * that it gets processed the most efficient way possible.
     */
    proseTags: ["dd", "li", "p", "td"],
    /**
     * Names of tags that don’t contain timestamps either directly or
     * indirectly.
     */
    codeTags: ["code", "input", "pre", "textarea"],
    /**
     * Expected format or formats of the timestamps in existing wikitext. If
     * very different formats have been used over the course of the wiki’s
     * history, specify an array of formats.
     * 
     * This option expects parsing format strings
     * <http://momentjs.com/docs/#/parsing/string-format/>.
     */
    parseFormat: "H:m, D MMM YYYY",
    /**
     * Regular expression matching all the timestamps inserted by this MediaWiki
     * installation over the years. This regular expression should more or less
     * agree with the parseFormat option.
     * 
     * Until 2005:
     * 	18:16, 23 Dec 2004 (UTC)
     * 2005–present:
     * 	08:51, 23 November 2015 (UTC)
     */
    parseRegExp: /\d\d:\d\d, \d\d? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w* \d{4} \(UTC\)/,
    /**
     * UTC offset of the wiki's default local timezone. See
     * [[mw:Manual:Timezone]].
     */
    utcOffset: 0
  }, window.LocalComments);
  $(function () {
    if (!LocalComments.enabled || LocalComments.excludeNamespaces.indexOf(mw.config.get("wgNamespaceNumber")) !== -1 || ["view", "submit"].indexOf(mw.config.get("wgAction")) === -1 || mw.util.getParamValue("disable") === "loco") {
      return;
    }
    var proseTags = LocalComments.proseTags.join("\n").toUpperCase().split("\n");
    // Exclude <time> to avoid an infinite loop when iterating over text nodes.
    var codeTags = $.merge(LocalComments.codeTags, ["time"]).join(", ");

    // Look in the content body for DOM text nodes that may contain timestamps.
    // The wiki software has already localized other parts of the page.
    var root = $("#wikiPreview, #mw-content-text")[0];
    if (!root || !("createNodeIterator" in document)) return;
    var iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, {
      acceptNode: function acceptNode(node) {
        // We can’t just check the node’s direct parent, because templates
        // like [[Template:Talkback]] and [[Template:Resolved]] may place a
        // signature inside a nondescript <span>.
        var isInProse = proseTags.indexOf(node.parentElement.nodeName) !== -1 || !$(node).parents(codeTags).length;
        var isDateNode = isInProse && LocalComments.parseRegExp.test(node.data);
        return isDateNode ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
      }
    });

    // Mark up each timestamp found.
    function wrapTimestamps() {
      var prefixNode;
      while (prefixNode = iter.nextNode()) {
        var result = LocalComments.parseRegExp.exec(prefixNode.data);
        if (!result) continue;

        // Split out the timestamp into a separate text node.
        var dateNode = prefixNode.splitText(result.index);
        var suffixNode = dateNode.splitText(result[0].length);

        // Determine the represented time.
        var then = moment.utc(result[0], LocalComments.parseFormat);
        if (!then.isValid()) {
          // Many Wikipedias started out with English as the default
          // localization, so fall back to English.
          then = moment.utc(result[0], "H:m, D MMM YYYY", "en");
        }
        if (!then.isValid()) continue;
        then.utcOffset(-LocalComments.utcOffset);

        // Wrap the timestamp inside a <time> element for findability.
        var timeElt = $("<time />");
        // MediaWiki core styles .explain[title] the same way as
        // abbr[title], guiding the user to the tooltip.
        timeElt.addClass("localcomments explain");
        timeElt.attr("datetime", then.toISOString());
        $(dateNode).wrap(timeElt);
      }
    }

    /**
     * Returns a formatted string for the given moment object.
     * 
     * @param {Moment} then The moment object to format.
     * @param {String} fmt A format string or function.
     * @returns {String} A formatted string.
     */
    function formatMoment(then, fmt) {
      return fmt instanceof Function ? fmt(then) : then.format(fmt);
    }

    /**
     * Reformats a timestamp marked up with the <time> element.
     * 
     * @param {Number} idx Unused.
     * @param {Element} elt The <time> element.
     */
    function formatTimestamp(idx, elt) {
      var iso = $(elt).attr("datetime");
      var then = moment(iso, moment.ISO_8601);
      var now = moment();
      var withinHours = Math.abs(then.diff(now, "hours", true)) <= moment.relativeTimeThreshold("h");
      var formats = LocalComments.formats;
      var text;
      if (withinHours) {
        text = formatMoment(then, formats.day || formats.other);
      } else {
        var dayDiff = then.diff(moment().startOf("day"), "days", true);
        if (dayDiff > -6 && dayDiff < 7) {
          text = formatMoment(then, formats.week || formats.other);
        } else text = formatMoment(then, formats.other);
      }
      $(elt).text(text);

      // Add a tooltip with multiple formats.
      elt.title = $.map(LocalComments.tooltipFormats, function (fmt, idx) {
        return formatMoment(then, fmt);
      }).join("\n");

      // Register for periodic updates.
      var withinMinutes = withinHours && Math.abs(then.diff(now, "minutes", true)) <= moment.relativeTimeThreshold("m");
      var withinSeconds = withinMinutes && Math.abs(then.diff(now, "seconds", true)) <= moment.relativeTimeThreshold("s");
      var unit = withinSeconds ? "seconds" : withinMinutes ? "minutes" : withinHours ? "hours" : "days";
      $(elt).attr("data-localcomments-unit", unit);
    }

    /**
     * Reformat all marked-up timestamps and start updating timestamps on an
     * interval as necessary.
     */
    function formatTimestamps() {
      wrapTimestamps();
      $(".localcomments").each(function (idx, elt) {
        // Update every timestamp at least this once.
        formatTimestamp(idx, elt);
        if (!LocalComments.dynamic) return;

        // Update this minute’s timestamps every second.
        if ($("[data-localcomments-unit='seconds']").length) {
          setInterval(function () {
            $("[data-localcomments-unit='seconds']").each(formatTimestamp);
          }, 1000 /* ms */);
        }
        // Update this hour’s timestamps every minute.
        setInterval(function () {
          $("[data-localcomments-unit='minutes']").each(formatTimestamp);
        }, 60 /* s */ * 1000 /* ms */);
        // Update today’s timestamps every hour.
        setInterval(function () {
          $("[data-localcomments-unit='hours']").each(formatTimestamp);
        }, 60 /* min */ * 60 /* s */ * 1000 /* ms */);
      });
    }
    mw.loader.using("moment", function () {
      wrapTimestamps();
      formatTimestamps();
    });
  });
}