/* ui-holidays.js - Standalone holiday repository browser */ (function () { "use strict"; const { getTarotCardDisplayName, getTarotCardSearchAliases } = window.TarotCardImages || {}; const state = { initialized: false, referenceData: null, magickDataset: null, selectedYear: new Date().getFullYear(), selectedSource: "all", searchQuery: "", holidays: [], filteredHolidays: [], selectedHolidayId: null, planetsById: new Map(), signsById: new Map(), godsById: new Map(), hebrewById: new Map(), calendarData: {} }; const TAROT_TRUMP_NUMBER_BY_NAME = { "the fool": 0, fool: 0, "the magus": 1, magus: 1, magician: 1, "the high priestess": 2, "high priestess": 2, "the empress": 3, empress: 3, "the emperor": 4, emperor: 4, "the hierophant": 5, hierophant: 5, "the lovers": 6, lovers: 6, "the chariot": 7, chariot: 7, strength: 8, lust: 8, "the hermit": 9, hermit: 9, fortune: 10, "wheel of fortune": 10, justice: 11, "the hanged man": 12, "hanged man": 12, death: 13, temperance: 14, art: 14, "the devil": 15, devil: 15, "the tower": 16, tower: 16, "the star": 17, star: 17, "the moon": 18, moon: 18, "the sun": 19, sun: 19, aeon: 20, judgement: 20, judgment: 20, universe: 21, world: 21, "the world": 21 }; const HEBREW_MONTH_ALIAS_BY_ID = { nisan: ["nisan"], iyar: ["iyar"], sivan: ["sivan"], tammuz: ["tamuz", "tammuz"], av: ["av"], elul: ["elul"], tishrei: ["tishri", "tishrei"], cheshvan: ["heshvan", "cheshvan", "marcheshvan"], kislev: ["kislev"], tevet: ["tevet"], shvat: ["shevat", "shvat"], adar: ["adar", "adar i", "adar 1"], "adar-ii": ["adar ii", "adar 2"] }; const MONTH_NAME_TO_INDEX = { january: 0, february: 1, march: 2, april: 3, may: 4, june: 5, july: 6, august: 7, september: 8, october: 9, november: 10, december: 11 }; const GREGORIAN_MONTH_ID_TO_ORDER = { january: 1, february: 2, march: 3, april: 4, may: 5, june: 6, july: 7, august: 8, september: 9, october: 10, november: 11, december: 12 }; function getElements() { return { sourceSelectEl: document.getElementById("holiday-source-select"), yearInputEl: document.getElementById("holiday-year-input"), searchInputEl: document.getElementById("holiday-search-input"), searchClearEl: document.getElementById("holiday-search-clear"), countEl: document.getElementById("holiday-count"), listEl: document.getElementById("holiday-list"), detailNameEl: document.getElementById("holiday-detail-name"), detailSubEl: document.getElementById("holiday-detail-sub"), detailBodyEl: document.getElementById("holiday-detail-body") }; } function normalizeText(value) { return String(value || "").trim(); } function normalizeSearchValue(value) { return String(value || "").trim().toLowerCase(); } function cap(value) { const text = normalizeText(value); return text ? text.charAt(0).toUpperCase() + text.slice(1) : text; } function normalizeTarotName(value) { return String(value || "") .trim() .toLowerCase() .replace(/\s+/g, " "); } function resolveTarotTrumpNumber(cardName) { const key = normalizeTarotName(cardName); if (!key) { return null; } if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, key)) { return TAROT_TRUMP_NUMBER_BY_NAME[key]; } const withoutLeadingThe = key.replace(/^the\s+/, ""); if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, withoutLeadingThe)) { return TAROT_TRUMP_NUMBER_BY_NAME[withoutLeadingThe]; } return null; } function getDisplayTarotName(cardName, trumpNumber) { if (!cardName) { return ""; } if (typeof getTarotCardDisplayName !== "function") { return cardName; } if (Number.isFinite(Number(trumpNumber))) { return getTarotCardDisplayName(cardName, { trumpNumber: Number(trumpNumber) }) || cardName; } return getTarotCardDisplayName(cardName) || cardName; } function normalizeCalendarText(value) { return String(value || "") .normalize("NFKD") .replace(/[\u0300-\u036f]/g, "") .replace(/['`]/g, "") .toLowerCase() .replace(/[^a-z0-9]+/g, " ") .trim(); } function readNumericPart(parts, partType) { const raw = parts.find((part) => part.type === partType)?.value; if (!raw) { return null; } const digits = String(raw).replace(/[^0-9]/g, ""); if (!digits) { return null; } const parsed = Number(digits); return Number.isFinite(parsed) ? parsed : null; } function getGregorianMonthOrderFromId(monthId) { if (!monthId) { return null; } const key = String(monthId).trim().toLowerCase(); const value = GREGORIAN_MONTH_ID_TO_ORDER[key]; return Number.isFinite(value) ? value : null; } function parseMonthDayStartToken(token) { const match = String(token || "").match(/(\d{2})-(\d{2})/); if (!match) { return null; } const month = Number(match[1]); const day = Number(match[2]); if (!Number.isFinite(month) || !Number.isFinite(day)) { return null; } return { month, day }; } function createDateAtNoon(year, monthIndex, dayOfMonth) { return new Date(Math.trunc(year), monthIndex, Math.trunc(dayOfMonth), 12, 0, 0, 0); } function computeWesternEasterDate(year) { const y = Math.trunc(Number(year)); if (!Number.isFinite(y)) { return null; } // Meeus/Jones/Butcher Gregorian algorithm. const a = y % 19; const b = Math.floor(y / 100); const c = y % 100; const d = Math.floor(b / 4); const e = b % 4; const f = Math.floor((b + 8) / 25); const g = Math.floor((b - f + 1) / 3); const h = (19 * a + b - d - g + 15) % 30; const i = Math.floor(c / 4); const k = c % 4; const l = (32 + 2 * e + 2 * i - h - k) % 7; const m = Math.floor((a + 11 * h + 22 * l) / 451); const month = Math.floor((h + l - 7 * m + 114) / 31); const day = ((h + l - 7 * m + 114) % 31) + 1; return createDateAtNoon(y, month - 1, day); } function computeNthWeekdayOfMonth(year, monthIndex, weekday, ordinal) { const y = Math.trunc(Number(year)); if (!Number.isFinite(y)) { return null; } const first = createDateAtNoon(y, monthIndex, 1); const firstWeekday = first.getDay(); const offset = (weekday - firstWeekday + 7) % 7; const dayOfMonth = 1 + offset + (Math.trunc(ordinal) - 1) * 7; const daysInMonth = new Date(y, monthIndex + 1, 0).getDate(); if (dayOfMonth > daysInMonth) { return null; } return createDateAtNoon(y, monthIndex, dayOfMonth); } function resolveGregorianDateRule(rule) { const key = String(rule || "").trim().toLowerCase(); if (!key) { return null; } if (key === "gregorian-easter-sunday") { return computeWesternEasterDate(state.selectedYear); } if (key === "gregorian-good-friday") { const easter = computeWesternEasterDate(state.selectedYear); if (!(easter instanceof Date) || Number.isNaN(easter.getTime())) { return null; } return createDateAtNoon(easter.getFullYear(), easter.getMonth(), easter.getDate() - 2); } if (key === "gregorian-thanksgiving-us") { // US Thanksgiving: 4th Thursday of November. return computeNthWeekdayOfMonth(state.selectedYear, 10, 4, 4); } return null; } function parseFirstMonthDayFromText(dateText) { const text = String(dateText || "").replace(/~/g, " "); const firstSegment = text.split("/")[0] || text; const match = firstSegment.match(/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})/i); if (!match) { return null; } const monthIndex = MONTH_NAME_TO_INDEX[String(match[1]).toLowerCase()]; const day = Number(match[2]); if (!Number.isFinite(monthIndex) || !Number.isFinite(day)) { return null; } return { monthIndex, day }; } function findHebrewMonthDayInGregorianYear(monthId, day, year) { const aliases = HEBREW_MONTH_ALIAS_BY_ID[String(monthId || "").toLowerCase()] || []; const targetDay = Number(day); if (!aliases.length || !Number.isFinite(targetDay) || !Number.isFinite(year)) { return null; } const normalizedAliases = aliases.map((alias) => normalizeCalendarText(alias)).filter(Boolean); const formatter = new Intl.DateTimeFormat("en-u-ca-hebrew", { day: "numeric", month: "long", year: "numeric" }); const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0); const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0); while (cursor.getTime() <= end.getTime()) { const parts = formatter.formatToParts(cursor); const currentDay = readNumericPart(parts, "day"); const monthName = normalizeCalendarText(parts.find((part) => part.type === "month")?.value); if (currentDay === Math.trunc(targetDay) && normalizedAliases.includes(monthName)) { return new Date(cursor); } cursor.setDate(cursor.getDate() + 1); } return null; } function getIslamicMonthOrderById(monthId) { const month = (state.calendarData?.islamic || []).find((item) => item?.id === monthId); const order = Number(month?.order); return Number.isFinite(order) ? Math.trunc(order) : null; } function findIslamicMonthDayInGregorianYear(monthId, day, year) { const monthOrder = getIslamicMonthOrderById(monthId); const targetDay = Number(day); if (!Number.isFinite(monthOrder) || !Number.isFinite(targetDay) || !Number.isFinite(year)) { return null; } const formatter = new Intl.DateTimeFormat("en-u-ca-islamic", { day: "numeric", month: "numeric", year: "numeric" }); const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0); const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0); while (cursor.getTime() <= end.getTime()) { const parts = formatter.formatToParts(cursor); const currentDay = readNumericPart(parts, "day"); const currentMonth = readNumericPart(parts, "month"); if (currentDay === Math.trunc(targetDay) && currentMonth === monthOrder) { return new Date(cursor); } cursor.setDate(cursor.getDate() + 1); } return null; } function resolveHolidayGregorianDate(holiday) { if (!holiday || typeof holiday !== "object") { return null; } const calendarId = String(holiday.calendarId || "").trim().toLowerCase(); const monthId = String(holiday.monthId || "").trim().toLowerCase(); const day = Number(holiday.day); if (calendarId === "gregorian") { if (holiday?.dateRule) { const ruledDate = resolveGregorianDateRule(holiday.dateRule); if (ruledDate) { return ruledDate; } } const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseMonthDayStartToken(holiday.dateText); if (monthDay) { return new Date(state.selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0); } const order = getGregorianMonthOrderFromId(monthId); if (Number.isFinite(order) && Number.isFinite(day)) { return new Date(state.selectedYear, order - 1, Math.trunc(day), 12, 0, 0, 0); } return null; } if (calendarId === "hebrew") { return findHebrewMonthDayInGregorianYear(monthId, day, state.selectedYear); } if (calendarId === "islamic") { return findIslamicMonthDayInGregorianYear(monthId, day, state.selectedYear); } if (calendarId === "wheel-of-year") { const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseFirstMonthDayFromText(holiday.dateText); if (monthDay?.month && monthDay?.day) { return new Date(state.selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0); } if (monthDay?.monthIndex != null && monthDay?.day) { return new Date(state.selectedYear, monthDay.monthIndex, monthDay.day, 12, 0, 0, 0); } } return null; } function formatGregorianReferenceDate(date) { if (!(date instanceof Date) || Number.isNaN(date.getTime())) { return "--"; } return date.toLocaleDateString(undefined, { weekday: "long", year: "numeric", month: "long", day: "numeric" }); } function formatCalendarDateFromGregorian(date, calendarId) { if (!(date instanceof Date) || Number.isNaN(date.getTime())) { return "--"; } const locale = calendarId === "hebrew" ? "en-u-ca-hebrew" : (calendarId === "islamic" ? "en-u-ca-islamic" : "en"); return new Intl.DateTimeFormat(locale, { weekday: "long", year: "numeric", month: "long", day: "numeric" }).format(date); } function buildPlanetMap(planetsObj) { const map = new Map(); if (!planetsObj || typeof planetsObj !== "object") { return map; } Object.values(planetsObj).forEach((planet) => { if (!planet?.id) { return; } map.set(planet.id, planet); }); return map; } function buildSignsMap(signs) { const map = new Map(); if (!Array.isArray(signs)) { return map; } signs.forEach((sign) => { if (!sign?.id) { return; } map.set(sign.id, sign); }); return map; } function buildGodsMap(magickDataset) { const gods = magickDataset?.grouped?.gods?.gods; const map = new Map(); if (!Array.isArray(gods)) { return map; } gods.forEach((god) => { if (!god?.id) { return; } map.set(god.id, god); }); return map; } function buildHebrewMap(magickDataset) { const map = new Map(); const letters = magickDataset?.grouped?.alphabets?.hebrew; if (!Array.isArray(letters)) { return map; } letters.forEach((letter) => { if (!letter?.hebrewLetterId) { return; } map.set(letter.hebrewLetterId, letter); }); return map; } function calendarLabel(calendarId) { const key = String(calendarId || "").trim().toLowerCase(); if (key === "hebrew") return "Hebrew"; if (key === "islamic") return "Islamic"; if (key === "wheel-of-year") return "Wheel of the Year"; return "Gregorian"; } function monthLabelForCalendar(calendarId, monthId) { const months = state.calendarData?.[calendarId]; if (!Array.isArray(months)) { return monthId || "--"; } const month = months.find((entry) => String(entry?.id || "").toLowerCase() === String(monthId || "").toLowerCase()); return month?.name || monthId || "--"; } function normalizeSourceFilter(value) { const key = String(value || "").trim().toLowerCase(); if (key === "gregorian" || key === "hebrew" || key === "islamic" || key === "wheel-of-year") { return key; } return "all"; } function buildAllHolidays() { if (Array.isArray(state.referenceData?.calendarHolidays) && state.referenceData.calendarHolidays.length) { return [...state.referenceData.calendarHolidays].sort((left, right) => { const calCmp = calendarLabel(left?.calendarId).localeCompare(calendarLabel(right?.calendarId)); if (calCmp !== 0) return calCmp; const leftDay = Number(left?.day); const rightDay = Number(right?.day); if (Number.isFinite(leftDay) && Number.isFinite(rightDay) && leftDay !== rightDay) { return leftDay - rightDay; } return String(left?.name || "").localeCompare(String(right?.name || "")); }); } const legacy = Array.isArray(state.referenceData?.celestialHolidays) ? state.referenceData.celestialHolidays : []; return legacy.map((holiday) => ({ ...holiday, calendarId: "gregorian", dateText: holiday?.date || holiday?.dateRange || "" })); } function planetLabel(planetId) { if (!planetId) { return "Planet"; } const planet = state.planetsById.get(planetId); if (!planet) { return cap(planetId); } return `${planet.symbol || ""} ${planet.name || cap(planetId)}`.trim(); } function zodiacLabel(signId) { if (!signId) { return "Zodiac"; } const sign = state.signsById.get(signId); if (!sign) { return cap(signId); } return `${sign.symbol || ""} ${sign.name || cap(signId)}`.trim(); } function godLabel(godId, godName) { if (godName) { return godName; } if (!godId) { return "Deity"; } const god = state.godsById.get(godId); return god?.name || cap(godId); } function hebrewLabel(hebrewLetterId) { if (!hebrewLetterId) { return "Hebrew Letter"; } const letter = state.hebrewById.get(hebrewLetterId); if (!letter) { return cap(hebrewLetterId); } return `${letter.char || ""} ${letter.name || cap(hebrewLetterId)}`.trim(); } function computeDigitalRoot(value) { let current = Math.abs(Math.trunc(Number(value))); if (!Number.isFinite(current)) { return null; } while (current >= 10) { current = String(current) .split("") .reduce((sum, digit) => sum + Number(digit), 0); } return current; } function buildAssociationButtons(associations) { if (!associations || typeof associations !== "object") { return "