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

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