Thimbleweed Park™ Blog fixes

Fixes time format and timezone; Highlights new comments since last visit; Pre-fills comment input form; User settings can be changed via menu command

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==

// @name         Thimbleweed Park™ Blog fixes
// @namespace    NorTreblig
// @version      0.20
// @description  Fixes time format and timezone; Highlights new comments since last visit; Pre-fills comment input form; User settings can be changed via menu command
// @author       Nor Treblig

// @match        http://blog.thimbleweedpark.com/*
// @match        https://blog.thimbleweedpark.com/*

// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.0/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment-with-locales.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.1/moment-timezone-with-data-2010-2020.min.js
// @require      https://openuserjs.org/src/libs/sizzle/GM_config.min.js
// @grant        GM_addStyle
// @grant        GM_deleteValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_setValue
// @grant        GM_registerMenuCommand

// ==/UserScript==
/* jshint -W097 */
'use strict';


var localUserSettings = { };
var remoteUserSettings = { };
var scriptSettings =
	{
		serverTimeZone:			"America/New_York",
		serverDateFormat:		"MMM D, YYYY",	// example: "Mar 28, 2016"
		serverDateTimeFormat:	"MMM D, YYYY [at] h:m",	// example: "Mar 29, 2016 at 02:02"

		newCommentIndicator:	"<span class=\"newComment commentdate\"><a id=\"{0}\" name=\"{0}\" href=\"{1}\" title=\"click to go to next new comment\">new comment</a></span><div style=\"clear: right; margin-bottom: 0px;\" />",
		newCommentsIndicator:	"<span class=\"newComment date\"><a href=\"{3}\">{1} new comment(s)</a> since {2} (total: {0})</span><div style=\"clear: right; margin-bottom: 0px;\" />",
		noNewCommentsIndicator:	"<span class=\"noNewComments date\">no new comments since {2} (total: {0})</span><div style=\"clear: right; margin-bottom: 0px;\" />",
		archiveListHeader:		"<span class=\"date\"> - <span class=\"{2}\">{0} new comment(s)</span> (total: {1})</span>",
		archiveListMarkRead:	"<a href=\"#\" class=\"markRead\">[mark comments of all new blog posts as read]</a>",

		countdownSnippet:		"<center style=\"height: 300px; position: relative;\"><iframe width=\"545\" height=\"280\" src=\"https://w2.countingdownto.com/1759791\" frameborder=\"0\" style=\"background-color: #090B22;\" /><div style=\"position: relative; width: 90px; height: 90px; left: 227px; top: -280px; background-color: #090B22; z-index: 10;\">&nbsp;</div><div style=\"position: absolute; top: 0; left: 0; width: 100%; height: 280px; z-index: 15;\"><a style=\"display: inline-block; width: 545px; height: 280px;\" href=\"http://www.cinemapioxi.it/zak/twp/ThimbleweedParkITA.html\" /></div></div>",
	};

var settingsLoaded = false;
var referenceTime = null;
var currentVisit = moment().utc();


