legit

Fork of https://git.icyphox.sh/legit

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
// Copyright 2025 Shota FUJI <pockawoooh@gmail.com>
// SPDX-License-Identifier: MIT
//
// ===
//
// This script scans every "<time>" elements with "data-local-format" attribute,
// replaces each element's text node with local datetime.
// If a "<time>" element does not have valid "datetime" attribute, this script
// skips the "<time>" element.
//
// "data-local-format" attribute accepts one of the below:
// - "datetime" ... displays date and time (default)
// - "date"     ... displays date
// - "time"     ... displays time
// - "relative" ... displays relative datetime (e.g. 3 days ago)
//
// This script intentionally uses scratch serialization instead of locale-aware
// APIs; locale strings often includes non-English characters thus does not
// incorporate with the rest of a page. As majority of readers are familiar with
// softwares, using RFC3339-ish style date and time format fits better than an
// uncontrollable locale string.

const MS_PER_MIN = 60 * 1000;
const MS_PER_HOUR = MS_PER_MIN * 60;
const MS_PER_DAY = MS_PER_HOUR * 24;
const MS_PER_WEEK = MS_PER_DAY * 7;
const MS_PER_MONTH = MS_PER_DAY * 30;
const MS_PER_YEAR = MS_PER_MONTH * 12;

function toTimeString(date) {
	return date.getHours().toString().padStart(2, "0") + ":"
		+ date.getMinutes().toString().padStart(2, "0") + ":"
		+ date.getSeconds().toString().padStart(2, "0");
}

function toDateString(date) {
	// I don't expect this software/code will survive for 8000yrs.
	return date.getFullYear().toString().padStart(4, "0") + "-"
		// stupid spec made by American
		+ (date.getMonth() + 1).toString().padStart(2, "0") + "-"
		+ date.getDate().toString().padStart(2, "0");
}

function toDateTimeString(date) {
	return toDateString(date) + " " + toTimeString(date);
}

function toRelativeDatetimeString(date, now = Date.now()) {
	if (typeof Intl.RelativeTimeFormat !== "function") {
		return toDateTimeString(date);
	}

	const formatter = new Intl.RelativeTimeFormat(document.documentElement.lang);
	const delta = now - date.valueOf();

	if (delta < MS_PER_MIN) {
		return formatter.format(-Math.floor(delta / 1000), "second");
	}

	if (delta < MS_PER_HOUR) {
		return formatter.format(-Math.floor(delta / MS_PER_MIN), "minute");
	}

	if (delta < MS_PER_DAY) {
		return formatter.format(-Math.floor(delta / MS_PER_HOUR), "hour");
	}

	if (delta < MS_PER_WEEK) {
		return formatter.format(-Math.floor(delta / MS_PER_DAY), "day");
	}

	if (delta < MS_PER_MONTH) {
		return formatter.format(-Math.floor(delta / MS_PER_WEEK), "week");
	}

	if (delta < MS_PER_YEAR) {
		return formatter.format(-Math.floor(delta / MS_PER_MONTH), "month");
	}

	return formatter.format(-Math.floor(delta / MS_PER_YEAR), "year");
}

const targets = document.querySelectorAll("time[data-local-format]");
for (const target of targets) {
	const datetime = target.dateTime && Date.parse(target.dateTime);
	if (Number.isNaN(datetime)) {
		continue;
	}

	const originalText = target.textContent.trim();
	const format = target.dataset.localFormat || "datetime";

	switch (format) {
		case "datetime":
			target.textContent = toDateTimeString(new Date(datetime));
			break;
		case "date":
			target.textContent = toDateString(new Date(datetime));
			break;
		case "time":
			target.textContent = toTimeString(new Date(datetime));
			break;
		case "relative":
			target.textContent = toRelativeDatetimeString(new Date(datetime));
			break;
		default:
			console.warn(
				`Found unknown value on data-local-format attribute: "${target.dataset.localFormat}".\n`
					+ `Available values are: "datetime", "date", "time"\n`
					+ "Skipping text replacement.",
			);
			break;
	}

	target.title = format !== "relative"
		? `${originalText} (${toRelativeDatetimeString(new Date(datetime))})`
		: new Date(datetime).toString();
}