From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.

/**

 * 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 (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 (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: function (then) {

			var pref = mw.user.options.values.date;

			return then.format(window.LocalComments.formatOptionspref || "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"],

	

	/**

	 * An object mapping the date format user options provided by this MediaWiki

	 * installation to corresponding Moment.js format strings. The user can

	 * choose a preferred date format in

	 * [[Special:Preferences#mw-prefsection-rendering-dateformat]]. See

	 * [[mw:Manual:Date formatting]]. These formats determine the default

	 * timestamp display format.

	 * 

	 * These formats come from

	 * <https://doc.wikimedia.org/mediawiki-core/1.34.0/php/MessagesEn_8php.html#a2fc93ea5327f655d3ed306e221ee33f0>.

	 * When customizing these formats for a different wiki’s content language,

	 * consult the language’s corresponding message file’s `$dateFormats`

	 * variable. Use only the messages with the “both” suffix, and remove that

	 * suffix from each key. The MediaWiki date format syntax is described in

	 * <https://doc.wikimedia.org/mediawiki-core/1.34.0/php/classLanguage.html#a94f84f82d7f954c4cb2e191d22c6e6a6>

	 * and [[mw:Help:Extension:ParserFunctions##time]]. The Moment.js syntax is

	 * described in <https://momentjs.com/docs/#/parsing/string-format/>.

	 * 

	 * @todo Automatically convert MediaWiki date format syntax to Moment.js

	 *		 date format syntax.

	 */

	formatOptions: {

		mdy: "HH:mm, MMMM D, YYYY", // H:i, F j, Y

		dmy: "HH:mm, D MMMM YYYY", // H:i, j F Y

		ymd: "HH:mm, YYYY MMMM D", // H:i, Y F j

		"ISO 8601": "YYYY-MM-DDTHH:mm:ss", // xnY-xnm-xnd"T"xnH:xni:xns

	},

	

	/**

	 * 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 (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;

		},

	});

	

	/**

	 * Marks 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(result0].length);

			

			// Determine the represented time.

			var then = moment.utc(result0], LocalComments.parseFormat);

			if (!then.isValid()) {

				// Many Wikipedias started out with English as the default

				// localization, so fall back to English.

				then = moment.utc(result0], "H:m, D MMM YYYY", "en");

			}

			

			// Wrap the timestamp inside a <time> element for findability.

			// This loop must wrap the text in a <time> element no matter what,

			// even if the time is invalid, to avoid an infinite loop as the

			// same node keeps coming up as a candidate that the node iterator

			// thinks is valid.

			// [[User talk:Mxn/CommentsInLocalTime.js#Interface-protected edit request on 18 November 2022]]

			var timeElt = $("<time />");

			if (then.isValid()) {

				then.utcOffset(-LocalComments.utcOffset);

				// 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();

	});

});