function Main()
{
	// initialize settings
	InitSettings();

	// load settings
	LoadSettings();

	// add styles
	GM_addStyle(".commentform input { max-width: 100%; }");	// fix width of textboxes in narrow browser windows
	GM_addStyle("span.newComment { margin-left: 10px !important; float: right; }");
	GM_addStyle("span.newComment.commentdate { margin-top: 2px; }");
	GM_addStyle("span.newComment.date { margin-top: 8px; }");
	GM_addStyle("span.newComment a { padding: 2 5 2 5; background: yellow; text-decoration: none; }");
	GM_addStyle("span.newComment a:focus { background: orange; }");
	GM_addStyle("span.noNewComments { margin-left: 10px !important; margin-top: 8px; float: right; padding: 2 5 2 5; }");
	GM_addStyle(".archiveBody span.newComment.date, .mainCommentLink span.newComment.date { margin-top: 0px; }");
	GM_addStyle(".archiveBody span.noNewComments, .mainCommentLink span.noNewComments { margin-top: 0px; }");
	GM_addStyle(".archiveBody span.newComments, .mainCommentLink span.newComments { padding: 2 5 2 5; background: yellow; }");
	GM_addStyle(".archiveBody .markRead { margin-left: 5px; padding: 2 5 2 5; color: gray !important; background: yellow; font-size: 60%; text-decoration: none !important; }");


	// add coundown
	if (localUserSettings.showCountdown && false)
		ShowCountdown();

	// process reply links
	ProcessReplyLinks();

	// pre-fill comment form
	ProcessCommentForm();

	// add indicators to blog entries of archive list
	ProcessArchiveList();

	// get reference time of blog post and fix its format
	referenceTime = GetBlogPostReferenceTime(true);

	// fix time format and time zone on comments
	// example: "Mar 29, 2016 at 02:02" -> "Tue 2016-03-29 08:02"
	jQuery.each(jQuery(".commentdate"),
		function(i, e)
		{
			var element = $(e);

			var timeText = element.text();
			var serverTime = ParseServerTime(timeText);
			if (serverTime.isValid())
			{
				var utcTime = serverTime.clone().utc();
				var localTime = utcTime.clone().local();

				// set element attributes
				element.attr("data-utcvalue", utcTime.format());
				element.attr("title", timeText);

				element.text(localTime.locale(localUserSettings.displayLocale).format(localUserSettings.displayDateTimeFormat));
			}
			else
				console.warn("Couldn't parse time: \"" + timeText + "\"");
		});

	// mark new comments and add indicator to blog title
	ProcessBlogPosts();

	// use jQuery to scroll to anchors with scroll offset
	jQuery("a[href^='#newComment']").click(
		function(e)
		{
			var anchorID = $(e.target).attr("href");
			var anchor = jQuery(anchorID);
			if (anchor.length)
			{
				// scroll and set focus
				jQuery('html, body').scrollTop(anchor.offset().top + localUserSettings.scrollOffset);
				anchor.focus();
			}
			e.preventDefault();
		});
}

function InitSettings()
{
	GM_config.init(
		{
			id: "UserSettings",
			title: "Thimbleweed Park™ Blog fixes - Settings",
			fields:
				{
					"settingsVersion":
						{
							"type": "hidden",
							"default": "0",
						},

					"displayDateFormat":
						{
							"section": [ GM_config.create("Date/Time Format"), "override display format for dates and times" ],
							"label": "Display date format",
							"labelPos": "left",
							"type": "text",
							"default": "ddd YYYY-MM-DD",
						},
					"displayDateTimeFormat":
						{
							"label": "Display datetime format",
							"labelPos": "left",
							"type": "text",
							"default": "ddd YYYY-MM-DD HH:mm",
						},
					"displayLocale":
						{
							"label": "Override browser locale (e.g. en-UK)",
							"labelPos": "left",
							"type": "text",
							"default": null,
						},

					"scrollOffset":
						{
							"section": [ GM_config.create("Highlight New Comments"), "settings related to highlighting comments" ],
							"label": "Scroll offset in pixels",
							"labelPos": "left",
							"type": "int",
							"default": -125
						},

					"replyInNewTab":
						{
							"section": [ GM_config.create("Reply Links"), "change behaviour of reply links" ],
							"label": "Open reply in new tab",
							"labelPos": "right",
							"type": "checkbox",
							"default": false,
						},
					"invertReplyLinkBehaviour":
						{
							"label": "Invert reply link behaviour (Click vs. Ctrl + Click)",
							"labelPos": "right",
							"type": "checkbox",
							"default": false,
						},

					"userName":
						{
							"section": [ GM_config.create("Create Comment"), "allows pre-filling of comment input form" ],
							"label": "Name",
							"labelPos": "left",
							"type": "text",
							"default": null,
						},
					"userEmail":
						{
							"label": "Email",
							"labelPos": "left",
							"type": "text",
							"default": null,
						},
					"userWebsite":
						{
							"label": "Website",
							"labelPos": "left",
							"type": "text",
							"default": null,
						},
					"tmtsotsomi":
						{
							"label": "Tell me the secret of The Secret of Monkey Island",
							"labelPos": "right",
							"type": "checkbox",
							"default": false,
						},

					"showCountdown":
						{
							"section": [ GM_config.create("Miscellaneous"), "stuff related to miscellaneousness" ],
							"label": "Show Final Countdown (da da DAA daa, da da DA da daa) [DISABLED]",
							"labelPos": "right",
							//"type": "checkbox",
							"type": "hidden",
							"default": true,
						},

					"importExportData":
						{
							"section": [ GM_config.create("Import/Export"), null ],
							"label": "Data:",
							"labelPos": "left",
							"type": "textarea",
							"default": null,
							"save": false,
						},
					"importButton":
						{
							"label": "Import",
							"type": "button",
							"click": ImportData,
						},
					"exportButton":
						{
							"label": "Export",
							"type": "button",
							"click": ExportData,
						},
				},
			events:
				{
					save:
						function(values)
						{
							if (settingsLoaded)
							{
								// reload page
								//document.location.reload();
								alert("Note: Refresh page to see changes.");
							}
						},
				},
		});
}

