1342 lines
42 KiB
JavaScript
1342 lines
42 KiB
JavaScript
(function () {
|
||
const MAJOR_CARDS = [
|
||
{
|
||
number: 0,
|
||
name: "The Fool",
|
||
summary: "Open-hearted beginnings, leap of faith, and the sacred unknown.",
|
||
upright: "Fresh starts, trust in the path, and inspired spontaneity.",
|
||
reversed: "Recklessness, fear of risk, or resistance to a needed new beginning.",
|
||
keywords: ["beginnings", "faith", "innocence", "freedom", "journey"],
|
||
relations: ["Element: Air", "Hebrew Letter: Aleph"]
|
||
},
|
||
{
|
||
number: 1,
|
||
name: "The Magus",
|
||
summary: "Focused will and conscious manifestation through aligned tools.",
|
||
upright: "Skill, initiative, concentration, and transforming intent into action.",
|
||
reversed: "Scattered power, manipulation, or blocked self-belief.",
|
||
keywords: ["will", "manifestation", "focus", "skill", "agency"],
|
||
relations: ["Planet: Mercury", "Hebrew Letter: Beth"]
|
||
},
|
||
{
|
||
number: 2,
|
||
name: "The High Priestess",
|
||
summary: "Inner knowing, sacred silence, and intuitive perception.",
|
||
upright: "Intuition, subtle insight, patience, and hidden wisdom.",
|
||
reversed: "Disconnection from inner voice or confusion around truth.",
|
||
keywords: ["intuition", "mystery", "stillness", "inner-voice", "receptivity"],
|
||
relations: ["Planet: Luna", "Hebrew Letter: Gimel"]
|
||
},
|
||
{
|
||
number: 3,
|
||
name: "The Empress",
|
||
summary: "Creative abundance, nurture, and embodied growth.",
|
||
upright: "Fertility, care, beauty, comfort, and creative flourishing.",
|
||
reversed: "Creative drought, overgiving, or neglecting self-nourishment.",
|
||
keywords: ["abundance", "creation", "nurture", "beauty", "growth"],
|
||
relations: ["Planet: Venus", "Hebrew Letter: Daleth"]
|
||
},
|
||
{
|
||
number: 4,
|
||
name: "The Emperor",
|
||
summary: "Structure, authority, and stable leadership.",
|
||
upright: "Order, discipline, protection, and strategic direction.",
|
||
reversed: "Rigidity, domination, or weak boundaries.",
|
||
keywords: ["structure", "authority", "stability", "leadership", "boundaries"],
|
||
relations: ["Zodiac: Aries", "Hebrew Letter: He"]
|
||
},
|
||
{
|
||
number: 5,
|
||
name: "The Hierophant",
|
||
summary: "Tradition, spiritual instruction, and shared values.",
|
||
upright: "Learning from lineage, ritual, ethics, and mentorship.",
|
||
reversed: "Dogma, rebellion without grounding, or spiritual stagnation.",
|
||
keywords: ["tradition", "teaching", "ritual", "ethics", "lineage"],
|
||
relations: ["Zodiac: Taurus", "Hebrew Letter: Vav"]
|
||
},
|
||
{
|
||
number: 6,
|
||
name: "The Lovers",
|
||
summary: "Union, alignment, and value-centered choices.",
|
||
upright: "Harmony, connection, and conscious commitment.",
|
||
reversed: "Misalignment, indecision, or disconnection in relationship.",
|
||
keywords: ["union", "choice", "alignment", "connection", "values"],
|
||
relations: ["Zodiac: Gemini", "Hebrew Letter: Zayin"]
|
||
},
|
||
{
|
||
number: 7,
|
||
name: "The Chariot",
|
||
summary: "Directed momentum and mastery through discipline.",
|
||
upright: "Determination, movement, and victory through control.",
|
||
reversed: "Loss of direction, conflict of will, or stalled progress.",
|
||
keywords: ["drive", "control", "momentum", "victory", "direction"],
|
||
relations: ["Zodiac: Cancer", "Hebrew Letter: Cheth"]
|
||
},
|
||
{
|
||
number: 8,
|
||
name: "Lust",
|
||
summary: "Courageous life-force, passionate integrity, and heart-led power.",
|
||
upright: "Vitality, confidence, and wholehearted creative expression.",
|
||
reversed: "Self-doubt, depletion, or misdirected desire.",
|
||
keywords: ["vitality", "passion", "courage", "magnetism", "heart-power"],
|
||
relations: ["Zodiac: Leo", "Hebrew Letter: Teth"]
|
||
},
|
||
{
|
||
number: 9,
|
||
name: "The Hermit",
|
||
summary: "Inner pilgrimage, discernment, and sacred solitude.",
|
||
upright: "Reflection, guidance, and deepening wisdom.",
|
||
reversed: "Isolation, avoidance, or overanalysis.",
|
||
keywords: ["solitude", "wisdom", "search", "reflection", "discernment"],
|
||
relations: ["Zodiac: Virgo", "Hebrew Letter: Yod"]
|
||
},
|
||
{
|
||
number: 10,
|
||
name: "Fortune",
|
||
summary: "Cycles, timing, and turning points guided by greater rhythms.",
|
||
upright: "Opportunity, momentum, and fated change.",
|
||
reversed: "Resistance to cycles, delay, or repeating old patterns.",
|
||
keywords: ["cycles", "timing", "change", "destiny", "turning-point"],
|
||
relations: ["Planet: Jupiter", "Hebrew Letter: Kaph"]
|
||
},
|
||
{
|
||
number: 11,
|
||
name: "Justice",
|
||
summary: "Balance, accountability, and clear consequence.",
|
||
upright: "Fairness, truth, and ethical alignment.",
|
||
reversed: "Bias, denial, or avoidance of responsibility.",
|
||
keywords: ["balance", "truth", "accountability", "law", "clarity"],
|
||
relations: ["Zodiac: Libra", "Hebrew Letter: Lamed"]
|
||
},
|
||
{
|
||
number: 12,
|
||
name: "The Hanged Man",
|
||
summary: "Sacred pause, surrender, and transformed perspective.",
|
||
upright: "Release, contemplation, and spiritual reframing.",
|
||
reversed: "Stagnation, martyrdom, or refusing to let go.",
|
||
keywords: ["surrender", "pause", "perspective", "suspension", "release"],
|
||
relations: ["Element: Water", "Hebrew Letter: Mem"]
|
||
},
|
||
{
|
||
number: 13,
|
||
name: "Death",
|
||
summary: "Endings that clear space for rebirth and renewal.",
|
||
upright: "Transformation, completion, and deep release.",
|
||
reversed: "Clinging, fear of change, or prolonged transition.",
|
||
keywords: ["transformation", "ending", "renewal", "release", "rebirth"],
|
||
relations: ["Zodiac: Scorpio", "Hebrew Letter: Nun"]
|
||
},
|
||
{
|
||
number: 14,
|
||
name: "Art",
|
||
summary: "Alchemy, integration, and harmonizing opposites.",
|
||
upright: "Balance through blending, refinement, and patience.",
|
||
reversed: "Imbalance, excess, or fragmented energies.",
|
||
keywords: ["alchemy", "integration", "balance", "blending", "refinement"],
|
||
relations: ["Zodiac: Sagittarius", "Hebrew Letter: Samekh"]
|
||
},
|
||
{
|
||
number: 15,
|
||
name: "The Devil",
|
||
summary: "Attachment, shadow desire, and material entanglement.",
|
||
upright: "Confronting bondage, ambition, and raw instinct.",
|
||
reversed: "Liberation, breaking chains, or denial of shadow.",
|
||
keywords: ["attachment", "shadow", "instinct", "temptation", "bondage"],
|
||
relations: ["Zodiac: Capricorn", "Hebrew Letter: Ayin"]
|
||
},
|
||
{
|
||
number: 16,
|
||
name: "The Tower",
|
||
summary: "Sudden revelation that dismantles false structures.",
|
||
upright: "Shock, breakthrough, and necessary collapse.",
|
||
reversed: "Delayed upheaval, denial, or internal crisis.",
|
||
keywords: ["upheaval", "revelation", "breakthrough", "collapse", "truth"],
|
||
relations: ["Planet: Mars", "Hebrew Letter: Pe"]
|
||
},
|
||
{
|
||
number: 17,
|
||
name: "The Star",
|
||
summary: "Healing hope, guidance, and spiritual renewal.",
|
||
upright: "Inspiration, serenity, and trust in the future.",
|
||
reversed: "Discouragement, doubt, or loss of faith.",
|
||
keywords: ["hope", "healing", "guidance", "renewal", "faith"],
|
||
relations: ["Zodiac: Aquarius", "Hebrew Letter: Tzaddi"]
|
||
},
|
||
{
|
||
number: 18,
|
||
name: "The Moon",
|
||
summary: "Dream, uncertainty, and passage through the subconscious.",
|
||
upright: "Intuition, mystery, and emotional depth.",
|
||
reversed: "Confusion clearing, projection, or fear illusions.",
|
||
keywords: ["dream", "mystery", "intuition", "subconscious", "illusion"],
|
||
relations: ["Zodiac: Pisces", "Hebrew Letter: Qoph"]
|
||
},
|
||
{
|
||
number: 19,
|
||
name: "The Sun",
|
||
summary: "Radiance, confidence, and vital life affirmation.",
|
||
upright: "Joy, clarity, success, and openness.",
|
||
reversed: "Temporary clouding, ego glare, or delayed confidence.",
|
||
keywords: ["joy", "clarity", "vitality", "success", "radiance"],
|
||
relations: ["Planet: Sol", "Hebrew Letter: Resh"]
|
||
},
|
||
{
|
||
number: 20,
|
||
name: "Aeon",
|
||
summary: "Awakening call, reckoning, and soul-level renewal.",
|
||
upright: "Judgment, liberation, and answering purpose.",
|
||
reversed: "Self-judgment, hesitation, or resistance to calling.",
|
||
keywords: ["awakening", "reckoning", "calling", "renewal", "release"],
|
||
relations: ["Element: Fire", "Hebrew Letter: Shin"]
|
||
},
|
||
{
|
||
number: 21,
|
||
name: "Universe",
|
||
summary: "Completion, integration, and embodied wholeness.",
|
||
upright: "Fulfillment, mastery, and successful closure.",
|
||
reversed: "Incomplete cycle, loose ends, or delayed completion.",
|
||
keywords: ["completion", "wholeness", "integration", "mastery", "closure"],
|
||
relations: ["Planet: Saturn", "Hebrew Letter: Tav"]
|
||
}
|
||
];
|
||
|
||
const SUITS = ["Cups", "Wands", "Swords", "Disks"];
|
||
const NUMBER_RANKS = ["Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"];
|
||
const COURT_RANKS = ["Knight", "Queen", "Prince", "Princess"];
|
||
|
||
const SUIT_INFO = {
|
||
cups: {
|
||
element: "Water",
|
||
domain: "emotion, intuition, relationship",
|
||
keywords: ["emotion", "intuition", "connection"]
|
||
},
|
||
wands: {
|
||
element: "Fire",
|
||
domain: "will, creativity, drive",
|
||
keywords: ["will", "drive", "inspiration"]
|
||
},
|
||
swords: {
|
||
element: "Air",
|
||
domain: "mind, truth, communication",
|
||
keywords: ["mind", "truth", "clarity"]
|
||
},
|
||
disks: {
|
||
element: "Earth",
|
||
domain: "body, craft, material life",
|
||
keywords: ["resources", "craft", "stability"]
|
||
}
|
||
};
|
||
|
||
const RANK_INFO = {
|
||
ace: {
|
||
number: 1,
|
||
upright: "Seed-force, pure potential, and concentrated essence.",
|
||
reversed: "Blocked potential, delay, or difficulty beginning.",
|
||
keywords: ["seed", "potential", "spark"]
|
||
},
|
||
two: {
|
||
number: 2,
|
||
upright: "Polarity seeking balance, exchange, and response.",
|
||
reversed: "Imbalance, friction, or uncertain choices.",
|
||
keywords: ["duality", "balance", "exchange"]
|
||
},
|
||
three: {
|
||
number: 3,
|
||
upright: "Initial growth, expansion, and expression.",
|
||
reversed: "Scattered growth or stalled development.",
|
||
keywords: ["growth", "expansion", "expression"]
|
||
},
|
||
four: {
|
||
number: 4,
|
||
upright: "Structure, boundaries, and stabilizing form.",
|
||
reversed: "Rigidity, inertia, or unstable foundations.",
|
||
keywords: ["structure", "stability", "foundation"]
|
||
},
|
||
five: {
|
||
number: 5,
|
||
upright: "Challenge, disruption, and catalytic conflict.",
|
||
reversed: "Lingering tension or fear of needed change.",
|
||
keywords: ["challenge", "conflict", "change"]
|
||
},
|
||
six: {
|
||
number: 6,
|
||
upright: "Rebalancing, harmony, and restorative flow.",
|
||
reversed: "Partial recovery or unresolved imbalance.",
|
||
keywords: ["harmony", "repair", "flow"]
|
||
},
|
||
seven: {
|
||
number: 7,
|
||
upright: "Testing, strategy, and spiritual/mental refinement.",
|
||
reversed: "Doubt, overdefense, or poor strategy.",
|
||
keywords: ["test", "strategy", "refinement"]
|
||
},
|
||
eight: {
|
||
number: 8,
|
||
upright: "Adjustment, movement, and disciplined power.",
|
||
reversed: "Restriction, frustration, or scattered effort.",
|
||
keywords: ["movement", "discipline", "adjustment"]
|
||
},
|
||
nine: {
|
||
number: 9,
|
||
upright: "Intensity, culmination, and mature force.",
|
||
reversed: "Excess, overload, or unresolved pressure.",
|
||
keywords: ["culmination", "intensity", "maturity"]
|
||
},
|
||
ten: {
|
||
number: 10,
|
||
upright: "Completion, overflow, and transition into a new cycle.",
|
||
reversed: "Overextension, depletion, or difficulty releasing.",
|
||
keywords: ["completion", "threshold", "transition"]
|
||
}
|
||
};
|
||
|
||
const COURT_INFO = {
|
||
knight: {
|
||
role: "active catalyst and questing force",
|
||
elementalFace: "Fire of",
|
||
upright: "Bold pursuit, direct action, and forward momentum.",
|
||
reversed: "Impulsiveness, burnout, or unfocused drive.",
|
||
keywords: ["action", "drive", "initiative"]
|
||
},
|
||
queen: {
|
||
role: "magnetic vessel and emotional intelligence",
|
||
elementalFace: "Water of",
|
||
upright: "Receptive mastery, mature feeling, and embodied wisdom.",
|
||
reversed: "Withholding, moodiness, or passive control.",
|
||
keywords: ["receptivity", "maturity", "depth"]
|
||
},
|
||
prince: {
|
||
role: "strategic mediator and organizing mind",
|
||
elementalFace: "Air of",
|
||
upright: "Insight, communication, and adaptive coordination.",
|
||
reversed: "Overthinking, detachment, or mixed signals.",
|
||
keywords: ["strategy", "communication", "adaptability"]
|
||
},
|
||
princess: {
|
||
role: "manifesting ground and practical seed-force",
|
||
elementalFace: "Earth of",
|
||
upright: "Practical growth, devotion, and tangible follow-through.",
|
||
reversed: "Stagnation, timidity, or blocked growth.",
|
||
keywords: ["manifestation", "grounding", "devotion"]
|
||
}
|
||
};
|
||
|
||
const MAJOR_ALIASES = {
|
||
magician: "magus",
|
||
strength: "lust",
|
||
"wheel of fortune": "fortune",
|
||
temperance: "art",
|
||
judgement: "aeon",
|
||
judgment: "aeon",
|
||
world: "universe",
|
||
adjustment: "justice"
|
||
};
|
||
|
||
const MINOR_SUIT_ALIASES = {
|
||
pentacles: "disks",
|
||
pentacle: "disks",
|
||
coins: "disks",
|
||
disks: "disks"
|
||
};
|
||
|
||
const MINOR_NUMERAL_ALIASES = {
|
||
1: "ace",
|
||
2: "two",
|
||
3: "three",
|
||
4: "four",
|
||
5: "five",
|
||
6: "six",
|
||
7: "seven",
|
||
8: "eight",
|
||
9: "nine",
|
||
10: "ten"
|
||
};
|
||
|
||
const MONTH_NAME_BY_NUMBER = {
|
||
1: "January",
|
||
2: "February",
|
||
3: "March",
|
||
4: "April",
|
||
5: "May",
|
||
6: "June",
|
||
7: "July",
|
||
8: "August",
|
||
9: "September",
|
||
10: "October",
|
||
11: "November",
|
||
12: "December"
|
||
};
|
||
|
||
const MONTH_ID_BY_NUMBER = {
|
||
1: "january",
|
||
2: "february",
|
||
3: "march",
|
||
4: "april",
|
||
5: "may",
|
||
6: "june",
|
||
7: "july",
|
||
8: "august",
|
||
9: "september",
|
||
10: "october",
|
||
11: "november",
|
||
12: "december"
|
||
};
|
||
|
||
const COURT_DECAN_WINDOWS = {
|
||
"Knight of Wands": ["scorpio-3", "sagittarius-1", "sagittarius-2"],
|
||
"Queen of Disks": ["sagittarius-3", "capricorn-1", "capricorn-2"],
|
||
"Prince of Swords": ["capricorn-3", "aquarius-1", "aquarius-2"],
|
||
"Knight of Cups": ["aquarius-3", "pisces-1", "pisces-2"],
|
||
"Queen of Wands": ["pisces-3", "aries-1", "aries-2"],
|
||
"Prince of Disks": ["aries-3", "taurus-1", "taurus-2"],
|
||
"Knight of Swords": ["taurus-3", "gemini-1", "gemini-2"],
|
||
"Queen of Cups": ["gemini-3", "cancer-1", "cancer-2"],
|
||
"Prince of Wands": ["cancer-3", "leo-1", "leo-2"],
|
||
"Knight of Disks": ["leo-3", "virgo-1", "virgo-2"],
|
||
"Queen of Swords": ["virgo-3", "libra-1", "libra-2"],
|
||
"Prince of Cups": ["libra-3", "scorpio-1", "scorpio-2"]
|
||
};
|
||
|
||
const MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||
|
||
const MAJOR_HEBREW_LETTER_ID_BY_CARD = {
|
||
fool: "alef",
|
||
magus: "bet",
|
||
"high priestess": "gimel",
|
||
empress: "dalet",
|
||
emperor: "he",
|
||
hierophant: "vav",
|
||
lovers: "zayin",
|
||
chariot: "het",
|
||
lust: "tet",
|
||
hermit: "yod",
|
||
fortune: "kaf",
|
||
justice: "lamed",
|
||
"hanged man": "mem",
|
||
death: "nun",
|
||
art: "samekh",
|
||
devil: "ayin",
|
||
tower: "pe",
|
||
star: "tsadi",
|
||
moon: "qof",
|
||
sun: "resh",
|
||
aeon: "shin",
|
||
universe: "tav"
|
||
};
|
||
|
||
const HEBREW_LETTER_ALIASES = {
|
||
aleph: "alef",
|
||
alef: "alef",
|
||
beth: "bet",
|
||
bet: "bet",
|
||
gimel: "gimel",
|
||
daleth: "dalet",
|
||
dalet: "dalet",
|
||
he: "he",
|
||
vav: "vav",
|
||
zayin: "zayin",
|
||
cheth: "het",
|
||
chet: "het",
|
||
het: "het",
|
||
teth: "tet",
|
||
tet: "tet",
|
||
yod: "yod",
|
||
kaph: "kaf",
|
||
kaf: "kaf",
|
||
lamed: "lamed",
|
||
mem: "mem",
|
||
nun: "nun",
|
||
samekh: "samekh",
|
||
ayin: "ayin",
|
||
pe: "pe",
|
||
tzaddi: "tsadi",
|
||
tzadi: "tsadi",
|
||
tsadi: "tsadi",
|
||
qoph: "qof",
|
||
qof: "qof",
|
||
resh: "resh",
|
||
shin: "shin",
|
||
tav: "tav"
|
||
};
|
||
|
||
function getTarotDbConfig(referenceData) {
|
||
const db = referenceData?.tarotDatabase;
|
||
const hasDb = db && typeof db === "object";
|
||
|
||
const majorCards = hasDb && Array.isArray(db.majorCards) && db.majorCards.length
|
||
? db.majorCards
|
||
: MAJOR_CARDS;
|
||
|
||
const suits = hasDb && Array.isArray(db.suits) && db.suits.length
|
||
? db.suits
|
||
: SUITS;
|
||
|
||
const numberRanks = hasDb && Array.isArray(db.numberRanks) && db.numberRanks.length
|
||
? db.numberRanks
|
||
: NUMBER_RANKS;
|
||
|
||
const courtRanks = hasDb && Array.isArray(db.courtRanks) && db.courtRanks.length
|
||
? db.courtRanks
|
||
: COURT_RANKS;
|
||
|
||
const suitInfo = hasDb && db.suitInfo && typeof db.suitInfo === "object"
|
||
? db.suitInfo
|
||
: SUIT_INFO;
|
||
|
||
const rankInfo = hasDb && db.rankInfo && typeof db.rankInfo === "object"
|
||
? db.rankInfo
|
||
: RANK_INFO;
|
||
|
||
const courtInfo = hasDb && db.courtInfo && typeof db.courtInfo === "object"
|
||
? db.courtInfo
|
||
: COURT_INFO;
|
||
|
||
const courtDecanWindows = hasDb && db.courtDecanWindows && typeof db.courtDecanWindows === "object"
|
||
? db.courtDecanWindows
|
||
: COURT_DECAN_WINDOWS;
|
||
|
||
return {
|
||
majorCards,
|
||
suits,
|
||
numberRanks,
|
||
courtRanks,
|
||
suitInfo,
|
||
rankInfo,
|
||
courtInfo,
|
||
courtDecanWindows
|
||
};
|
||
}
|
||
|
||
function normalizeCardName(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/^the\s+/, "")
|
||
.replace(/\s+/g, " ");
|
||
}
|
||
|
||
function canonicalCardName(value) {
|
||
const normalized = normalizeCardName(value);
|
||
const majorCanonical = MAJOR_ALIASES[normalized] || normalized;
|
||
const withSuitAliases = majorCanonical.replace(/\bof\s+(pentacles?|coins?)\b/i, "of disks");
|
||
const numberMatch = withSuitAliases.match(/^(\d{1,2})\s+of\s+(.+)$/i);
|
||
|
||
if (numberMatch) {
|
||
const number = Number(numberMatch[1]);
|
||
const suit = String(numberMatch[2] || "").trim();
|
||
const numberWord = MINOR_NUMERAL_ALIASES[number];
|
||
if (numberWord && suit) {
|
||
return `${numberWord} of ${suit}`;
|
||
}
|
||
}
|
||
|
||
return withSuitAliases;
|
||
}
|
||
|
||
function parseMonthDay(value) {
|
||
const [month, day] = String(value || "").split("-").map((part) => Number(part));
|
||
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||
return null;
|
||
}
|
||
return { month, day };
|
||
}
|
||
|
||
function monthDayToDate(monthDay, year) {
|
||
const parsed = parseMonthDay(monthDay);
|
||
if (!parsed) {
|
||
return null;
|
||
}
|
||
return new Date(year, parsed.month - 1, parsed.day);
|
||
}
|
||
|
||
function addDays(date, days) {
|
||
const next = new Date(date);
|
||
next.setDate(next.getDate() + days);
|
||
return next;
|
||
}
|
||
|
||
function formatMonthDayLabel(date) {
|
||
if (!(date instanceof Date)) {
|
||
return "--";
|
||
}
|
||
return `${MONTH_SHORT[date.getMonth()]} ${date.getDate()}`;
|
||
}
|
||
|
||
function formatMonthDayToken(date) {
|
||
if (!(date instanceof Date)) {
|
||
return "";
|
||
}
|
||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||
const day = String(date.getDate()).padStart(2, "0");
|
||
return `${month}-${day}`;
|
||
}
|
||
|
||
function normalizeMinorTarotCardName(value) {
|
||
const normalized = canonicalCardName(value);
|
||
return normalized
|
||
.split(" ")
|
||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||
.join(" ");
|
||
}
|
||
|
||
function deriveSummaryFromMeaning(meaningText, fallbackSummary) {
|
||
const normalized = String(meaningText || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
|
||
if (!normalized) {
|
||
return fallbackSummary;
|
||
}
|
||
|
||
const sentenceMatch = normalized.match(/^(.+?[.!?])(?:\s|$)/);
|
||
if (sentenceMatch && sentenceMatch[1]) {
|
||
return sentenceMatch[1].trim();
|
||
}
|
||
|
||
if (normalized.length <= 220) {
|
||
return normalized;
|
||
}
|
||
|
||
return `${normalized.slice(0, 217).trimEnd()}…`;
|
||
}
|
||
|
||
function applyMeaningText(cards, referenceData) {
|
||
const majorByTrumpNumber = referenceData?.tarotDatabase?.meanings?.majorByTrumpNumber;
|
||
const byCardName = referenceData?.tarotDatabase?.meanings?.byCardName;
|
||
|
||
if ((!majorByTrumpNumber || typeof majorByTrumpNumber !== "object")
|
||
&& (!byCardName || typeof byCardName !== "object")) {
|
||
return cards;
|
||
}
|
||
|
||
return cards.map((card) => {
|
||
const trumpNumber = Number(card?.number);
|
||
const isMajorTrumpCard = card?.arcana === "Major" && Number.isFinite(trumpNumber);
|
||
const canonicalName = canonicalCardName(card?.name);
|
||
|
||
const majorMeaning = isMajorTrumpCard
|
||
? String(majorByTrumpNumber?.[trumpNumber] || "").trim()
|
||
: "";
|
||
const nameMeaning = String(byCardName?.[canonicalName] || "").trim();
|
||
const selectedMeaning = majorMeaning || nameMeaning;
|
||
|
||
if (!selectedMeaning) {
|
||
return card;
|
||
}
|
||
|
||
return {
|
||
...card,
|
||
meaning: selectedMeaning,
|
||
summary: deriveSummaryFromMeaning(selectedMeaning, card.summary),
|
||
meanings: {
|
||
upright: selectedMeaning,
|
||
reversed: card?.meanings?.reversed || ""
|
||
}
|
||
};
|
||
});
|
||
}
|
||
|
||
function getSignDateBounds(sign) {
|
||
const start = monthDayToDate(sign?.start, 2025);
|
||
const endBase = monthDayToDate(sign?.end, 2025);
|
||
if (!start || !endBase) {
|
||
return null;
|
||
}
|
||
|
||
const wraps = endBase.getTime() < start.getTime();
|
||
const end = wraps ? monthDayToDate(sign?.end, 2026) : endBase;
|
||
if (!end) {
|
||
return null;
|
||
}
|
||
|
||
return { start, end };
|
||
}
|
||
|
||
function buildDecanDateRange(sign, decanIndex) {
|
||
const bounds = getSignDateBounds(sign);
|
||
if (!bounds || !Number.isFinite(Number(decanIndex))) {
|
||
return null;
|
||
}
|
||
|
||
const index = Number(decanIndex);
|
||
const start = addDays(bounds.start, (index - 1) * 10);
|
||
const nominalEnd = addDays(start, 9);
|
||
const end = nominalEnd.getTime() > bounds.end.getTime() ? bounds.end : nominalEnd;
|
||
|
||
return {
|
||
start,
|
||
end,
|
||
startMonth: start.getMonth() + 1,
|
||
endMonth: end.getMonth() + 1,
|
||
startToken: formatMonthDayToken(start),
|
||
endToken: formatMonthDayToken(end),
|
||
label: `${formatMonthDayLabel(start)}–${formatMonthDayLabel(end)}`
|
||
};
|
||
}
|
||
|
||
function listMonthNumbersBetween(start, end) {
|
||
if (!(start instanceof Date) || !(end instanceof Date)) {
|
||
return [];
|
||
}
|
||
|
||
const result = [];
|
||
const seen = new Set();
|
||
const cursor = new Date(start.getFullYear(), start.getMonth(), 1);
|
||
const limit = new Date(end.getFullYear(), end.getMonth(), 1);
|
||
|
||
while (cursor.getTime() <= limit.getTime()) {
|
||
const monthNo = cursor.getMonth() + 1;
|
||
if (!seen.has(monthNo)) {
|
||
seen.add(monthNo);
|
||
result.push(monthNo);
|
||
}
|
||
cursor.setMonth(cursor.getMonth() + 1);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function getSignName(sign, fallback) {
|
||
return sign?.name?.en || sign?.name || sign?.id || fallback || "Unknown";
|
||
}
|
||
|
||
function buildDecanMetadata(decan, sign) {
|
||
if (!decan || !sign) {
|
||
return null;
|
||
}
|
||
|
||
const index = Number(decan.index);
|
||
if (!Number.isFinite(index)) {
|
||
return null;
|
||
}
|
||
|
||
const startDegree = (index - 1) * 10;
|
||
const endDegree = startDegree + 10;
|
||
const dateRange = buildDecanDateRange(sign, index);
|
||
|
||
return {
|
||
decan,
|
||
sign,
|
||
index,
|
||
signId: sign.id,
|
||
signName: getSignName(sign, decan.signId),
|
||
signSymbol: sign.symbol || "",
|
||
startDegree,
|
||
endDegree,
|
||
dateRange,
|
||
normalizedCardName: normalizeMinorTarotCardName(decan.tarotMinorArcana || "")
|
||
};
|
||
}
|
||
|
||
function collectCalendarMonthRelationsFromDecan(targetKey, relationMap, decanMeta) {
|
||
const dateRange = decanMeta?.dateRange;
|
||
if (!dateRange?.start || !dateRange?.end) {
|
||
return;
|
||
}
|
||
|
||
const monthNumbers = listMonthNumbersBetween(dateRange.start, dateRange.end);
|
||
monthNumbers.forEach((monthNo) => {
|
||
const monthId = MONTH_ID_BY_NUMBER[monthNo];
|
||
const monthName = MONTH_NAME_BY_NUMBER[monthNo] || `Month ${monthNo}`;
|
||
if (!monthId) {
|
||
return;
|
||
}
|
||
|
||
pushMapValue(
|
||
relationMap,
|
||
targetKey,
|
||
createRelation(
|
||
"calendarMonth",
|
||
`${monthId}-${decanMeta.signId}-${decanMeta.index}`,
|
||
`Calendar month: ${monthName} (${decanMeta.signName} decan ${decanMeta.index})`,
|
||
{
|
||
monthId,
|
||
name: monthName,
|
||
monthOrder: monthNo,
|
||
signId: decanMeta.signId,
|
||
signName: decanMeta.signName,
|
||
decanIndex: decanMeta.index,
|
||
dateRange: dateRange.label
|
||
}
|
||
)
|
||
);
|
||
});
|
||
}
|
||
|
||
function normalizeHebrewKey(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z]/g, "");
|
||
}
|
||
|
||
function buildHebrewLetterLookup(magickDataset) {
|
||
const letters = magickDataset?.grouped?.hebrewLetters;
|
||
const lookup = new Map();
|
||
|
||
if (!letters || typeof letters !== "object") {
|
||
return lookup;
|
||
}
|
||
|
||
Object.entries(letters).forEach(([letterId, entry]) => {
|
||
const idKey = normalizeHebrewKey(letterId);
|
||
const canonicalKey = HEBREW_LETTER_ALIASES[idKey] || idKey;
|
||
|
||
if (canonicalKey && !lookup.has(canonicalKey)) {
|
||
lookup.set(canonicalKey, entry);
|
||
}
|
||
|
||
const nameKey = normalizeHebrewKey(entry?.letter?.name);
|
||
const canonicalNameKey = HEBREW_LETTER_ALIASES[nameKey] || nameKey;
|
||
if (canonicalNameKey && !lookup.has(canonicalNameKey)) {
|
||
lookup.set(canonicalNameKey, entry);
|
||
}
|
||
|
||
const entryIdKey = normalizeHebrewKey(entry?.id);
|
||
const canonicalEntryIdKey = HEBREW_LETTER_ALIASES[entryIdKey] || entryIdKey;
|
||
if (canonicalEntryIdKey && !lookup.has(canonicalEntryIdKey)) {
|
||
lookup.set(canonicalEntryIdKey, entry);
|
||
}
|
||
});
|
||
|
||
return lookup;
|
||
}
|
||
|
||
function normalizeRelationId(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9]+/g, "-")
|
||
.replace(/(^-|-$)/g, "") || "unknown";
|
||
}
|
||
|
||
function createRelation(type, id, label, data = null) {
|
||
return {
|
||
type,
|
||
id: normalizeRelationId(id),
|
||
label: String(label || "").trim(),
|
||
data
|
||
};
|
||
}
|
||
|
||
function relationSignature(value) {
|
||
if (!value || typeof value !== "object") {
|
||
return String(value || "");
|
||
}
|
||
|
||
return `${value.type || "text"}|${value.id || ""}|${value.label || ""}`;
|
||
}
|
||
|
||
function parseLegacyRelation(text) {
|
||
const raw = String(text || "").trim();
|
||
const match = raw.match(/^([^:]+):\s*(.+)$/);
|
||
if (!match) {
|
||
return createRelation("note", raw, raw, { value: raw });
|
||
}
|
||
|
||
const key = normalizeRelationId(match[1]);
|
||
const value = String(match[2] || "").trim();
|
||
|
||
if (key === "element") {
|
||
return createRelation("element", value, `Element: ${value}`, { name: value });
|
||
}
|
||
|
||
if (key === "planet") {
|
||
return createRelation("planet", value, `Planet: ${value}`, { name: value });
|
||
}
|
||
|
||
if (key === "zodiac") {
|
||
return createRelation("zodiac", value, `Zodiac: ${value}`, { name: value });
|
||
}
|
||
|
||
if (key === "suit-domain") {
|
||
return createRelation("suitDomain", value, `Suit domain: ${value}`, { value });
|
||
}
|
||
|
||
if (key === "numerology") {
|
||
const numeric = Number(value);
|
||
return createRelation("numerology", value, `Numerology: ${value}`, {
|
||
value: Number.isFinite(numeric) ? numeric : value
|
||
});
|
||
}
|
||
|
||
if (key === "court-role") {
|
||
return createRelation("courtRole", value, `Court role: ${value}`, { value });
|
||
}
|
||
|
||
if (key === "hebrew-letter") {
|
||
const normalized = normalizeHebrewKey(value);
|
||
const canonical = HEBREW_LETTER_ALIASES[normalized] || normalized;
|
||
return createRelation("hebrewLetter", canonical, `Hebrew Letter: ${value}`, {
|
||
requestedName: value
|
||
});
|
||
}
|
||
|
||
return createRelation(key || "relation", value, raw, { value });
|
||
}
|
||
|
||
function buildHebrewLetterRelation(hebrewLetterId, hebrewLookup) {
|
||
if (!hebrewLetterId || !hebrewLookup) {
|
||
return null;
|
||
}
|
||
|
||
const normalizedId = normalizeHebrewKey(hebrewLetterId);
|
||
const canonicalId = HEBREW_LETTER_ALIASES[normalizedId] || normalizedId;
|
||
const entry = hebrewLookup.get(canonicalId);
|
||
if (!entry) {
|
||
return createRelation("hebrewLetter", canonicalId, `Hebrew Letter: ${hebrewLetterId}`, null);
|
||
}
|
||
|
||
const glyph = entry?.letter?.he || "";
|
||
const name = entry?.letter?.name || hebrewLetterId;
|
||
const latin = entry?.letter?.latin || "";
|
||
const index = Number.isFinite(entry?.index) ? entry.index : null;
|
||
const value = Number.isFinite(entry?.value) ? entry.value : null;
|
||
const meaning = entry?.meaning?.en || "";
|
||
|
||
const indexText = index !== null ? index : "?";
|
||
const valueText = value !== null ? value : "?";
|
||
const meaningText = meaning ? ` · ${meaning}` : "";
|
||
|
||
return createRelation(
|
||
"hebrewLetter",
|
||
entry?.id || canonicalId,
|
||
`Hebrew Letter: ${glyph} ${name} (${latin}) (index ${indexText}, value ${valueText})${meaningText}`.trim(),
|
||
{
|
||
id: entry?.id || canonicalId,
|
||
glyph,
|
||
name,
|
||
latin,
|
||
index,
|
||
value,
|
||
meaning
|
||
}
|
||
);
|
||
}
|
||
|
||
function pushMapValue(map, key, value) {
|
||
if (!key || !value) {
|
||
return;
|
||
}
|
||
|
||
if (!map.has(key)) {
|
||
map.set(key, []);
|
||
}
|
||
|
||
const existing = map.get(key);
|
||
const signature = relationSignature(value);
|
||
const duplicate = existing.some((entry) => relationSignature(entry) === signature);
|
||
|
||
if (!duplicate) {
|
||
existing.push(value);
|
||
}
|
||
}
|
||
|
||
function buildMajorDynamicRelations(referenceData) {
|
||
const relationMap = new Map();
|
||
|
||
const planets = referenceData?.planets && typeof referenceData.planets === "object"
|
||
? Object.values(referenceData.planets)
|
||
: [];
|
||
|
||
planets.forEach((planet) => {
|
||
const cardName = planet?.tarot?.majorArcana;
|
||
if (!cardName) {
|
||
return;
|
||
}
|
||
|
||
const relation = createRelation(
|
||
"planetCorrespondence",
|
||
planet?.id || planet?.name || cardName,
|
||
`Planet correspondence: ${planet.symbol || ""} ${planet.name || ""}`.trim(),
|
||
{
|
||
planetId: planet?.id || null,
|
||
symbol: planet?.symbol || "",
|
||
name: planet?.name || ""
|
||
}
|
||
);
|
||
pushMapValue(relationMap, canonicalCardName(cardName), relation);
|
||
});
|
||
|
||
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
||
signs.forEach((sign) => {
|
||
const cardName = sign?.tarot?.majorArcana;
|
||
if (!cardName) {
|
||
return;
|
||
}
|
||
|
||
const relation = createRelation(
|
||
"zodiacCorrespondence",
|
||
sign?.id || sign?.name || cardName,
|
||
`Zodiac correspondence: ${sign.symbol || ""} ${sign.name || ""}`.trim(),
|
||
{
|
||
signId: sign?.id || null,
|
||
symbol: sign?.symbol || "",
|
||
name: sign?.name || ""
|
||
}
|
||
);
|
||
pushMapValue(relationMap, canonicalCardName(cardName), relation);
|
||
});
|
||
|
||
return relationMap;
|
||
}
|
||
|
||
function buildMinorDecanRelations(referenceData) {
|
||
const relationMap = new Map();
|
||
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
||
const signById = Object.fromEntries(signs.map((sign) => [sign.id, sign]));
|
||
const planets = referenceData?.planets || {};
|
||
|
||
if (!referenceData?.decansBySign || typeof referenceData.decansBySign !== "object") {
|
||
return relationMap;
|
||
}
|
||
|
||
Object.entries(referenceData.decansBySign).forEach(([signId, decans]) => {
|
||
const sign = signById[signId];
|
||
if (!Array.isArray(decans) || !sign) {
|
||
return;
|
||
}
|
||
|
||
decans.forEach((decan) => {
|
||
const cardName = decan?.tarotMinorArcana;
|
||
if (!cardName) {
|
||
return;
|
||
}
|
||
|
||
const decanMeta = buildDecanMetadata(decan, sign);
|
||
if (!decanMeta) {
|
||
return;
|
||
}
|
||
|
||
const { startDegree, endDegree, dateRange, signId, signName, signSymbol, index } = decanMeta;
|
||
const ruler = planets[decan.rulerPlanetId] || null;
|
||
const cardKey = canonicalCardName(cardName);
|
||
|
||
pushMapValue(
|
||
relationMap,
|
||
cardKey,
|
||
createRelation(
|
||
"zodiac",
|
||
signId,
|
||
`Zodiac: ${sign.symbol || ""} ${signName}`.trim(),
|
||
{
|
||
signId,
|
||
signName,
|
||
symbol: sign.symbol || ""
|
||
}
|
||
)
|
||
);
|
||
|
||
pushMapValue(
|
||
relationMap,
|
||
cardKey,
|
||
createRelation(
|
||
"decan",
|
||
`${signId}-${index}`,
|
||
`Decan ${decan.index}: ${sign.symbol || ""} ${signName} (${startDegree}°–${endDegree}°)${dateRange ? ` · ${dateRange.label}` : ""}`.trim(),
|
||
{
|
||
signId,
|
||
signName,
|
||
signSymbol,
|
||
index,
|
||
startDegree,
|
||
endDegree,
|
||
dateStart: dateRange?.startToken || null,
|
||
dateEnd: dateRange?.endToken || null,
|
||
dateRange: dateRange?.label || null
|
||
}
|
||
)
|
||
);
|
||
|
||
collectCalendarMonthRelationsFromDecan(cardKey, relationMap, decanMeta);
|
||
|
||
if (ruler) {
|
||
pushMapValue(
|
||
relationMap,
|
||
cardKey,
|
||
createRelation(
|
||
"decanRuler",
|
||
`${signId}-${index}-${decan.rulerPlanetId}`,
|
||
`Decan ruler: ${ruler.symbol || ""} ${ruler.name || decan.rulerPlanetId}`.trim(),
|
||
{
|
||
signId,
|
||
decanIndex: index,
|
||
planetId: decan.rulerPlanetId,
|
||
symbol: ruler.symbol || "",
|
||
name: ruler.name || decan.rulerPlanetId
|
||
}
|
||
)
|
||
);
|
||
}
|
||
});
|
||
});
|
||
|
||
return relationMap;
|
||
}
|
||
|
||
function buildMajorCards(referenceData, magickDataset) {
|
||
const tarotDb = getTarotDbConfig(referenceData);
|
||
const dynamicRelations = buildMajorDynamicRelations(referenceData);
|
||
const hebrewLookup = buildHebrewLetterLookup(magickDataset);
|
||
|
||
return tarotDb.majorCards.map((card) => {
|
||
const canonicalName = canonicalCardName(card.name);
|
||
const dynamic = dynamicRelations.get(canonicalName) || [];
|
||
const hebrewLetterId = MAJOR_HEBREW_LETTER_ID_BY_CARD[canonicalName] || null;
|
||
const hebrewLetterRelation = buildHebrewLetterRelation(hebrewLetterId, hebrewLookup);
|
||
const staticRelations = (card.relations || [])
|
||
.map((relation) => parseLegacyRelation(relation))
|
||
.filter((relation) => relation.type !== "hebrewLetter" && relation.type !== "zodiac" && relation.type !== "planet");
|
||
|
||
return {
|
||
arcana: "Major",
|
||
name: card.name,
|
||
number: card.number,
|
||
suit: null,
|
||
rank: null,
|
||
hebrewLetterId,
|
||
hebrewLetter: hebrewLetterRelation?.data || null,
|
||
summary: card.summary,
|
||
meanings: {
|
||
upright: card.upright,
|
||
reversed: card.reversed
|
||
},
|
||
keywords: [...card.keywords],
|
||
relations: [
|
||
...staticRelations,
|
||
...(hebrewLetterRelation ? [hebrewLetterRelation] : []),
|
||
...dynamic
|
||
]
|
||
};
|
||
});
|
||
}
|
||
|
||
function buildNumberMinorCards(referenceData) {
|
||
const tarotDb = getTarotDbConfig(referenceData);
|
||
const decanRelations = buildMinorDecanRelations(referenceData);
|
||
const cards = [];
|
||
|
||
tarotDb.suits.forEach((suit) => {
|
||
const suitKey = suit.toLowerCase();
|
||
const suitInfo = tarotDb.suitInfo[suitKey];
|
||
if (!suitInfo) {
|
||
return;
|
||
}
|
||
|
||
tarotDb.numberRanks.forEach((rank) => {
|
||
const rankKey = rank.toLowerCase();
|
||
const rankInfo = tarotDb.rankInfo[rankKey];
|
||
if (!rankInfo) {
|
||
return;
|
||
}
|
||
const cardName = `${rank} of ${suit}`;
|
||
const dynamicRelations = decanRelations.get(canonicalCardName(cardName)) || [];
|
||
|
||
cards.push({
|
||
arcana: "Minor",
|
||
name: cardName,
|
||
number: null,
|
||
suit,
|
||
rank,
|
||
summary: `${rank} energy expressed through ${suitInfo.domain}.`,
|
||
meanings: {
|
||
upright: `${rankInfo.upright} In ${suit}, this emphasizes ${suitInfo.domain}.`,
|
||
reversed: `${rankInfo.reversed} In ${suit}, this may distort ${suitInfo.domain}.`
|
||
},
|
||
keywords: [...rankInfo.keywords, ...suitInfo.keywords],
|
||
relations: [
|
||
createRelation("element", suitInfo.element, `Element: ${suitInfo.element}`, {
|
||
name: suitInfo.element
|
||
}),
|
||
createRelation("suitDomain", `${suitKey}-${rankKey}`, `Suit domain: ${suitInfo.domain}`, {
|
||
suit: suit,
|
||
rank,
|
||
domain: suitInfo.domain
|
||
}),
|
||
createRelation("numerology", rankInfo.number, `Numerology: ${rankInfo.number}`, {
|
||
value: rankInfo.number
|
||
}),
|
||
...dynamicRelations
|
||
]
|
||
});
|
||
});
|
||
});
|
||
|
||
return cards;
|
||
}
|
||
|
||
function buildCourtMinorCards(referenceData) {
|
||
const tarotDb = getTarotDbConfig(referenceData);
|
||
const cards = [];
|
||
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
||
const signById = Object.fromEntries(signs.map((sign) => [sign.id, sign]));
|
||
|
||
const decanById = new Map();
|
||
const decansBySign = referenceData?.decansBySign || {};
|
||
Object.entries(decansBySign).forEach(([signId, decans]) => {
|
||
const sign = signById[signId];
|
||
if (!sign || !Array.isArray(decans)) {
|
||
return;
|
||
}
|
||
decans.forEach((decan) => {
|
||
if (!decan?.id) {
|
||
return;
|
||
}
|
||
const meta = buildDecanMetadata(decan, sign);
|
||
if (meta) {
|
||
decanById.set(decan.id, meta);
|
||
}
|
||
});
|
||
});
|
||
|
||
tarotDb.suits.forEach((suit) => {
|
||
const suitKey = suit.toLowerCase();
|
||
const suitInfo = tarotDb.suitInfo[suitKey];
|
||
if (!suitInfo) {
|
||
return;
|
||
}
|
||
|
||
tarotDb.courtRanks.forEach((rank) => {
|
||
const rankKey = rank.toLowerCase();
|
||
const courtInfo = tarotDb.courtInfo[rankKey];
|
||
if (!courtInfo) {
|
||
return;
|
||
}
|
||
const cardName = `${rank} of ${suit}`;
|
||
const windowDecanIds = tarotDb.courtDecanWindows[cardName] || [];
|
||
const windowDecans = windowDecanIds
|
||
.map((decanId) => decanById.get(decanId) || null)
|
||
.filter(Boolean);
|
||
|
||
const dynamicRelations = [];
|
||
const monthKeys = new Set();
|
||
|
||
windowDecans.forEach((meta) => {
|
||
dynamicRelations.push(
|
||
createRelation(
|
||
"decan",
|
||
`${meta.signId}-${meta.index}-${rankKey}-${suitKey}`,
|
||
`Decan ${meta.index}: ${meta.signSymbol} ${meta.signName} (${meta.startDegree}°–${meta.endDegree}°)${meta.dateRange ? ` · ${meta.dateRange.label}` : ""}`.trim(),
|
||
{
|
||
signId: meta.signId,
|
||
signName: meta.signName,
|
||
signSymbol: meta.signSymbol,
|
||
index: meta.index,
|
||
startDegree: meta.startDegree,
|
||
endDegree: meta.endDegree,
|
||
dateStart: meta.dateRange?.startToken || null,
|
||
dateEnd: meta.dateRange?.endToken || null,
|
||
dateRange: meta.dateRange?.label || null
|
||
}
|
||
)
|
||
);
|
||
|
||
const dateRange = meta.dateRange;
|
||
if (dateRange?.start && dateRange?.end) {
|
||
const monthNumbers = listMonthNumbersBetween(dateRange.start, dateRange.end);
|
||
monthNumbers.forEach((monthNo) => {
|
||
const monthId = MONTH_ID_BY_NUMBER[monthNo];
|
||
const monthName = MONTH_NAME_BY_NUMBER[monthNo] || `Month ${monthNo}`;
|
||
const monthKey = `${monthId}:${meta.signId}:${meta.index}`;
|
||
if (!monthId || monthKeys.has(monthKey)) {
|
||
return;
|
||
}
|
||
monthKeys.add(monthKey);
|
||
|
||
dynamicRelations.push(
|
||
createRelation(
|
||
"calendarMonth",
|
||
`${monthId}-${meta.signId}-${meta.index}-${rankKey}-${suitKey}`,
|
||
`Calendar month: ${monthName} (${meta.signName} decan ${meta.index})`,
|
||
{
|
||
monthId,
|
||
name: monthName,
|
||
monthOrder: monthNo,
|
||
signId: meta.signId,
|
||
signName: meta.signName,
|
||
decanIndex: meta.index,
|
||
dateRange: meta.dateRange?.label || null
|
||
}
|
||
)
|
||
);
|
||
});
|
||
}
|
||
});
|
||
|
||
if (windowDecans.length) {
|
||
const firstRange = windowDecans[0].dateRange;
|
||
const lastRange = windowDecans[windowDecans.length - 1].dateRange;
|
||
const windowLabel = firstRange && lastRange
|
||
? `${formatMonthDayLabel(firstRange.start)}–${formatMonthDayLabel(lastRange.end)}`
|
||
: "--";
|
||
|
||
dynamicRelations.unshift(
|
||
createRelation(
|
||
"courtDateWindow",
|
||
`${rankKey}-${suitKey}`,
|
||
`Court date window: ${windowLabel}`,
|
||
{
|
||
dateStart: firstRange?.startToken || null,
|
||
dateEnd: lastRange?.endToken || null,
|
||
dateRange: windowLabel,
|
||
decanIds: windowDecanIds
|
||
}
|
||
)
|
||
);
|
||
}
|
||
|
||
cards.push({
|
||
arcana: "Minor",
|
||
name: cardName,
|
||
number: null,
|
||
suit,
|
||
rank,
|
||
summary: `${rank} as ${courtInfo.role} within ${suitInfo.domain}.`,
|
||
meanings: {
|
||
upright: `${courtInfo.upright} In ${suit}, this guides ${suitInfo.domain}.`,
|
||
reversed: `${courtInfo.reversed} In ${suit}, this complicates ${suitInfo.domain}.`
|
||
},
|
||
keywords: [...courtInfo.keywords, ...suitInfo.keywords],
|
||
relations: [
|
||
createRelation("element", suitInfo.element, `Element: ${suitInfo.element}`, {
|
||
name: suitInfo.element
|
||
}),
|
||
createRelation(
|
||
"elementalFace",
|
||
`${rankKey}-${suitKey}`,
|
||
`${courtInfo.elementalFace} ${suitInfo.element}`,
|
||
{
|
||
rank,
|
||
suit,
|
||
elementalFace: courtInfo.elementalFace,
|
||
element: suitInfo.element
|
||
}
|
||
),
|
||
createRelation("courtRole", rankKey, `Court role: ${courtInfo.role}`, {
|
||
rank,
|
||
role: courtInfo.role
|
||
}),
|
||
...dynamicRelations
|
||
]
|
||
});
|
||
});
|
||
});
|
||
|
||
return cards;
|
||
}
|
||
|
||
function buildTarotDatabase(referenceData, magickDataset = null) {
|
||
const cards = [
|
||
...buildMajorCards(referenceData, magickDataset),
|
||
...buildNumberMinorCards(referenceData),
|
||
...buildCourtMinorCards(referenceData)
|
||
];
|
||
|
||
return applyMeaningText(cards, referenceData);
|
||
}
|
||
|
||
window.TarotCardDatabase = {
|
||
buildTarotDatabase
|
||
};
|
||
})();
|