// ==UserScript== // @name AD2YOLD // @namespace YOLD // @version 1.1.0 // @description Replaces Gregorian AD with Discordian YOLD. Hail Eris. // @match *://*/* // @run-at document-start // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (function () { 'use strict'; const CONFIG = { // 'long' > Prickle-Prickle, Bureaucracy 60, 3192 YOLD 23:05:32 // 'compact' > 60-bcy-3192(PP)23:05:32 hr-2 format: 'long', relative: true, // For ambiguous numeric dates like dd/mm/yyyy (both parts <= 12) using / or -. // Dotted dates (dd.mm.yyyy) are always treated as dd.mm.yyyy. ambiguousOrder: 'DMY', // 'DMY' or 'MDY' includeWeekday: true, // long format: "Sweetmorn, Chaos 5, 3192 YOLD" showHolydays: true, // long format: append "(Mungday)" etc. hoverOriginal: true, // hover a converted date to see the original text }; // Persisted choices const store = { get: (k, def) => (typeof GM_getValue === 'function' ? GM_getValue(k, def) : def), set: (k, v) => { if (typeof GM_setValue === 'function') GM_setValue(k, v); }, }; CONFIG.format = store.get('format', CONFIG.format); CONFIG.relative = store.get('relative', CONFIG.relative); // Calendar const SEASONS = ['Chaos', 'Discord', 'Confusion', 'Bureaucracy', 'The Aftermath']; const SEASONS_SHORT = ['chs', 'dsc', 'cfn', 'bcy', 'afm']; const WEEKDAYS = ['Sweetmorn', 'Boomtime', 'Pungenday', 'Prickle-Prickle', 'Setting Orange']; const WEEKDAYS_SHORT = ['SM', 'BT', 'PD', 'PP', 'SO']; const HOLYDAYS = { 5: ['Mungday', 'Mojoday', 'Syaday', 'Zaraday', 'Maladay'], 50: ['Chaoflux', 'Discoflux', 'Confuflux', 'Bureflux', 'Afflux'], }; const CUM_DAYS = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; const YOLD_OFFSET = 1166; const isLeap = (y) => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0; const daysIn = (y, m) => [31, isLeap(y) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1]; const pad2 = (n) => String(n).padStart(2, '0'); function discordianParts(y, m, d) { const yold = y + YOLD_OFFSET; if (m === 2 && d === 29) return { tib: true, yold }; const doy = CUM_DAYS[m - 1] + d; // non-leap day of year; St. Tib's Day doesn't count return { tib: false, yold, season: Math.floor((doy - 1) / 73), day: ((doy - 1) % 73) + 1, weekday: (doy - 1) % 5, }; } function timeString(time) { if (!time) return ''; return `${pad2(time.h)}:${pad2(time.mi)}` + (time.s !== null ? `:${pad2(time.s)}` : ''); } // Relative time const MINUTE = 6e4, HOUR = 36e5, DAY = 864e5, WEEK = 6048e5, MONTH = 2592e6, YEAR = 31536e6; function relativeString(y, mo, d, time) { const then = new Date(y, mo - 1, d, time ? time.h : 0, time ? time.mi : 0, time && time.s ? time.s : 0); let t = Date.now() - then.getTime(); let future = t < 0; t = Math.abs(t); if (!time) { const today = new Date(); today.setHours(0, 0, 0, 0); const days = Math.round(Math.abs(today.getTime() - then.getTime()) / DAY); if (days === 0) return ''; t = days * DAY; future = then.getTime() > today.getTime(); } let n, unit; if (t < 357e4) { n = Math.round(t / MINUTE); unit = 'mi'; } else if (t < 846e5) { n = Math.round(t / HOUR); unit = 'hr'; } else if (t < 6.5 * DAY) { n = Math.round(t / DAY); unit = 'dy'; } else if (t < 3.5 * WEEK) { n = Math.round(t / WEEK); unit = 'wk'; } else if (t < 11.5 * MONTH) { n = Math.round(t / MONTH); unit = 'mo'; } else { n = Math.round(t / YEAR); unit = 'yr'; } return `${unit}${future ? '+' : '-'}${n}`; } // Rendering function renderLong({ y, mo, d, time }) { const p = discordianParts(y, mo, d); let out; if (p.tib) { out = `St. Tib's Day, ${p.yold} YOLD`; } else { out = (CONFIG.includeWeekday ? WEEKDAYS[p.weekday] + ', ' : '') + `${SEASONS[p.season]} ${p.day}, ${p.yold} YOLD`; if (CONFIG.showHolydays && HOLYDAYS[p.day]) out += ` (${HOLYDAYS[p.day][p.season]})`; } if (time) out += ' ' + timeString(time); return out; } function renderCompact({ y, mo, d, time }) { const p = discordianParts(y, mo, d); if (p.tib) { return `tib-${p.yold}` + (time ? ' ' + timeString(time) : ''); } let out = `${p.day}-${SEASONS_SHORT[p.season]}-${p.yold}(${WEEKDAYS_SHORT[p.weekday]})`; const holyday = HOLYDAYS[p.day] ? HOLYDAYS[p.day][p.season] : null; if (holyday) { out += ` ${holyday}!`; if (time) out += ' ' + timeString(time); return out; } out += timeString(time); if (CONFIG.relative) { const rel = relativeString(y, mo, d, time); if (rel) out += ' ' + rel; } return out; } const render = (meta) => (CONFIG.format === 'compact' ? renderCompact(meta) : renderLong(meta)); const META = new WeakMap(); const MARK = 'discordian-date'; function rerenderAll() { document.querySelectorAll(`.${MARK}`).forEach((el) => { const meta = META.get(el); if (meta) el.textContent = render(meta); }); } // Date detection const MONTHS = { // English january: 1, jan: 1, february: 2, feb: 2, march: 3, mar: 3, april: 4, apr: 4, may: 5, june: 6, jun: 6, july: 7, jul: 7, august: 8, aug: 8, september: 9, sept: 9, sep: 9, october: 10, oct: 10, november: 11, nov: 11, december: 12, dec: 12, // German januar: 1, februar: 2, 'märz': 3, maerz: 3, mai: 5, juni: 6, juli: 7, oktober: 10, dezember: 12, dez: 12, okt: 10, 'mär': 3, }; const MONTH_ALT = Object.keys(MONTHS).sort((a, b) => b.length - a.length).join('|'); const PATTERNS = [ // 1963-11-22, 1963/11/22, 1963.11.22 { re: /(? [+m[1], +m[3], +m[4]], }, // 22.11.1963, 11/22/1963, 22-11-1963 { re: /(? { const a = +m[1], b = +m[3], y = +m[4]; if (m[2] === '.') return [y, b, a]; if (a > 12) return [y, b, a]; if (b > 12) return [y, a, b]; return CONFIG.ambiguousOrder === 'MDY' ? [y, a, b] : [y, b, a]; }, }, // 22 November 1963, 22nd of Nov, 1963, 22. November 1963 { re: new RegExp(`(? [+m[3], MONTHS[m[2].toLowerCase()], +m[1]], }, // November 22, 1963 / Nov. 22nd 1963 { re: new RegExp(`\\b(${MONTH_ALT})\\.?\\s+(\\d{1,2})(?:st|nd|rd|th)?,?\\s+(\\d{4})(?!\\d)`, 'gi'), parse: (m) => [+m[3], MONTHS[m[1].toLowerCase()], +m[2]], }, ]; // Optional: "T23:05:32", ", 23:05", " at 5:30 PM", " um 14:30" const TIME_RE = /(?:T|,?\s+(?:at\s+|um\s+|@\s*)?)(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*([ap])\.?m\b\.?)?(?![\d:])/iy; function parseTimeAt(text, pos) { TIME_RE.lastIndex = pos; const m = TIME_RE.exec(text); if (!m) return null; let h = +m[1]; const mi = +m[2]; const s = m[3] !== undefined ? +m[3] : null; if (m[4]) { if (h < 1 || h > 12) return null; h = (h % 12) + (m[4].toLowerCase() === 'p' ? 12 : 0); } else if (h > 23) { return null; } if (mi > 59 || (s !== null && s > 59)) return null; return { h, mi, s, len: m[0].length }; } function findDates(text) { const found = []; for (const { re, parse } of PATTERNS) { re.lastIndex = 0; let m; while ((m = re.exec(text))) { const [y, mo, d] = parse(m); if (y < 1000 || y > 2999 || !mo || mo < 1 || mo > 12 || d < 1 || d > daysIn(y, mo)) continue; let end = m.index + m[0].length; const time = parseTimeAt(text, end); if (time) end += time.len; found.push({ start: m.index, end, y, mo, d, time, raw: text.slice(m.index, end) }); } } // Sort and drop overlaps found.sort((a, b) => a.start - b.start || b.end - a.end); const result = []; let lastEnd = -1; for (const f of found) { if (f.start >= lastEnd) { result.push(f); lastEnd = f.end; } } return result; } // DOM const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT', 'OPTION', 'CODE', 'PRE', 'KBD', 'SAMP']); function shouldSkip(node) { const p = node.parentElement; if (!p) return true; if (SKIP_TAGS.has(p.tagName)) return true; if (p.isContentEditable) return true; if (p.closest(`.${MARK}`)) return true; return false; } function convertTextNode(node) { const text = node.nodeValue; if (!text || text.length < 6 || !/\d/.test(text)) return; if (shouldSkip(node)) return; const dates = findDates(text); if (!dates.length) return; const frag = document.createDocumentFragment(); let pos = 0; for (const dt of dates) { if (dt.start > pos) frag.appendChild(document.createTextNode(text.slice(pos, dt.start))); const meta = { y: dt.y, mo: dt.mo, d: dt.d, time: dt.time }; const span = document.createElement('span'); span.className = MARK; span.textContent = render(meta); if (CONFIG.hoverOriginal) span.title = dt.raw; META.set(span, meta); frag.appendChild(span); pos = dt.end; } if (pos < text.length) frag.appendChild(document.createTextNode(text.slice(pos))); node.parentNode.replaceChild(frag, node); } function scan(root) { if (!root) return; if (root.nodeType === Node.TEXT_NODE) { convertTextNode(root); return; } if (root.nodeType !== Node.ELEMENT_NODE && root.nodeType !== Node.DOCUMENT_NODE) return; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes = []; let n; while ((n = walker.nextNode())) nodes.push(n); nodes.forEach(convertTextNode); } // Dynamically loaded content const pending = new Set(); let scheduled = false; function flush() { scheduled = false; const items = [...pending]; pending.clear(); for (const item of items) { if (item.isConnected) scan(item); } } new MutationObserver((mutations) => { for (const mu of mutations) { if (mu.type === 'characterData') { pending.add(mu.target); } else { mu.addedNodes.forEach((n) => pending.add(n)); } } if (!scheduled && pending.size) { scheduled = true; queueMicrotask(flush); } }).observe(document.documentElement, { childList: true, subtree: true, characterData: true }); function initialize() { if (!document.body) return; scan(document.body); } if (document.body) { initialize(); } else { document.addEventListener('DOMContentLoaded', initialize, { once: true }); } setInterval(() => { if (CONFIG.format === 'compact' && CONFIG.relative) rerenderAll(); }, 60000); // Manager if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('Format: long', () => { CONFIG.format = 'long'; store.set('format', 'long'); rerenderAll(); }); GM_registerMenuCommand('Format: compact (60-bcy-3192(PP)23:05:32 hr-2)', () => { CONFIG.format = 'compact'; store.set('format', 'compact'); rerenderAll(); }); GM_registerMenuCommand('Toggle relative time (compact)', () => { CONFIG.relative = !CONFIG.relative; store.set('relative', CONFIG.relative); rerenderAll(); }); } })();