function LoadSettings()
{
	var currentVersion = 1;

	// upgrade settings if necessary
	var version = parseInt(GM_config.get("settingsVersion"));
	if (version <= 0)
	{
		// backward compatibilty: try to load individual settings from persistent storage
		if (GM_getValue("UserSetting_DisplayDateFormat") != null)		GM_config.set("displayDateFormat", GM_getValue("UserSetting_DisplayDateFormat"));
		if (GM_getValue("UserSetting_DisplayDateTimeFormat") != null)	GM_config.set("displayDateTimeFormat", GM_getValue("UserSetting_DisplayDateTimeFormat"));
		if (GM_getValue("UserSetting_DisplayLocale") != null)			GM_config.set("displayLocale", GM_getValue("UserSetting_DisplayLocale"));
		if (GM_getValue("UserSetting_ScrollOffset") != null)			GM_config.set("scrollOffset", GM_getValue("UserSetting_ScrollOffset"));
		if (GM_getValue("UserSetting_UserName") != null)				GM_config.set("userName", GM_getValue("UserSetting_UserName"));
		if (GM_getValue("UserSetting_UserEmail") != null)				GM_config.set("userEmail", GM_getValue("UserSetting_UserEmail"));
		if (GM_getValue("UserSetting_UserWebsite") != null)				GM_config.set("userWebsite", GM_getValue("UserSetting_UserWebsite"));
		if (GM_getValue("UserSetting_TMTSOTSOMI") != null)				GM_config.set("tmtsotsomi", GM_getValue("UserSetting_TMTSOTSOMI"));
	}
	if (version < currentVersion)
	{
		// update version
		GM_config.set("settingsVersion", currentVersion.toString());
		GM_config.save();
		console.log("upgraded settings from version " + version.toString() + " to " + currentVersion.toString());
	}

	// load local user settings
	localUserSettings.displayDateFormat			= GM_config.get("displayDateFormat");
	localUserSettings.displayDateTimeFormat		= GM_config.get("displayDateTimeFormat");
	localUserSettings.displayLocale				= GM_config.get("displayLocale");

	localUserSettings.scrollOffset				= GM_config.get("scrollOffset");

	localUserSettings.replyInNewTab				= GM_config.get("replyInNewTab");
	localUserSettings.invertReplyLinkBehaviour	= GM_config.get("invertReplyLinkBehaviour");

	localUserSettings.userName					= GM_config.get("userName");
	localUserSettings.userEmail					= GM_config.get("userEmail");
	localUserSettings.userWebsite				= GM_config.get("userWebsite");
	localUserSettings.tmtsotsomi				= GM_config.get("tmtsotsomi");

	localUserSettings.showCountdown				= GM_config.get("showCountdown");

	// TODO load remote user settings

	// setup locale: use browser setting as default
	if (!localUserSettings.displayLocale)
		localUserSettings.displayLocale = window.navigator.userLanguage || window.navigator.language;

	settingsLoaded = true;
}

function ShowSettings()
{
	// show settings
	GM_config.open();
}

