933 lines
30 KiB
JavaScript
933 lines
30 KiB
JavaScript
|
|
(function () {
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
let initialized = false;
|
||
|
|
let activeNumberValue = 0;
|
||
|
|
let config = {
|
||
|
|
getReferenceData: () => null,
|
||
|
|
getMagickDataset: () => null,
|
||
|
|
ensureTarotSection: null
|
||
|
|
};
|
||
|
|
|
||
|
|
const NUMBERS_SPECIAL_BASE_VALUES = [1, 2, 3, 4];
|
||
|
|
const numbersSpecialFlipState = new Map();
|
||
|
|
|
||
|
|
const DEFAULT_NUMBER_ENTRIES = Array.from({ length: 10 }, (_, value) => ({
|
||
|
|
value,
|
||
|
|
label: `${value}`,
|
||
|
|
opposite: 9 - value,
|
||
|
|
digitalRoot: value,
|
||
|
|
summary: "",
|
||
|
|
keywords: [],
|
||
|
|
associations: {
|
||
|
|
kabbalahNode: value === 0 ? 10 : value,
|
||
|
|
playingSuit: "hearts"
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
|
||
|
|
const PLAYING_SUIT_SYMBOL = {
|
||
|
|
hearts: "♥",
|
||
|
|
diamonds: "♦",
|
||
|
|
clubs: "♣",
|
||
|
|
spades: "♠"
|
||
|
|
};
|
||
|
|
|
||
|
|
const PLAYING_SUIT_LABEL = {
|
||
|
|
hearts: "Hearts",
|
||
|
|
diamonds: "Diamonds",
|
||
|
|
clubs: "Clubs",
|
||
|
|
spades: "Spades"
|
||
|
|
};
|
||
|
|
|
||
|
|
const PLAYING_SUIT_TO_TAROT = {
|
||
|
|
hearts: "Cups",
|
||
|
|
diamonds: "Pentacles",
|
||
|
|
clubs: "Wands",
|
||
|
|
spades: "Swords"
|
||
|
|
};
|
||
|
|
|
||
|
|
const PLAYING_RANKS = [
|
||
|
|
{ rank: "A", rankLabel: "Ace", rankValue: 1 },
|
||
|
|
{ rank: "2", rankLabel: "Two", rankValue: 2 },
|
||
|
|
{ rank: "3", rankLabel: "Three", rankValue: 3 },
|
||
|
|
{ rank: "4", rankLabel: "Four", rankValue: 4 },
|
||
|
|
{ rank: "5", rankLabel: "Five", rankValue: 5 },
|
||
|
|
{ rank: "6", rankLabel: "Six", rankValue: 6 },
|
||
|
|
{ rank: "7", rankLabel: "Seven", rankValue: 7 },
|
||
|
|
{ rank: "8", rankLabel: "Eight", rankValue: 8 },
|
||
|
|
{ rank: "9", rankLabel: "Nine", rankValue: 9 },
|
||
|
|
{ rank: "10", rankLabel: "Ten", rankValue: 10 },
|
||
|
|
{ rank: "J", rankLabel: "Jack", rankValue: null },
|
||
|
|
{ rank: "Q", rankLabel: "Queen", rankValue: null },
|
||
|
|
{ rank: "K", rankLabel: "King", rankValue: null }
|
||
|
|
];
|
||
|
|
|
||
|
|
const TAROT_RANK_NUMBER_MAP = {
|
||
|
|
ace: 1,
|
||
|
|
two: 2,
|
||
|
|
three: 3,
|
||
|
|
four: 4,
|
||
|
|
five: 5,
|
||
|
|
six: 6,
|
||
|
|
seven: 7,
|
||
|
|
eight: 8,
|
||
|
|
nine: 9,
|
||
|
|
ten: 10
|
||
|
|
};
|
||
|
|
|
||
|
|
function getReferenceData() {
|
||
|
|
return typeof config.getReferenceData === "function" ? config.getReferenceData() : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getMagickDataset() {
|
||
|
|
return typeof config.getMagickDataset === "function" ? config.getMagickDataset() : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getElements() {
|
||
|
|
return {
|
||
|
|
countEl: document.getElementById("numbers-count"),
|
||
|
|
listEl: document.getElementById("numbers-list"),
|
||
|
|
detailNameEl: document.getElementById("numbers-detail-name"),
|
||
|
|
detailTypeEl: document.getElementById("numbers-detail-type"),
|
||
|
|
detailSummaryEl: document.getElementById("numbers-detail-summary"),
|
||
|
|
detailBodyEl: document.getElementById("numbers-detail-body"),
|
||
|
|
specialPanelEl: document.getElementById("numbers-special-panel")
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeNumberValue(value) {
|
||
|
|
const parsed = Number(value);
|
||
|
|
if (!Number.isFinite(parsed)) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
const normalized = Math.trunc(parsed);
|
||
|
|
if (normalized < 0) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
if (normalized > 9) {
|
||
|
|
return 9;
|
||
|
|
}
|
||
|
|
return normalized;
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeNumberEntry(rawEntry) {
|
||
|
|
if (!rawEntry || typeof rawEntry !== "object") {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const value = normalizeNumberValue(rawEntry.value);
|
||
|
|
const oppositeRaw = Number(rawEntry.opposite);
|
||
|
|
const opposite = Number.isFinite(oppositeRaw)
|
||
|
|
? normalizeNumberValue(oppositeRaw)
|
||
|
|
: (9 - value);
|
||
|
|
const digitalRootRaw = Number(rawEntry.digitalRoot);
|
||
|
|
const digitalRoot = Number.isFinite(digitalRootRaw)
|
||
|
|
? normalizeNumberValue(digitalRootRaw)
|
||
|
|
: value;
|
||
|
|
const kabbalahNodeRaw = Number(rawEntry?.associations?.kabbalahNode);
|
||
|
|
const kabbalahNode = Number.isFinite(kabbalahNodeRaw)
|
||
|
|
? Math.max(1, Math.trunc(kabbalahNodeRaw))
|
||
|
|
: (value === 0 ? 10 : value);
|
||
|
|
const tarotTrumpNumbersRaw = Array.isArray(rawEntry?.associations?.tarotTrumpNumbers)
|
||
|
|
? rawEntry.associations.tarotTrumpNumbers
|
||
|
|
: [];
|
||
|
|
const tarotTrumpNumbers = Array.from(new Set(
|
||
|
|
tarotTrumpNumbersRaw
|
||
|
|
.map((item) => Number(item))
|
||
|
|
.filter((item) => Number.isFinite(item))
|
||
|
|
.map((item) => Math.trunc(item))
|
||
|
|
));
|
||
|
|
const playingSuitRaw = String(rawEntry?.associations?.playingSuit || "").trim().toLowerCase();
|
||
|
|
const playingSuit = ["hearts", "diamonds", "clubs", "spades"].includes(playingSuitRaw)
|
||
|
|
? playingSuitRaw
|
||
|
|
: "hearts";
|
||
|
|
|
||
|
|
return {
|
||
|
|
value,
|
||
|
|
label: String(rawEntry.label || value),
|
||
|
|
opposite,
|
||
|
|
digitalRoot,
|
||
|
|
summary: String(rawEntry.summary || ""),
|
||
|
|
keywords: Array.isArray(rawEntry.keywords)
|
||
|
|
? rawEntry.keywords.map((keyword) => String(keyword || "").trim()).filter(Boolean)
|
||
|
|
: [],
|
||
|
|
associations: {
|
||
|
|
kabbalahNode,
|
||
|
|
tarotTrumpNumbers,
|
||
|
|
playingSuit
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function getNumbersDatasetEntries() {
|
||
|
|
const numbersData = getMagickDataset()?.grouped?.numbers;
|
||
|
|
const rawEntries = Array.isArray(numbersData)
|
||
|
|
? numbersData
|
||
|
|
: (Array.isArray(numbersData?.entries) ? numbersData.entries : []);
|
||
|
|
|
||
|
|
const normalizedEntries = rawEntries
|
||
|
|
.map((entry) => normalizeNumberEntry(entry))
|
||
|
|
.filter(Boolean)
|
||
|
|
.sort((left, right) => left.value - right.value);
|
||
|
|
|
||
|
|
return normalizedEntries.length
|
||
|
|
? normalizedEntries
|
||
|
|
: DEFAULT_NUMBER_ENTRIES;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getNumberEntryByValue(value) {
|
||
|
|
const entries = getNumbersDatasetEntries();
|
||
|
|
const normalized = normalizeNumberValue(value);
|
||
|
|
return entries.find((entry) => entry.value === normalized) || entries[0] || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
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 getCalendarMonthLinksForNumber(value) {
|
||
|
|
const referenceData = getReferenceData();
|
||
|
|
const normalized = normalizeNumberValue(value);
|
||
|
|
const calendarGroups = [
|
||
|
|
{
|
||
|
|
calendarId: "gregorian",
|
||
|
|
calendarLabel: "Gregorian",
|
||
|
|
months: Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : []
|
||
|
|
},
|
||
|
|
{
|
||
|
|
calendarId: "hebrew",
|
||
|
|
calendarLabel: "Hebrew",
|
||
|
|
months: Array.isArray(referenceData?.hebrewCalendar?.months) ? referenceData.hebrewCalendar.months : []
|
||
|
|
},
|
||
|
|
{
|
||
|
|
calendarId: "islamic",
|
||
|
|
calendarLabel: "Islamic",
|
||
|
|
months: Array.isArray(referenceData?.islamicCalendar?.months) ? referenceData.islamicCalendar.months : []
|
||
|
|
},
|
||
|
|
{
|
||
|
|
calendarId: "wheel-of-year",
|
||
|
|
calendarLabel: "Wheel of the Year",
|
||
|
|
months: Array.isArray(referenceData?.wheelOfYear?.months) ? referenceData.wheelOfYear.months : []
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const links = [];
|
||
|
|
calendarGroups.forEach((group) => {
|
||
|
|
group.months.forEach((month) => {
|
||
|
|
const monthOrder = Number(month?.order);
|
||
|
|
const normalizedOrder = Number.isFinite(monthOrder) ? Math.trunc(monthOrder) : null;
|
||
|
|
const monthRoot = normalizedOrder != null ? computeDigitalRoot(normalizedOrder) : null;
|
||
|
|
if (monthRoot !== normalized) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
links.push({
|
||
|
|
calendarId: group.calendarId,
|
||
|
|
calendarLabel: group.calendarLabel,
|
||
|
|
monthId: String(month.id || "").trim(),
|
||
|
|
monthName: String(month.name || month.id || "Month").trim(),
|
||
|
|
monthOrder: normalizedOrder
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
return links.filter((link) => link.monthId);
|
||
|
|
}
|
||
|
|
|
||
|
|
function rankLabelToTarotMinorRank(rankLabel) {
|
||
|
|
const key = String(rankLabel || "").trim().toLowerCase();
|
||
|
|
if (key === "10" || key === "ten") return "Princess";
|
||
|
|
if (key === "j" || key === "jack") return "Prince";
|
||
|
|
if (key === "q" || key === "queen") return "Queen";
|
||
|
|
if (key === "k" || key === "king") return "Knight";
|
||
|
|
return String(rankLabel || "").trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildFallbackPlayingDeckEntries() {
|
||
|
|
const entries = [];
|
||
|
|
Object.keys(PLAYING_SUIT_SYMBOL).forEach((suit) => {
|
||
|
|
PLAYING_RANKS.forEach((rank) => {
|
||
|
|
const tarotSuit = PLAYING_SUIT_TO_TAROT[suit];
|
||
|
|
const tarotRank = rankLabelToTarotMinorRank(rank.rankLabel);
|
||
|
|
entries.push({
|
||
|
|
id: `${rank.rank}${PLAYING_SUIT_SYMBOL[suit]}`,
|
||
|
|
suit,
|
||
|
|
suitLabel: PLAYING_SUIT_LABEL[suit],
|
||
|
|
suitSymbol: PLAYING_SUIT_SYMBOL[suit],
|
||
|
|
rank: rank.rank,
|
||
|
|
rankLabel: rank.rankLabel,
|
||
|
|
rankValue: rank.rankValue,
|
||
|
|
tarotSuit,
|
||
|
|
tarotCard: `${tarotRank} of ${tarotSuit}`
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPlayingDeckEntries() {
|
||
|
|
const deckData = getMagickDataset()?.grouped?.["playing-cards-52"];
|
||
|
|
const rawEntries = Array.isArray(deckData)
|
||
|
|
? deckData
|
||
|
|
: (Array.isArray(deckData?.entries) ? deckData.entries : []);
|
||
|
|
|
||
|
|
if (!rawEntries.length) {
|
||
|
|
return buildFallbackPlayingDeckEntries();
|
||
|
|
}
|
||
|
|
|
||
|
|
return rawEntries
|
||
|
|
.map((entry) => {
|
||
|
|
const suit = String(entry?.suit || "").trim().toLowerCase();
|
||
|
|
const rankLabel = String(entry?.rankLabel || "").trim();
|
||
|
|
const rank = String(entry?.rank || "").trim();
|
||
|
|
if (!suit || !rank) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const suitSymbol = String(entry?.suitSymbol || PLAYING_SUIT_SYMBOL[suit] || "").trim();
|
||
|
|
const tarotSuit = String(entry?.tarotSuit || PLAYING_SUIT_TO_TAROT[suit] || "").trim();
|
||
|
|
const tarotCard = String(entry?.tarotCard || "").trim();
|
||
|
|
const rankValueRaw = Number(entry?.rankValue);
|
||
|
|
const rankValue = Number.isFinite(rankValueRaw) ? Math.trunc(rankValueRaw) : null;
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: String(entry?.id || `${rank}${suitSymbol}`).trim(),
|
||
|
|
suit,
|
||
|
|
suitLabel: String(entry?.suitLabel || PLAYING_SUIT_LABEL[suit] || suit).trim(),
|
||
|
|
suitSymbol,
|
||
|
|
rank,
|
||
|
|
rankLabel: rankLabel || rank,
|
||
|
|
rankValue,
|
||
|
|
tarotSuit,
|
||
|
|
tarotCard: tarotCard || `${rankLabelToTarotMinorRank(rankLabel || rank)} of ${tarotSuit}`
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
}
|
||
|
|
|
||
|
|
function findPlayingCardBySuitAndValue(entries, suit, value) {
|
||
|
|
const normalizedSuit = String(suit || "").trim().toLowerCase();
|
||
|
|
const targetValue = Number(value);
|
||
|
|
return entries.find((entry) => entry.suit === normalizedSuit && Number(entry.rankValue) === targetValue) || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildNumbersSpecialCardSlots(playingSuit) {
|
||
|
|
const suit = String(playingSuit || "hearts").trim().toLowerCase();
|
||
|
|
const selectedSuit = ["hearts", "diamonds", "clubs", "spades"].includes(suit) ? suit : "hearts";
|
||
|
|
const deckEntries = getPlayingDeckEntries();
|
||
|
|
|
||
|
|
const cardEl = document.createElement("div");
|
||
|
|
cardEl.className = "numbers-detail-card numbers-special-card-section";
|
||
|
|
|
||
|
|
const headingEl = document.createElement("strong");
|
||
|
|
headingEl.textContent = "4 Card Arrangement";
|
||
|
|
|
||
|
|
const subEl = document.createElement("div");
|
||
|
|
subEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
subEl.textContent = `Click a card to flip to its opposite (${PLAYING_SUIT_LABEL[selectedSuit]} ↔ ${PLAYING_SUIT_TO_TAROT[selectedSuit]}).`;
|
||
|
|
|
||
|
|
const boardEl = document.createElement("div");
|
||
|
|
boardEl.className = "numbers-special-board";
|
||
|
|
|
||
|
|
NUMBERS_SPECIAL_BASE_VALUES.forEach((baseValue) => {
|
||
|
|
const oppositeValue = 9 - baseValue;
|
||
|
|
const frontCard = findPlayingCardBySuitAndValue(deckEntries, selectedSuit, baseValue);
|
||
|
|
const backCard = findPlayingCardBySuitAndValue(deckEntries, selectedSuit, oppositeValue);
|
||
|
|
if (!frontCard || !backCard) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const slotKey = `${selectedSuit}:${baseValue}`;
|
||
|
|
const isFlipped = Boolean(numbersSpecialFlipState.get(slotKey));
|
||
|
|
|
||
|
|
const faceBtn = document.createElement("button");
|
||
|
|
faceBtn.type = "button";
|
||
|
|
faceBtn.className = `numbers-special-card${isFlipped ? " is-flipped" : ""}`;
|
||
|
|
faceBtn.setAttribute("aria-pressed", isFlipped ? "true" : "false");
|
||
|
|
faceBtn.setAttribute("aria-label", `${frontCard.rankLabel} of ${frontCard.suitLabel}. Click to flip to ${backCard.rankLabel}.`);
|
||
|
|
faceBtn.dataset.suit = selectedSuit;
|
||
|
|
|
||
|
|
const innerEl = document.createElement("div");
|
||
|
|
innerEl.className = "numbers-special-card-inner";
|
||
|
|
|
||
|
|
const frontFaceEl = document.createElement("div");
|
||
|
|
frontFaceEl.className = "numbers-special-card-face numbers-special-card-face--front";
|
||
|
|
|
||
|
|
const frontRankEl = document.createElement("div");
|
||
|
|
frontRankEl.className = "numbers-special-card-rank";
|
||
|
|
frontRankEl.textContent = frontCard.rankLabel;
|
||
|
|
|
||
|
|
const frontSuitEl = document.createElement("div");
|
||
|
|
frontSuitEl.className = "numbers-special-card-suit";
|
||
|
|
frontSuitEl.textContent = frontCard.suitSymbol;
|
||
|
|
|
||
|
|
const frontMetaEl = document.createElement("div");
|
||
|
|
frontMetaEl.className = "numbers-special-card-meta";
|
||
|
|
frontMetaEl.textContent = frontCard.tarotCard;
|
||
|
|
|
||
|
|
frontFaceEl.append(frontRankEl, frontSuitEl, frontMetaEl);
|
||
|
|
|
||
|
|
const backFaceEl = document.createElement("div");
|
||
|
|
backFaceEl.className = "numbers-special-card-face numbers-special-card-face--back";
|
||
|
|
|
||
|
|
const backTagEl = document.createElement("div");
|
||
|
|
backTagEl.className = "numbers-special-card-tag";
|
||
|
|
backTagEl.textContent = "Opposite";
|
||
|
|
|
||
|
|
const backRankEl = document.createElement("div");
|
||
|
|
backRankEl.className = "numbers-special-card-rank";
|
||
|
|
backRankEl.textContent = backCard.rankLabel;
|
||
|
|
|
||
|
|
const backSuitEl = document.createElement("div");
|
||
|
|
backSuitEl.className = "numbers-special-card-suit";
|
||
|
|
backSuitEl.textContent = backCard.suitSymbol;
|
||
|
|
|
||
|
|
const backMetaEl = document.createElement("div");
|
||
|
|
backMetaEl.className = "numbers-special-card-meta";
|
||
|
|
backMetaEl.textContent = backCard.tarotCard;
|
||
|
|
|
||
|
|
backFaceEl.append(backTagEl, backRankEl, backSuitEl, backMetaEl);
|
||
|
|
|
||
|
|
innerEl.append(frontFaceEl, backFaceEl);
|
||
|
|
faceBtn.append(innerEl);
|
||
|
|
|
||
|
|
faceBtn.addEventListener("click", () => {
|
||
|
|
const next = !Boolean(numbersSpecialFlipState.get(slotKey));
|
||
|
|
numbersSpecialFlipState.set(slotKey, next);
|
||
|
|
faceBtn.classList.toggle("is-flipped", next);
|
||
|
|
faceBtn.setAttribute("aria-pressed", next ? "true" : "false");
|
||
|
|
});
|
||
|
|
|
||
|
|
boardEl.appendChild(faceBtn);
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!boardEl.childElementCount) {
|
||
|
|
const emptyEl = document.createElement("div");
|
||
|
|
emptyEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
emptyEl.textContent = "No card slots available for this mapping yet.";
|
||
|
|
boardEl.appendChild(emptyEl);
|
||
|
|
}
|
||
|
|
|
||
|
|
cardEl.append(headingEl, subEl, boardEl);
|
||
|
|
return cardEl;
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderNumbersSpecialPanel(value) {
|
||
|
|
const { specialPanelEl } = getElements();
|
||
|
|
if (!specialPanelEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const entry = getNumberEntryByValue(value);
|
||
|
|
const playingSuit = entry?.associations?.playingSuit || "hearts";
|
||
|
|
const boardCardEl = buildNumbersSpecialCardSlots(playingSuit);
|
||
|
|
specialPanelEl.replaceChildren(boardCardEl);
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseTarotCardNumber(rawValue) {
|
||
|
|
if (typeof rawValue === "number") {
|
||
|
|
return Number.isFinite(rawValue) ? Math.trunc(rawValue) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof rawValue === "string") {
|
||
|
|
const trimmed = rawValue.trim();
|
||
|
|
if (!trimmed || !/^-?\d+$/.test(trimmed)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return Number(trimmed);
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function extractTarotCardNumericValue(card) {
|
||
|
|
const directNumber = parseTarotCardNumber(card?.number);
|
||
|
|
if (directNumber !== null) {
|
||
|
|
return directNumber;
|
||
|
|
}
|
||
|
|
|
||
|
|
const rankKey = String(card?.rank || "").trim().toLowerCase();
|
||
|
|
if (Object.prototype.hasOwnProperty.call(TAROT_RANK_NUMBER_MAP, rankKey)) {
|
||
|
|
return TAROT_RANK_NUMBER_MAP[rankKey];
|
||
|
|
}
|
||
|
|
|
||
|
|
const numerologyRelation = Array.isArray(card?.relations)
|
||
|
|
? card.relations.find((relation) => String(relation?.type || "").trim().toLowerCase() === "numerology")
|
||
|
|
: null;
|
||
|
|
const relationValue = Number(numerologyRelation?.data?.value);
|
||
|
|
if (Number.isFinite(relationValue)) {
|
||
|
|
return Math.trunc(relationValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getAlphabetPositionLinksForDigitalRoot(targetRoot) {
|
||
|
|
const alphabets = getMagickDataset()?.grouped?.alphabets;
|
||
|
|
if (!alphabets || typeof alphabets !== "object") {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
const links = [];
|
||
|
|
|
||
|
|
const addLink = (alphabetLabel, entry, buttonLabel, detail) => {
|
||
|
|
const index = Number(entry?.index);
|
||
|
|
if (!Number.isFinite(index)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const normalizedIndex = Math.trunc(index);
|
||
|
|
if (computeDigitalRoot(normalizedIndex) !== targetRoot) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
links.push({
|
||
|
|
alphabet: alphabetLabel,
|
||
|
|
index: normalizedIndex,
|
||
|
|
label: buttonLabel,
|
||
|
|
detail
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const toTitle = (value) => String(value || "")
|
||
|
|
.trim()
|
||
|
|
.replace(/[_-]+/g, " ")
|
||
|
|
.replace(/\s+/g, " ")
|
||
|
|
.toLowerCase()
|
||
|
|
.replace(/\b([a-z])/g, (match, ch) => ch.toUpperCase());
|
||
|
|
|
||
|
|
const englishEntries = Array.isArray(alphabets.english) ? alphabets.english : [];
|
||
|
|
englishEntries.forEach((entry) => {
|
||
|
|
const letter = String(entry?.letter || "").trim();
|
||
|
|
if (!letter) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
addLink(
|
||
|
|
"English",
|
||
|
|
entry,
|
||
|
|
`${letter}`,
|
||
|
|
{
|
||
|
|
alphabet: "english",
|
||
|
|
englishLetter: letter
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
const greekEntries = Array.isArray(alphabets.greek) ? alphabets.greek : [];
|
||
|
|
greekEntries.forEach((entry) => {
|
||
|
|
const greekName = String(entry?.name || "").trim();
|
||
|
|
if (!greekName) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const glyph = String(entry?.char || "").trim();
|
||
|
|
const displayName = String(entry?.displayName || toTitle(greekName)).trim();
|
||
|
|
addLink(
|
||
|
|
"Greek",
|
||
|
|
entry,
|
||
|
|
glyph ? `${displayName} - ${glyph}` : displayName,
|
||
|
|
{
|
||
|
|
alphabet: "greek",
|
||
|
|
greekName
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
const hebrewEntries = Array.isArray(alphabets.hebrew) ? alphabets.hebrew : [];
|
||
|
|
hebrewEntries.forEach((entry) => {
|
||
|
|
const hebrewLetterId = String(entry?.hebrewLetterId || "").trim();
|
||
|
|
if (!hebrewLetterId) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const glyph = String(entry?.char || "").trim();
|
||
|
|
const name = String(entry?.name || hebrewLetterId).trim();
|
||
|
|
const displayName = toTitle(name);
|
||
|
|
addLink(
|
||
|
|
"Hebrew",
|
||
|
|
entry,
|
||
|
|
glyph ? `${displayName} - ${glyph}` : displayName,
|
||
|
|
{
|
||
|
|
alphabet: "hebrew",
|
||
|
|
hebrewLetterId
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
const arabicEntries = Array.isArray(alphabets.arabic) ? alphabets.arabic : [];
|
||
|
|
arabicEntries.forEach((entry) => {
|
||
|
|
const arabicName = String(entry?.name || "").trim();
|
||
|
|
if (!arabicName) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const glyph = String(entry?.char || "").trim();
|
||
|
|
const displayName = toTitle(arabicName);
|
||
|
|
addLink(
|
||
|
|
"Arabic",
|
||
|
|
entry,
|
||
|
|
glyph ? `${displayName} - ${glyph}` : displayName,
|
||
|
|
{
|
||
|
|
alphabet: "arabic",
|
||
|
|
arabicName
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
const enochianEntries = Array.isArray(alphabets.enochian) ? alphabets.enochian : [];
|
||
|
|
enochianEntries.forEach((entry) => {
|
||
|
|
const enochianId = String(entry?.id || "").trim();
|
||
|
|
if (!enochianId) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const title = String(entry?.title || enochianId).trim();
|
||
|
|
const displayName = toTitle(title);
|
||
|
|
addLink(
|
||
|
|
"Enochian",
|
||
|
|
entry,
|
||
|
|
`${displayName}`,
|
||
|
|
{
|
||
|
|
alphabet: "enochian",
|
||
|
|
enochianId
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
return links.sort((left, right) => {
|
||
|
|
if (left.index !== right.index) {
|
||
|
|
return left.index - right.index;
|
||
|
|
}
|
||
|
|
const alphabetCompare = left.alphabet.localeCompare(right.alphabet);
|
||
|
|
if (alphabetCompare !== 0) {
|
||
|
|
return alphabetCompare;
|
||
|
|
}
|
||
|
|
return left.label.localeCompare(right.label);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function getTarotCardsForDigitalRoot(targetRoot, numberEntry = null) {
|
||
|
|
const referenceData = getReferenceData();
|
||
|
|
const magickDataset = getMagickDataset();
|
||
|
|
if (typeof config.ensureTarotSection === "function" && referenceData) {
|
||
|
|
config.ensureTarotSection(referenceData, magickDataset);
|
||
|
|
}
|
||
|
|
|
||
|
|
const allCards = window.TarotSectionUi?.getCards?.() || [];
|
||
|
|
const explicitTrumpNumbers = Array.isArray(numberEntry?.associations?.tarotTrumpNumbers)
|
||
|
|
? numberEntry.associations.tarotTrumpNumbers
|
||
|
|
.map((value) => Number(value))
|
||
|
|
.filter((value) => Number.isFinite(value))
|
||
|
|
.map((value) => Math.trunc(value))
|
||
|
|
: [];
|
||
|
|
|
||
|
|
const filteredCards = explicitTrumpNumbers.length
|
||
|
|
? allCards.filter((card) => {
|
||
|
|
const numberValue = parseTarotCardNumber(card?.number);
|
||
|
|
return card?.arcana === "Major" && numberValue !== null && explicitTrumpNumbers.includes(numberValue);
|
||
|
|
})
|
||
|
|
: allCards.filter((card) => {
|
||
|
|
const numberValue = extractTarotCardNumericValue(card);
|
||
|
|
return numberValue !== null && computeDigitalRoot(numberValue) === targetRoot;
|
||
|
|
});
|
||
|
|
|
||
|
|
return filteredCards
|
||
|
|
.sort((left, right) => {
|
||
|
|
const leftNumber = extractTarotCardNumericValue(left);
|
||
|
|
const rightNumber = extractTarotCardNumericValue(right);
|
||
|
|
if (leftNumber !== rightNumber) {
|
||
|
|
return (leftNumber ?? 0) - (rightNumber ?? 0);
|
||
|
|
}
|
||
|
|
if (left?.arcana !== right?.arcana) {
|
||
|
|
return left?.arcana === "Major" ? -1 : 1;
|
||
|
|
}
|
||
|
|
return String(left?.name || "").localeCompare(String(right?.name || ""));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderNumbersList() {
|
||
|
|
const { listEl, countEl } = getElements();
|
||
|
|
if (!listEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const entries = getNumbersDatasetEntries();
|
||
|
|
if (!entries.some((entry) => entry.value === activeNumberValue)) {
|
||
|
|
activeNumberValue = entries[0]?.value ?? 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
const fragment = document.createDocumentFragment();
|
||
|
|
entries.forEach((entry) => {
|
||
|
|
const button = document.createElement("button");
|
||
|
|
button.type = "button";
|
||
|
|
button.className = `planet-list-item${entry.value === activeNumberValue ? " is-selected" : ""}`;
|
||
|
|
button.dataset.numberValue = String(entry.value);
|
||
|
|
button.setAttribute("role", "option");
|
||
|
|
button.setAttribute("aria-selected", entry.value === activeNumberValue ? "true" : "false");
|
||
|
|
|
||
|
|
const nameEl = document.createElement("span");
|
||
|
|
nameEl.className = "planet-list-name";
|
||
|
|
nameEl.textContent = `${entry.label}`;
|
||
|
|
|
||
|
|
const metaEl = document.createElement("span");
|
||
|
|
metaEl.className = "planet-list-meta";
|
||
|
|
metaEl.textContent = `Opposite ${entry.opposite}`;
|
||
|
|
|
||
|
|
button.append(nameEl, metaEl);
|
||
|
|
fragment.appendChild(button);
|
||
|
|
});
|
||
|
|
|
||
|
|
listEl.replaceChildren(fragment);
|
||
|
|
if (countEl) {
|
||
|
|
countEl.textContent = `${entries.length} entries`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderNumberDetail(value) {
|
||
|
|
const { detailNameEl, detailTypeEl, detailSummaryEl, detailBodyEl } = getElements();
|
||
|
|
const entry = getNumberEntryByValue(value);
|
||
|
|
if (!entry) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const normalized = entry.value;
|
||
|
|
const opposite = entry.opposite;
|
||
|
|
const rootTarget = normalizeNumberValue(entry.digitalRoot);
|
||
|
|
|
||
|
|
if (detailNameEl) {
|
||
|
|
detailNameEl.textContent = `Number ${normalized} · ${entry.label}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (detailTypeEl) {
|
||
|
|
detailTypeEl.textContent = `Opposite: ${opposite}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (detailSummaryEl) {
|
||
|
|
detailSummaryEl.textContent = entry.summary || "";
|
||
|
|
}
|
||
|
|
|
||
|
|
renderNumbersSpecialPanel(normalized);
|
||
|
|
|
||
|
|
if (!detailBodyEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
detailBodyEl.replaceChildren();
|
||
|
|
|
||
|
|
const pairCardEl = document.createElement("div");
|
||
|
|
pairCardEl.className = "numbers-detail-card";
|
||
|
|
|
||
|
|
const pairHeadingEl = document.createElement("strong");
|
||
|
|
pairHeadingEl.textContent = "Number Pair";
|
||
|
|
|
||
|
|
const pairTextEl = document.createElement("div");
|
||
|
|
pairTextEl.className = "numbers-detail-text";
|
||
|
|
pairTextEl.textContent = `Opposite: ${opposite}`;
|
||
|
|
|
||
|
|
const keywordText = entry.keywords.length
|
||
|
|
? `Keywords: ${entry.keywords.join(", ")}`
|
||
|
|
: "Keywords: --";
|
||
|
|
const pairKeywordsEl = document.createElement("div");
|
||
|
|
pairKeywordsEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
pairKeywordsEl.textContent = keywordText;
|
||
|
|
|
||
|
|
const oppositeBtn = document.createElement("button");
|
||
|
|
oppositeBtn.type = "button";
|
||
|
|
oppositeBtn.className = "numbers-nav-btn";
|
||
|
|
oppositeBtn.textContent = `Open Opposite Number ${opposite}`;
|
||
|
|
oppositeBtn.addEventListener("click", () => {
|
||
|
|
selectNumberEntry(opposite);
|
||
|
|
});
|
||
|
|
|
||
|
|
pairCardEl.append(pairHeadingEl, pairTextEl, pairKeywordsEl, oppositeBtn);
|
||
|
|
|
||
|
|
const kabbalahCardEl = document.createElement("div");
|
||
|
|
kabbalahCardEl.className = "numbers-detail-card";
|
||
|
|
|
||
|
|
const kabbalahHeadingEl = document.createElement("strong");
|
||
|
|
kabbalahHeadingEl.textContent = "Kabbalah Link";
|
||
|
|
|
||
|
|
const kabbalahNode = Number(entry?.associations?.kabbalahNode);
|
||
|
|
const kabbalahTextEl = document.createElement("div");
|
||
|
|
kabbalahTextEl.className = "numbers-detail-text";
|
||
|
|
kabbalahTextEl.textContent = `Tree node target: ${kabbalahNode}`;
|
||
|
|
|
||
|
|
const kabbalahBtn = document.createElement("button");
|
||
|
|
kabbalahBtn.type = "button";
|
||
|
|
kabbalahBtn.className = "numbers-nav-btn";
|
||
|
|
kabbalahBtn.textContent = `Open Kabbalah Tree Node ${kabbalahNode}`;
|
||
|
|
kabbalahBtn.addEventListener("click", () => {
|
||
|
|
document.dispatchEvent(new CustomEvent("nav:kabbalah-path", {
|
||
|
|
detail: { pathNo: kabbalahNode }
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
kabbalahCardEl.append(kabbalahHeadingEl, kabbalahTextEl, kabbalahBtn);
|
||
|
|
|
||
|
|
const alphabetCardEl = document.createElement("div");
|
||
|
|
alphabetCardEl.className = "numbers-detail-card";
|
||
|
|
|
||
|
|
const alphabetHeadingEl = document.createElement("strong");
|
||
|
|
alphabetHeadingEl.textContent = "Alphabet Links";
|
||
|
|
|
||
|
|
const alphabetLinksWrapEl = document.createElement("div");
|
||
|
|
alphabetLinksWrapEl.className = "numbers-links-wrap";
|
||
|
|
|
||
|
|
const alphabetLinks = getAlphabetPositionLinksForDigitalRoot(rootTarget);
|
||
|
|
if (!alphabetLinks.length) {
|
||
|
|
const emptyAlphabetEl = document.createElement("div");
|
||
|
|
emptyAlphabetEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
emptyAlphabetEl.textContent = "No alphabet position entries found for this digital root yet.";
|
||
|
|
alphabetLinksWrapEl.appendChild(emptyAlphabetEl);
|
||
|
|
} else {
|
||
|
|
alphabetLinks.forEach((link) => {
|
||
|
|
const button = document.createElement("button");
|
||
|
|
button.type = "button";
|
||
|
|
button.className = "numbers-nav-btn";
|
||
|
|
button.textContent = `${link.alphabet}: ${link.label}`;
|
||
|
|
button.addEventListener("click", () => {
|
||
|
|
document.dispatchEvent(new CustomEvent("nav:alphabet", {
|
||
|
|
detail: link.detail
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
alphabetLinksWrapEl.appendChild(button);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
alphabetCardEl.append(alphabetHeadingEl, alphabetLinksWrapEl);
|
||
|
|
|
||
|
|
const tarotCardEl = document.createElement("div");
|
||
|
|
tarotCardEl.className = "numbers-detail-card";
|
||
|
|
|
||
|
|
const tarotHeadingEl = document.createElement("strong");
|
||
|
|
tarotHeadingEl.textContent = "Tarot Links";
|
||
|
|
|
||
|
|
const tarotLinksWrapEl = document.createElement("div");
|
||
|
|
tarotLinksWrapEl.className = "numbers-links-wrap";
|
||
|
|
|
||
|
|
const tarotCards = getTarotCardsForDigitalRoot(rootTarget, entry);
|
||
|
|
if (!tarotCards.length) {
|
||
|
|
const emptyEl = document.createElement("div");
|
||
|
|
emptyEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
emptyEl.textContent = "No tarot numeric entries found yet for this root. Add card numbers to map them.";
|
||
|
|
tarotLinksWrapEl.appendChild(emptyEl);
|
||
|
|
} else {
|
||
|
|
tarotCards.forEach((card) => {
|
||
|
|
const button = document.createElement("button");
|
||
|
|
button.type = "button";
|
||
|
|
button.className = "numbers-nav-btn";
|
||
|
|
button.textContent = `${card.name}`;
|
||
|
|
button.addEventListener("click", () => {
|
||
|
|
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
|
||
|
|
detail: { cardName: card.name }
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
tarotLinksWrapEl.appendChild(button);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
tarotCardEl.append(tarotHeadingEl, tarotLinksWrapEl);
|
||
|
|
|
||
|
|
const calendarCardEl = document.createElement("div");
|
||
|
|
calendarCardEl.className = "numbers-detail-card";
|
||
|
|
|
||
|
|
const calendarHeadingEl = document.createElement("strong");
|
||
|
|
calendarHeadingEl.textContent = "Calendar Links";
|
||
|
|
|
||
|
|
const calendarLinksWrapEl = document.createElement("div");
|
||
|
|
calendarLinksWrapEl.className = "numbers-links-wrap";
|
||
|
|
|
||
|
|
const calendarLinks = getCalendarMonthLinksForNumber(normalized);
|
||
|
|
if (!calendarLinks.length) {
|
||
|
|
const emptyCalendarEl = document.createElement("div");
|
||
|
|
emptyCalendarEl.className = "numbers-detail-text numbers-detail-text--muted";
|
||
|
|
emptyCalendarEl.textContent = "No calendar months currently mapped to this number.";
|
||
|
|
calendarLinksWrapEl.appendChild(emptyCalendarEl);
|
||
|
|
} else {
|
||
|
|
calendarLinks.forEach((link) => {
|
||
|
|
const button = document.createElement("button");
|
||
|
|
button.type = "button";
|
||
|
|
button.className = "numbers-nav-btn";
|
||
|
|
button.textContent = `${link.calendarLabel}: ${link.monthName} (Month ${link.monthOrder})`;
|
||
|
|
button.addEventListener("click", () => {
|
||
|
|
document.dispatchEvent(new CustomEvent("nav:calendar-month", {
|
||
|
|
detail: {
|
||
|
|
calendarId: link.calendarId,
|
||
|
|
monthId: link.monthId
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
calendarLinksWrapEl.appendChild(button);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
calendarCardEl.append(calendarHeadingEl, calendarLinksWrapEl);
|
||
|
|
|
||
|
|
detailBodyEl.append(pairCardEl, kabbalahCardEl, alphabetCardEl, tarotCardEl, calendarCardEl);
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectNumberEntry(value) {
|
||
|
|
const entry = getNumberEntryByValue(value);
|
||
|
|
activeNumberValue = entry ? entry.value : 0;
|
||
|
|
renderNumbersList();
|
||
|
|
renderNumberDetail(activeNumberValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
function ensureNumbersSection() {
|
||
|
|
const { listEl } = getElements();
|
||
|
|
if (!listEl) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!initialized) {
|
||
|
|
listEl.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 value = Number(button.dataset.numberValue);
|
||
|
|
if (!Number.isFinite(value)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
selectNumberEntry(value);
|
||
|
|
});
|
||
|
|
|
||
|
|
initialized = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
renderNumbersList();
|
||
|
|
renderNumberDetail(activeNumberValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
function init(nextConfig = {}) {
|
||
|
|
config = {
|
||
|
|
...config,
|
||
|
|
...nextConfig
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
window.TarotNumbersUi = {
|
||
|
|
...(window.TarotNumbersUi || {}),
|
||
|
|
init,
|
||
|
|
ensureNumbersSection,
|
||
|
|
selectNumberEntry,
|
||
|
|
normalizeNumberValue
|
||
|
|
};
|
||
|
|
})();
|