(function () { const { getTarotCardSearchAliases } = window.TarotCardImages || {}; const state = { initialized: false, hexagrams: [], filteredHexagrams: [], trigramsByName: {}, tarotByTrigramName: {}, monthRefsByHexagramNumber: new Map(), searchQuery: "", selectedNumber: null }; const ICHING_PLANET_BY_PLANET_ID = { sol: "Sun", luna: "Moon", mercury: "Mercury", venus: "Venus", mars: "Mars", jupiter: "Jupiter", saturn: "Saturn", earth: "Earth", uranus: "Uranus", neptune: "Neptune", pluto: "Pluto" }; function getElements() { return { ichingCardListEl: document.getElementById("iching-card-list"), ichingSearchInputEl: document.getElementById("iching-search-input"), ichingSearchClearEl: document.getElementById("iching-search-clear"), ichingCountEl: document.getElementById("iching-card-count"), ichingDetailNameEl: document.getElementById("iching-detail-name"), ichingDetailTypeEl: document.getElementById("iching-detail-type"), ichingDetailSummaryEl: document.getElementById("iching-detail-summary"), ichingDetailJudgementEl: document.getElementById("iching-detail-judgement"), ichingDetailImageEl: document.getElementById("iching-detail-image"), ichingDetailBinaryEl: document.getElementById("iching-detail-binary"), ichingDetailLineEl: document.getElementById("iching-detail-line"), ichingDetailKeywordsEl: document.getElementById("iching-detail-keywords"), ichingDetailTrigramsEl: document.getElementById("iching-detail-trigrams"), ichingDetailPlanetEl: document.getElementById("iching-detail-planet"), ichingDetailTarotEl: document.getElementById("iching-detail-tarot"), ichingDetailCalendarEl: document.getElementById("iching-detail-calendar") }; } function normalizeSearchValue(value) { return String(value || "").trim().toLowerCase(); } function clearChildren(element) { if (element) { element.replaceChildren(); } } function getTrigramByName(name) { const key = normalizeSearchValue(name); return key ? state.trigramsByName[key] || null : null; } function normalizePlanetInfluence(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^a-z]/g, ""); } function resolveAssociationPlanetInfluence(associations) { if (!associations || typeof associations !== "object") { return ""; } const explicit = normalizePlanetInfluence(associations.iChingPlanetaryInfluence || associations.planetaryInfluence); if (explicit) { return explicit; } const planetId = normalizePlanetInfluence(associations.planetId); if (!planetId) { return ""; } return normalizePlanetInfluence(ICHING_PLANET_BY_PLANET_ID[planetId]); } function buildMonthReferencesByHexagram(referenceData, hexagrams) { const map = new Map(); const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : []; const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : []; const monthById = new Map(months.map((month) => [month.id, month])); function parseMonthDayToken(value) { const text = String(value || "").trim(); const match = text.match(/^(\d{1,2})-(\d{1,2})$/); if (!match) { return null; } const monthNo = Number(match[1]); const dayNo = Number(match[2]); if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) { return null; } return { month: monthNo, day: dayNo }; } function parseMonthDayTokensFromText(value) { const text = String(value || ""); const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)]; return matches .map((match) => ({ month: Number(match[1]), day: Number(match[2]) })) .filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31); } function toDateToken(token, year) { if (!token) { return null; } return new Date(year, token.month - 1, token.day, 12, 0, 0, 0); } function splitMonthDayRangeByMonth(startToken, endToken) { const startDate = toDateToken(startToken, 2025); const endBase = toDateToken(endToken, 2025); if (!startDate || !endBase) { return []; } const wrapsYear = endBase.getTime() < startDate.getTime(); const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase; if (!endDate) { return []; } const segments = []; let cursor = new Date(startDate); while (cursor.getTime() <= endDate.getTime()) { const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0); const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate; segments.push({ monthNo: cursor.getMonth() + 1, startDay: cursor.getDate(), endDay: segmentEnd.getDate() }); cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0); } return segments; } function tokenToString(monthNo, dayNo) { return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`; } function formatRangeLabel(monthName, startDay, endDay) { if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) { return monthName; } if (startDay === endDay) { return `${monthName} ${startDay}`; } return `${monthName} ${startDay}-${endDay}`; } function resolveRangeForMonth(month, options = {}) { const monthOrder = Number(month?.order); const monthStart = parseMonthDayToken(month?.start); const monthEnd = parseMonthDayToken(month?.end); if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) { return { startToken: String(month?.start || "").trim() || null, endToken: String(month?.end || "").trim() || null, label: month?.name || month?.id || "", isFullMonth: true }; } let startToken = parseMonthDayToken(options.startToken); let endToken = parseMonthDayToken(options.endToken); if (!startToken || !endToken) { const tokens = parseMonthDayTokensFromText(options.rawDateText); if (tokens.length >= 2) { startToken = tokens[0]; endToken = tokens[1]; } else if (tokens.length === 1) { startToken = tokens[0]; endToken = tokens[0]; } } if (!startToken || !endToken) { startToken = monthStart; endToken = monthEnd; } const segments = splitMonthDayRangeByMonth(startToken, endToken); const segment = segments.find((entry) => entry.monthNo === monthOrder) || null; const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken; const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken; const startText = tokenToString(useStart.month, useStart.day); const endText = tokenToString(useEnd.month, useEnd.day); const isFullMonth = startText === month.start && endText === month.end; return { startToken: startText, endToken: endText, label: isFullMonth ? (month.name || month.id) : formatRangeLabel(month.name || month.id, useStart.day, useEnd.day), isFullMonth }; } function pushRef(hexagramNumber, month, options = {}) { if (!Number.isFinite(hexagramNumber) || !month?.id) { return; } if (!map.has(hexagramNumber)) { map.set(hexagramNumber, []); } const rows = map.get(hexagramNumber); const range = resolveRangeForMonth(month, options); const rowKey = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`; if (rows.some((entry) => entry.key === rowKey)) { return; } rows.push({ id: month.id, name: month.name || month.id, order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999, label: range.label, startToken: range.startToken, endToken: range.endToken, isFullMonth: range.isFullMonth, key: rowKey }); } function collectRefs(associations, month, options = {}) { const associationInfluence = resolveAssociationPlanetInfluence(associations); if (!associationInfluence) { return; } hexagrams.forEach((hexagram) => { const hexagramInfluence = normalizePlanetInfluence(hexagram?.planetaryInfluence); if (hexagramInfluence && hexagramInfluence === associationInfluence) { pushRef(hexagram.number, month, options); } }); } months.forEach((month) => { collectRefs(month?.associations, month); const events = Array.isArray(month?.events) ? month.events : []; events.forEach((event) => { collectRefs(event?.associations, month, { rawDateText: event?.dateRange || event?.date || "" }); }); }); holidays.forEach((holiday) => { const month = monthById.get(holiday?.monthId); if (!month) { return; } collectRefs(holiday?.associations, month, { rawDateText: holiday?.dateRange || holiday?.date || "" }); }); map.forEach((rows, key) => { const preciseMonthIds = new Set( rows .filter((entry) => !entry.isFullMonth) .map((entry) => entry.id) ); const filtered = rows.filter((entry) => { if (!entry.isFullMonth) { return true; } return !preciseMonthIds.has(entry.id); }); filtered.sort((left, right) => { if (left.order !== right.order) { return left.order - right.order; } const startLeft = parseMonthDayToken(left.startToken); const startRight = parseMonthDayToken(right.startToken); const dayLeft = startLeft ? startLeft.day : 999; const dayRight = startRight ? startRight.day : 999; if (dayLeft !== dayRight) { return dayLeft - dayRight; } return String(left.label || left.name || "").localeCompare(String(right.label || right.name || "")); }); map.set(key, filtered); }); return map; } function getBinaryPattern(value, expectedLength = 0) { const raw = String(value || "").trim(); if (!raw) { return ""; } if (/^[01]+$/.test(raw)) { if (!expectedLength || raw.length === expectedLength) { return raw; } return ""; } if (/^[|:]+$/.test(raw)) { const mapped = raw .split("") .map((char) => (char === "|" ? "1" : "0")) .join(""); if (!expectedLength || mapped.length === expectedLength) { return mapped; } } return ""; } function createLineStack(binaryPattern, variant = "trigram") { const container = document.createElement("div"); container.className = `iching-lines iching-lines-${variant}`; binaryPattern.split("").forEach((bit) => { const line = document.createElement("div"); line.className = bit === "1" ? "iching-line is-yang" : "iching-line is-yin"; container.appendChild(line); }); return container; } function buildHexagramSearchText(entry) { const upper = getTrigramByName(entry?.upperTrigram); const lower = getTrigramByName(entry?.lowerTrigram); const upperCards = upper ? state.tarotByTrigramName[normalizeSearchValue(upper.name)] || [] : []; const lowerCards = lower ? state.tarotByTrigramName[normalizeSearchValue(lower.name)] || [] : []; const tarotAliasText = typeof getTarotCardSearchAliases === "function" ? [...upperCards, ...lowerCards] .flatMap((cardName) => getTarotCardSearchAliases(cardName)) .join(" ") : [...upperCards, ...lowerCards].join(" "); const parts = [ entry?.number, entry?.name, entry?.chineseName, entry?.pinyin, entry?.judgement, entry?.image, entry?.upperTrigram, entry?.lowerTrigram, entry?.planetaryInfluence, entry?.binary, entry?.lineDiagram, ...(Array.isArray(entry?.keywords) ? entry.keywords : []), upper?.name, upper?.chineseName, upper?.pinyin, upper?.element, upper?.attribute, upper?.binary, upper?.description, lower?.name, lower?.chineseName, lower?.pinyin, lower?.element, lower?.attribute, lower?.binary, lower?.description, upperCards.join(" "), lowerCards.join(" "), tarotAliasText ]; return normalizeSearchValue(parts.filter((part) => part !== null && part !== undefined).join(" ")); } function updateSelection(elements) { if (!elements?.ichingCardListEl) { return; } const buttons = elements.ichingCardListEl.querySelectorAll(".planet-list-item"); buttons.forEach((button) => { const isSelected = Number(button.dataset.hexagramNumber) === state.selectedNumber; button.classList.toggle("is-selected", isSelected); button.setAttribute("aria-selected", isSelected ? "true" : "false"); }); } function renderEmptyDetail(elements, detailName = "No hexagrams found") { if (!elements) { return; } if (elements.ichingDetailNameEl) { elements.ichingDetailNameEl.textContent = detailName; } if (elements.ichingDetailTypeEl) { elements.ichingDetailTypeEl.textContent = "--"; } if (elements.ichingDetailSummaryEl) { elements.ichingDetailSummaryEl.textContent = "--"; } if (elements.ichingDetailJudgementEl) { elements.ichingDetailJudgementEl.textContent = "--"; } if (elements.ichingDetailImageEl) { elements.ichingDetailImageEl.textContent = "--"; } if (elements.ichingDetailBinaryEl) { elements.ichingDetailBinaryEl.textContent = "--"; } if (elements.ichingDetailLineEl) { clearChildren(elements.ichingDetailLineEl); elements.ichingDetailLineEl.textContent = "--"; } if (elements.ichingDetailPlanetEl) { elements.ichingDetailPlanetEl.textContent = "--"; } if (elements.ichingDetailTarotEl) { elements.ichingDetailTarotEl.textContent = "--"; } if (elements.ichingDetailCalendarEl) { clearChildren(elements.ichingDetailCalendarEl); elements.ichingDetailCalendarEl.textContent = "--"; } clearChildren(elements.ichingDetailKeywordsEl); clearChildren(elements.ichingDetailTrigramsEl); } function renderKeywords(entry, elements) { clearChildren(elements.ichingDetailKeywordsEl); const keywords = Array.isArray(entry?.keywords) ? entry.keywords : []; if (!keywords.length) { if (elements.ichingDetailKeywordsEl) { elements.ichingDetailKeywordsEl.textContent = "--"; } return; } keywords.forEach((keyword) => { const chip = document.createElement("span"); chip.className = "tarot-keyword-chip"; chip.textContent = keyword; elements.ichingDetailKeywordsEl?.appendChild(chip); }); } function createTrigramCard(label, trigram) { const card = document.createElement("div"); card.className = "iching-trigram-card"; if (!trigram) { card.textContent = `${label}: --`; return card; } const title = document.createElement("div"); title.className = "iching-trigram-title"; const chinese = trigram.chineseName ? ` (${trigram.chineseName})` : ""; title.textContent = `${label}: ${trigram.name || "--"}${chinese}`; const meta = document.createElement("div"); meta.className = "iching-trigram-meta"; const attribute = trigram.attribute || "--"; const element = trigram.element || "--"; meta.textContent = `${attribute} · ${element}`; const diagram = document.createElement("div"); diagram.className = "iching-trigram-diagram"; const binaryPattern = getBinaryPattern(trigram.binary || trigram.lineDiagram, 3); if (binaryPattern) { const binaryLabel = document.createElement("div"); binaryLabel.className = "iching-line-label"; binaryLabel.textContent = binaryPattern; diagram.append(binaryLabel, createLineStack(binaryPattern, "trigram")); } else { diagram.textContent = "--"; } card.append(title, meta, diagram); if (trigram.description) { const description = document.createElement("div"); description.className = "planet-text"; description.textContent = trigram.description; card.appendChild(description); } return card; } function renderTrigrams(entry, elements) { clearChildren(elements.ichingDetailTrigramsEl); const upper = getTrigramByName(entry?.upperTrigram); const lower = getTrigramByName(entry?.lowerTrigram); elements.ichingDetailTrigramsEl?.append( createTrigramCard("Upper", upper), createTrigramCard("Lower", lower) ); } function renderTarotCorrespondences(entry, elements) { if (!elements?.ichingDetailTarotEl) { return; } const upperKey = normalizeSearchValue(entry?.upperTrigram); const lowerKey = normalizeSearchValue(entry?.lowerTrigram); const upperCards = upperKey ? state.tarotByTrigramName[upperKey] || [] : []; const lowerCards = lowerKey ? state.tarotByTrigramName[lowerKey] || [] : []; const upperTrigram = upperKey ? state.trigramsByName[upperKey] : null; const lowerTrigram = lowerKey ? state.trigramsByName[lowerKey] : null; const upperLabel = upperTrigram?.element || entry?.upperTrigram || "--"; const lowerLabel = lowerTrigram?.element || entry?.lowerTrigram || "--"; const lines = []; if (upperKey) { lines.push(`Upper (${upperLabel}): ${upperCards.length ? upperCards.join(", ") : "--"}`); } if (lowerKey) { lines.push(`Lower (${lowerLabel}): ${lowerCards.length ? lowerCards.join(", ") : "--"}`); } elements.ichingDetailTarotEl.textContent = lines.length ? lines.join("\n\n") : "--"; } function renderCalendarMonths(entry, elements) { if (!elements?.ichingDetailCalendarEl) { return; } clearChildren(elements.ichingDetailCalendarEl); const rows = state.monthRefsByHexagramNumber.get(entry?.number) || []; if (!rows.length) { elements.ichingDetailCalendarEl.textContent = "--"; return; } rows.forEach((month) => { const button = document.createElement("button"); button.type = "button"; button.className = "alpha-nav-btn"; button.textContent = `${month.label || month.name} ↗`; button.addEventListener("click", () => { document.dispatchEvent(new CustomEvent("nav:calendar-month", { detail: { monthId: month.id } })); }); elements.ichingDetailCalendarEl.appendChild(button); }); } function renderDetail(entry, elements) { if (!entry || !elements) { return; } const number = Number.isFinite(entry.number) ? entry.number : "--"; if (elements.ichingDetailNameEl) { elements.ichingDetailNameEl.textContent = `${number} · ${entry.name || "--"}`; } if (elements.ichingDetailTypeEl) { const chinese = entry.chineseName || "--"; const pinyin = entry.pinyin || "--"; const upper = entry.upperTrigram || "--"; const lower = entry.lowerTrigram || "--"; elements.ichingDetailTypeEl.textContent = `Hexagram · ${chinese} · ${pinyin} · ${upper}/${lower}`; } if (elements.ichingDetailSummaryEl) { elements.ichingDetailSummaryEl.textContent = entry.judgement || "--"; } if (elements.ichingDetailJudgementEl) { elements.ichingDetailJudgementEl.textContent = entry.judgement || "--"; } if (elements.ichingDetailImageEl) { elements.ichingDetailImageEl.textContent = entry.image || "--"; } if (elements.ichingDetailBinaryEl) { const binary = entry.binary || "--"; elements.ichingDetailBinaryEl.textContent = `Binary: ${binary}`; } if (elements.ichingDetailLineEl) { clearChildren(elements.ichingDetailLineEl); const linePattern = getBinaryPattern(entry.binary, 6) || getBinaryPattern(entry.lineDiagram, 6); if (linePattern) { elements.ichingDetailLineEl.appendChild(createLineStack(linePattern, "hexagram")); } else { elements.ichingDetailLineEl.textContent = "--"; } } if (elements.ichingDetailPlanetEl) { elements.ichingDetailPlanetEl.textContent = entry.planetaryInfluence || "--"; } renderKeywords(entry, elements); renderTrigrams(entry, elements); renderTarotCorrespondences(entry, elements); renderCalendarMonths(entry, elements); } function selectByNumber(number, elements) { const numeric = Number(number); if (!Number.isFinite(numeric)) { return; } const entry = state.hexagrams.find((hexagram) => hexagram.number === numeric); if (!entry) { return; } state.selectedNumber = entry.number; updateSelection(elements); renderDetail(entry, elements); } function renderList(elements) { if (!elements?.ichingCardListEl) { return; } clearChildren(elements.ichingCardListEl); state.filteredHexagrams.forEach((entry) => { const button = document.createElement("button"); button.type = "button"; button.className = "planet-list-item"; button.dataset.hexagramNumber = String(entry.number); button.setAttribute("role", "option"); const nameEl = document.createElement("span"); nameEl.className = "planet-list-name"; const number = Number.isFinite(entry.number) ? `#${entry.number} ` : ""; nameEl.textContent = `${number}${entry.name || "--"}`; const metaEl = document.createElement("span"); metaEl.className = "planet-list-meta"; const upper = entry.upperTrigram || "--"; const lower = entry.lowerTrigram || "--"; metaEl.textContent = `${upper}/${lower} · ${entry.planetaryInfluence || "--"}`; button.append(nameEl, metaEl); elements.ichingCardListEl.appendChild(button); }); if (elements.ichingCountEl) { elements.ichingCountEl.textContent = state.searchQuery ? `${state.filteredHexagrams.length} of ${state.hexagrams.length} hexagrams` : `${state.hexagrams.length} hexagrams`; } } function applySearchFilter(elements) { const query = normalizeSearchValue(state.searchQuery); state.filteredHexagrams = query ? state.hexagrams.filter((entry) => buildHexagramSearchText(entry).includes(query)) : [...state.hexagrams]; if (elements?.ichingSearchClearEl) { elements.ichingSearchClearEl.disabled = !query; } renderList(elements); if (!state.filteredHexagrams.some((entry) => entry.number === state.selectedNumber)) { if (state.filteredHexagrams.length > 0) { selectByNumber(state.filteredHexagrams[0].number, elements); } else { state.selectedNumber = null; updateSelection(elements); renderEmptyDetail(elements); } return; } updateSelection(elements); } function ensureIChingSection(referenceData) { const elements = getElements(); if (state.initialized) { state.monthRefsByHexagramNumber = buildMonthReferencesByHexagram(referenceData, state.hexagrams); const selected = state.hexagrams.find((hexagram) => hexagram.number === state.selectedNumber); if (selected) { renderDetail(selected, elements); } return; } if (!elements.ichingCardListEl || !elements.ichingDetailNameEl) { return; } const iChing = referenceData?.iChing; const trigrams = Array.isArray(iChing?.trigrams) ? iChing.trigrams : []; const hexagrams = Array.isArray(iChing?.hexagrams) ? iChing.hexagrams : []; const correspondences = iChing?.correspondences; if (!hexagrams.length) { renderEmptyDetail(elements, "I Ching data unavailable"); return; } state.trigramsByName = trigrams.reduce((acc, trigram) => { const key = normalizeSearchValue(trigram?.name); if (key) { acc[key] = trigram; } return acc; }, {}); const tarotToTrigram = Array.isArray(correspondences?.tarotToTrigram) ? correspondences.tarotToTrigram : []; state.tarotByTrigramName = tarotToTrigram.reduce((acc, row) => { const key = normalizeSearchValue(row?.trigram); const tarotCard = String(row?.tarot || "").trim(); if (!key || !tarotCard) { return acc; } if (!Array.isArray(acc[key])) { acc[key] = []; } if (!acc[key].includes(tarotCard)) { acc[key].push(tarotCard); } return acc; }, {}); state.hexagrams = [...hexagrams] .map((entry) => ({ ...entry, number: Number(entry?.number) })) .filter((entry) => Number.isFinite(entry.number)) .sort((a, b) => a.number - b.number); state.monthRefsByHexagramNumber = buildMonthReferencesByHexagram(referenceData, state.hexagrams); state.filteredHexagrams = [...state.hexagrams]; renderList(elements); if (state.hexagrams.length > 0) { selectByNumber(state.hexagrams[0].number, elements); } elements.ichingCardListEl.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof Node)) { return; } const button = target instanceof Element ? target.closest(".planet-list-item") : null; if (!(button instanceof HTMLButtonElement)) { return; } const selectedNumber = button.dataset.hexagramNumber; if (!selectedNumber) { return; } selectByNumber(selectedNumber, elements); }); if (elements.ichingSearchInputEl) { elements.ichingSearchInputEl.addEventListener("input", () => { state.searchQuery = elements.ichingSearchInputEl.value || ""; applySearchFilter(elements); }); } if (elements.ichingSearchClearEl && elements.ichingSearchInputEl) { elements.ichingSearchClearEl.addEventListener("click", () => { elements.ichingSearchInputEl.value = ""; state.searchQuery = ""; applySearchFilter(elements); elements.ichingSearchInputEl.focus(); }); } state.initialized = true; } function selectByHexagramNumber(number) { if (!state.initialized) { return false; } const elements = getElements(); const numeric = Number(number); if (!Number.isFinite(numeric)) { return false; } const entry = state.hexagrams.find((hexagram) => hexagram.number === numeric); if (!entry) { return false; } selectByNumber(entry.number, elements); elements.ichingCardListEl ?.querySelector(`[data-hexagram-number="${entry.number}"]`) ?.scrollIntoView({ block: "nearest" }); return true; } function selectByPlanetaryInfluence(planetaryInfluence) { if (!state.initialized) { return false; } const targetInfluence = normalizePlanetInfluence(planetaryInfluence); if (!targetInfluence) { return false; } const entry = state.hexagrams.find((hexagram) => normalizePlanetInfluence(hexagram?.planetaryInfluence) === targetInfluence ); if (!entry) { return false; } return selectByHexagramNumber(entry.number); } window.IChingSectionUi = { ensureIChingSection, selectByHexagramNumber, selectByPlanetaryInfluence }; })();