function ImportData()
{
	var data = GM_config.fields["importExportData"].node.value;
	try
	{
		data = JSON.parse(data);
	}
	catch (ex)
	{
		alert("Invalid data format.");
		return;
	}

	if (!confirm("Import data?"))
		return;
	var deleteMissingValues = !confirm("Add and update only (no delete)?");

	var valueNames = GM_listValues().reduce(function(map, valueName)
		{
			if (valueName.startsWith("LastVisit_"))
				map[valueName] = null;
			return map;
		}, {});

	for (var valueName in data)
	{
		if (valueName.startsWith("LastVisit_"))
		{
			GM_setValue(valueName, data[valueName]);
			delete valueNames[valueName];
		}
	}

	if (deleteMissingValues)
	{
		for (var valueName in valueNames)
			GM_deleteValue(valueName);
	}

	// import done
	alert("Note: Refresh page to see changes.");
}

function ExportData()
{
	var data = {};
	var valueNames = GM_listValues();
	jQuery.each(valueNames,
		function(i, valueName)
		{
			if (valueName.startsWith("LastVisit_"))
				data[valueName] = GM_getValue(valueName);
		});
	GM_config.fields["importExportData"].node.value = JSON.stringify(data);
}

function TextToNumber(text)
{
	switch (text)
	{
		case "zero": return 0;
		case "one": return 1;
		case "two": return 2;
		case "three": return 3;
		case "four": return 4;
		case "five": return 5;
		case "six": return 6;
		case "seven": return 7;
		case "eight": return 8;
		case "nine": return 9;
		case "ten": return 10;
		case "eleven": return 11;
		case "twelve": return 12;
	}
}

function AnswerSecretQuestion()
{
	var input = $("input[name='seckrit_question']");

	// get question
	var contents = input.parent().contents();
	contents = contents.slice(contents.index(input) + 1, contents.index(input.next()));
	var question = contents.text();

	// evaluate question
	var answer = 0;
	var regex = /((\+|-)\s+)?(one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b/gi;
	var match;
	while (match = regex.exec(question))
	{
		var operand = match[2];
		var number = TextToNumber(match[3]);
		if (number)
		{
			if (operand == "-")
				number = -number;
			answer += number;
		}
	}

	// set answer
	input.attr("value", answer.toString());
}

function GetLastVisitSettings(blogID)
{
	var lastVisitSettingsText = GM_getValue("LastVisit_" + blogID);
	lastVisitSettingsText = lastVisitSettingsText != null ? lastVisitSettingsText.split("|") : [];

	var lastVisitSettings = { blogID: blogID };

	var index = 0;
	lastVisitSettings.lastVisitText		= lastVisitSettingsText.length >= index ? lastVisitSettingsText[index] : null; index++;
	lastVisitSettings.lastCommentText	= lastVisitSettingsText.length >= index ? lastVisitSettingsText[index] : null; index++;
	lastVisitSettings.lastCommentCount	= lastVisitSettingsText.length >= index ? lastVisitSettingsText[index] : null; index++;
	lastVisitSettings.totalCommentCount	= lastVisitSettingsText.length >= index ? lastVisitSettingsText[index] : null; index++;

	lastVisitSettings.lastVisit			= lastVisitSettings.lastVisitText		? moment(lastVisitSettings.lastVisitText).utc() : null;
	lastVisitSettings.lastComment		= lastVisitSettings.lastCommentText		? moment(lastVisitSettings.lastCommentText).utc() : null;
	lastVisitSettings.lastCommentCount	= lastVisitSettings.lastCommentCount	? parseInt(lastVisitSettings.lastCommentCount) : 0;
	lastVisitSettings.totalCommentCount	= lastVisitSettings.totalCommentCount	? parseInt(lastVisitSettings.totalCommentCount) : 0;

	// validate datetimes
	if (lastVisitSettings.lastVisit && !lastVisitSettings.lastVisit.isValid())
	{
		lastVisitSettings.lastVisit = null;
		lastVisitSettings.lastVisitText = null;
	}
	if (lastVisitSettings.lastComment && !lastVisitSettings.lastComment.isValid())
	{
		lastVisitSettings.lastComment = null;
		lastVisitSettings.lastCommentText = null;
	}

	return lastVisitSettings;
}

function SetLastVisitSettings(blogID, lastVisitText, lastCommentText, lastCommentCount, totalCommentCount)
{
	var lastVisitSettingsText = (lastVisitText ? lastVisitText : "") + "|" + (lastCommentText ? lastCommentText : "") + "|" + lastCommentCount.toString() + "|" + totalCommentCount.toString();
	//console.log("setValue: " + blogID + ": " + lastVisitSettingsText);
	GM_setValue("LastVisit_" + blogID, lastVisitSettingsText);
}

function ParseServerTime(text)
{
	// parse server time (includes year since 2016-04-03!)
	var serverTime = moment.tz(text, scriptSettings.serverDateTimeFormat, scriptSettings.serverTimeZone);
	return serverTime;

	/* obsolete
	// parse server time (which doesn't include year)
	var dateTimeText = referenceTime.format("YYYY ") + text;
	var serverTime = moment.tz(dateTimeText, "YYYY " + scriptSettings.serverDateTimeFormat, scriptSettings.serverTimeZone);
	// try to fix year
	if (!serverTime.isValid())
	{
		// possible special case: wrong year and date is leap day
		dateTimeText = referenceTime.clone().add(1, "y").format("YYYY ") + text;
		serverTime = moment.tz(dateTimeText, "YYYY " + scriptSettings.serverDateTimeFormat, scriptSettings.serverTimeZone);
	}
	if (serverTime.format() < referenceTime.format())
		serverTime.add(1, "y");
	return serverTime;
	*/
}

function GetBlogPostNewCommentsIndicator(commentCount, newCommentCount, lastVisit, url)
{
	var indicator = newCommentCount !== 0 ? scriptSettings.newCommentsIndicator : scriptSettings.noNewCommentsIndicator;
	indicator = indicator
		.replace("{0}", commentCount)
		.replace("{1}", newCommentCount)
		.replace("{2}", lastVisit ? lastVisit.clone().local().locale(localUserSettings.displayLocale).format(localUserSettings.displayDateTimeFormat) : "ever")
		.replace("{3}", url ? url : "#newComment0");
	return indicator;
}

function ProcessReplyLinks()
{
	if (localUserSettings.replyInNewTab || localUserSettings.invertReplyLinkBehaviour)
	{
		// add target _blank
		$("div.reply>a[target!='_blank']").attr("target", "_blank");
	}
	else
	{
		// remove target _blank
		$("div.reply>a[target='_blank']").removeAttr("target");
	}

	$("div.reply>a").click(
		function(e)
		{
			if (localUserSettings.invertReplyLinkBehaviour && e.ctrlKey)
			{
				// open link in same window
				var href = $(this).attr("href");;
				document.location.href = href;
				e.preventDefault();
			}
		});
}

function ProcessCommentForm()
{
	if (localUserSettings.userName)
		$("input[name='comment_name']").attr("value", localUserSettings.userName);
	if (localUserSettings.userEmail)
		$("input[name='comment_email']").attr("value", localUserSettings.userEmail);
	if (localUserSettings.userWebsite)
		$("input[name='comment_website']").attr("value", localUserSettings.userWebsite);

	if (localUserSettings.tmtsotsomi)
		AnswerSecretQuestion();
}

function ProcessArchiveList()
{
	var newCommentCountSum = 0;
	var totalCommentCountSum = 0;
	var newBlogPostWithCommentsCount = 0;

	// add new comment indicators to archive list
	jQuery.each(jQuery(".archiveBody h3"),
		function(i, e)
		{
			var element = $(e);

			var blogTitle = element.children("a");
			var dateElement = element.children("span.date");
			if (blogTitle && dateElement)
			{
				// get blog ID
				blogTitle = $(blogTitle[0]);
				var blogID = blogTitle.attr("href");

				// get last visit settings from persistent storage
				var lastVisit = GetLastVisitSettings(blogID);

				// parse comment count and date
				dateElement = $(dateElement[0]);
				var dateElementText = dateElement.text();
				var dateElementTextSplit = dateElementText.split("-");
				if (dateElementTextSplit.length >= 3 && dateElementTextSplit[1].indexOf("comments") >= 0)
				{
					var index = dateElementTextSplit[1].indexOf("comments");
					var commentCount = parseInt(dateElementTextSplit[1].substring(0, index).trim());
					if (isNaN(commentCount))
						commentCount = 0;
					var dateText = dateElementTextSplit[2].trim();
					var date = moment.tz(dateText, scriptSettings.serverDateFormat, scriptSettings.serverTimeZone);

					// set element attributes
					dateElement.attr("data-utcvalue", date.format());
					dateElement.attr("data-commentCount", commentCount);

					if (date.isValid())
					{
						// fix time format
						dateElement.text(" - " + date.clone().locale(localUserSettings.displayLocale).format(localUserSettings.displayDateFormat));
						dateElement.attr("title", dateElementText);
					}

					// add new comment indicator
					var newCommentCount = commentCount - lastVisit.totalCommentCount;
					if (newCommentCount < 0)
						// note: archive list is cached serverside and may have out-dated comment counts
						newCommentCount = 0;
					var indicator = GetBlogPostNewCommentsIndicator(commentCount, newCommentCount, lastVisit.lastVisit, blogID);
					$(indicator).insertAfter(dateElement);

					newCommentCountSum += newCommentCount;
					totalCommentCountSum += commentCount;
					if (!lastVisit.lastVisit && commentCount > 0)
						newBlogPostWithCommentsCount++;
				}
			}
		});

	// add comment counts to archive list header
	var header = scriptSettings.archiveListHeader
		.replace("{0}", newCommentCountSum)
		.replace("{1}", totalCommentCountSum)
		.replace("{2}", newCommentCountSum !== 0 ? "newComments" : "");
	jQuery(".archiveBody h1").append(header);

	if (newBlogPostWithCommentsCount >= 2)
	{
		var markRead = $(scriptSettings.archiveListMarkRead);
		markRead.click(
			function (e)
			{
				e.stopPropagation();
				e.preventDefault();

				if (confirm("Do you really want to mark all comments of all new blog posts as read?"))
				{
					// mark comments as read
					jQuery.each(jQuery(".archiveBody h3"),
						function(i, e)
						{
							var element = $(e);

							var blogTitle = element.children("a");
							var dateElement = element.children("span.date");
							if (blogTitle && dateElement)
							{
								// get blog ID
								blogTitle = $(blogTitle[0]);
								var blogID = blogTitle.attr("href");

								// get last visit settings from persistent storage
								var lastVisit = GetLastVisitSettings(blogID);
								if (!lastVisit.lastVisit)
								{
									// update last visit settings
									var nowText = moment().utc().format();
									var commentCount = parseInt(dateElement.attr("data-commentCount"));
									SetLastVisitSettings(blogID, nowText, nowText, 0, commentCount);
								}
							}
						});

					// reload page
					document.location.reload();
				}
			});
		jQuery(".archiveBody h1").append(markRead);
	}
}

function GetBlogPostReferenceTime(fixTimeFormat)
{
	var referenceTime;

	var dateElement = jQuery(".date").first();
	if (dateElement.length > 0)
	{
		// try to use date of blog post as reference
		dateElement = $(dateElement[0]);
		var htmlText = dateElement.html();
		var index = htmlText.indexOf("<br>");
		var timeText = index < 0 ? htmlText : htmlText.substring(index + 4);
		referenceTime = moment.tz(timeText, scriptSettings.serverDateFormat, scriptSettings.serverTimeZone);

		if (referenceTime.isValid() && fixTimeFormat)
		{
			// fix time format
			htmlText = index < 0 ? "" : htmlText.substring(0, index + 4);
			htmlText += referenceTime.clone().locale(localUserSettings.displayLocale).format(localUserSettings.displayDateFormat);
			dateElement.html(htmlText);
			dateElement.attr("title", timeText);
		}
	}

	if (referenceTime == null || !referenceTime.isValid())
	{
		// use first of january of current year as reference
		var currentServerYear = moment().tz(scriptSettings.serverTimeZone).year();
		referenceTime = moment.tz(currentServerYear, "YYYY", scriptSettings.serverTimeZone);
	}

	return referenceTime;
}

function ProcessBlogPosts()
{
	var regexCommentLinkCommentCount = /(\d+)\s+comment(s)?/i;

	// detect blog post with comments (note: multiple posts means blog main page -> skip)
	var blogTitles = jQuery(".entryBody .title a[href]");
	var isSingleBlogPost = blogTitles.length == 1;
	jQuery.each(blogTitles,
		function(i, e)
		{
			// get blog ID
			var blogTitle = $(e);
			var blogID = blogTitle.attr("href");

			// get body of blog post
			var blogBody = $(blogTitle.closest(".entryBody"));

			// get comment link (blog posts on main page)
			var commentLink = blogBody.find(".mainCommentLink a[href]")
			commentLink = commentLink.length == 1 ? $(commentLink[0]) : null;

			// get last visit settings from persistent storage
			var lastVisit = GetLastVisitSettings(blogID);

			if (isSingleBlogPost)
			{
				// count comments with same time as last comment and get (new) last comment
				var currentLastCommentCount = 0;
				var newLastCommentText = null;
				jQuery.each(blogBody.find(".commentdate[data-utcvalue]"),
					function(i, e)
					{
						var element = $(e);

						var utcTime = moment(element.attr("data-utcvalue")).utc();
						var utcTimeText = utcTime.format();
						// check if comment has same time then last comment
						if (lastVisit.lastCommentText && utcTimeText == lastVisit.lastCommentText)
							currentLastCommentCount++;
						// get (new) last comment
						if (newLastCommentText == null || newLastCommentText < utcTimeText)
							newLastCommentText = utcTimeText;
					});
				if (!lastVisit.lastCommentText)
					currentLastCommentCount = -1;

				// mark new comments
				var commentCount = 0;
				var newCommentCount = 0;
				var newLastCommentCount = 0;
				jQuery.each(blogBody.find(".commentdate"),
					function(i, e)
					{
						var element = $(e);
						commentCount++;

						var utcTime = moment(element.attr("data-utcvalue")).utc();
						var utcTimeText = utcTime.format();
						// check if comment is new
						if (lastVisit.lastCommentText == null || utcTimeText > lastVisit.lastCommentText || currentLastCommentCount != lastVisit.lastCommentCount && utcTimeText >= lastVisit.lastCommentText)
						{
							var indicator = scriptSettings.newCommentIndicator.replace("{0}", "newComment" + newCommentCount).replace("{1}", "#newComment" + (newCommentCount + 1));
							$(indicator).insertAfter(element);
							newCommentCount++;
						}
						// count last comments
						if (newLastCommentText == utcTimeText)
							newLastCommentCount++;
					});

				// only add indicator to title if commenting is/was possible
				if (commentCount > 0 || jQuery(".commentform").length > 0)
				{
					// add indicator to title of blog post
					var indicator = GetBlogPostNewCommentsIndicator(commentCount, newCommentCount, lastVisit.lastVisit, null);
					$(indicator).insertAfter(blogTitle);
				}

				// remember last visit
				var lastVisitText = currentVisit.format();
				SetLastVisitSettings(blogID, lastVisitText, newLastCommentText, newLastCommentCount, commentCount);
			}
			else if (commentLink !== null)
			{
				// process blog post on main page
				var match = regexCommentLinkCommentCount.exec(commentLink.text());
				if (match)
				{
					var commentCount = parseInt(match[1]);

					// add new comment indicators
					var newCommentCount = commentCount - lastVisit.totalCommentCount;
					if (newCommentCount < 0)
						// note: archive list is cached serverside and may have out-dated comment counts
						newCommentCount = 0;
					var indicator = GetBlogPostNewCommentsIndicator(commentCount, newCommentCount, lastVisit.lastVisit, blogID);
					$(indicator).insertAfter(blogTitle);
					$(indicator).insertAfter(commentLink);
				}
			}
		});
}

function ShowCountdown()
{
	// show on main page and archive page only for now
	//if (document.location.pathname != "/" && !document.location.pathname.startsWith("/archive"))
	var archiveBody = jQuery(".archiveBody h3")
	if (archiveBody.length <= 0)
		return;

	var snippet = scriptSettings.countdownSnippet;
	var menuElement = jQuery("div.title>div.menu")
	$(snippet).insertAfter(menuElement.parent());
}


// try to register menu entry for config dialog
try
{
	GM_registerMenuCommand("Thimbleweed Park™ Blog fixes - Settings", ShowSettings);
}
catch (ex)
{
	console.log(ex);
}

jQuery(document).ready(Main);