Files
TaroTime/app/ui-alphabet.js
2026-03-07 01:09:00 -08:00

2082 lines
71 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ui-alphabet.js — Multi-alphabet browser (English / Hebrew / Greek / Arabic / Enochian) */
(function () {
"use strict";
const state = {
initialized: false,
alphabets: null,
activeAlphabet: "all",
selectedKey: null,
filters: {
query: "",
letterType: ""
},
fourWorldLayers: [],
monthRefsByHebrewId: new Map(),
cubeRefs: {
hebrewPlacementById: new Map(),
signPlacementById: new Map(),
planetPlacementById: new Map(),
pathPlacementByNo: new Map()
},
gematria: {
loadingPromise: null,
db: null,
listenersBound: false,
activeCipherId: "",
inputText: "",
scriptCharMap: new Map()
}
};
// ── Arabic display name table ─────────────────────────────────────────
const ARABIC_DISPLAY_NAMES = {
alif: "Alif", ba: "Ba", jeem: "Jeem", dal: "Dal", ha: "H\u0101",
waw: "W\u0101w", zayn: "Zayn", ha_khaa: "\u1e24\u0101", ta_tay: "\u1e6c\u0101", ya: "Y\u0101",
kaf: "K\u0101f", lam: "L\u0101m", meem: "M\u012bm", nun: "N\u016bn", seen: "S\u012bn",
ayn: "\u02bfAyn", fa: "F\u0101", sad: "\u1e62\u0101d", qaf: "Q\u0101f", ra: "R\u0101",
sheen: "Sh\u012bn", ta: "T\u0101", tha: "Th\u0101", kha: "Kh\u0101",
dhal: "Dh\u0101l", dad: "\u1e0c\u0101d", dha: "\u1e92\u0101", ghayn: "Ghayn"
};
function arabicDisplayName(letter) {
return ARABIC_DISPLAY_NAMES[letter && letter.name] || (String(letter && letter.name || "").charAt(0).toUpperCase() + String(letter && letter.name || "").slice(1));
}
// ── Element cache ────────────────────────────────────────────────────
let listEl, countEl, detailNameEl, detailSubEl, detailBodyEl;
let tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian;
let searchInputEl, searchClearEl, typeFilterEl;
let gematriaCipherEl, gematriaInputEl, gematriaResultEl, gematriaBreakdownEl;
function getElements() {
listEl = document.getElementById("alpha-letter-list");
countEl = document.getElementById("alpha-letter-count");
detailNameEl = document.getElementById("alpha-detail-name");
detailSubEl = document.getElementById("alpha-detail-sub");
detailBodyEl = document.getElementById("alpha-detail-body");
tabAll = document.getElementById("alpha-tab-all");
tabHebrew = document.getElementById("alpha-tab-hebrew");
tabGreek = document.getElementById("alpha-tab-greek");
tabEnglish = document.getElementById("alpha-tab-english");
tabArabic = document.getElementById("alpha-tab-arabic");
tabEnochian = document.getElementById("alpha-tab-enochian");
searchInputEl = document.getElementById("alpha-search-input");
searchClearEl = document.getElementById("alpha-search-clear");
typeFilterEl = document.getElementById("alpha-type-filter");
gematriaCipherEl = document.getElementById("alpha-gematria-cipher");
gematriaInputEl = document.getElementById("alpha-gematria-input");
gematriaResultEl = document.getElementById("alpha-gematria-result");
gematriaBreakdownEl = document.getElementById("alpha-gematria-breakdown");
}
function getFallbackGematriaDb() {
return {
baseAlphabet: "abcdefghijklmnopqrstuvwxyz",
ciphers: [
{
id: "simple-ordinal",
name: "Simple Ordinal",
description: "A=1 ... Z=26",
values: Array.from({ length: 26 }, (_, index) => index + 1)
}
]
};
}
function normalizeGematriaText(value) {
return String(value || "")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase();
}
function transliterationToBaseLetters(transliteration, baseAlphabet) {
const normalized = normalizeGematriaText(transliteration);
if (!normalized) {
return "";
}
const primaryVariant = normalized.split(/[\/,;|]/)[0] || normalized;
const primaryLetters = [...primaryVariant].filter((char) => baseAlphabet.includes(char));
if (primaryLetters.length) {
return primaryLetters[0];
}
const allLetters = [...normalized].filter((char) => baseAlphabet.includes(char));
return allLetters[0] || "";
}
function addScriptCharMapEntry(map, scriptChar, mappedLetters) {
const key = String(scriptChar || "").trim();
const value = String(mappedLetters || "").trim();
if (!key || !value) {
return;
}
map.set(key, value);
}
function buildGematriaScriptMap(baseAlphabet) {
const map = new Map();
const hebrewLetters = Array.isArray(state.alphabets?.hebrew) ? state.alphabets.hebrew : [];
const greekLetters = Array.isArray(state.alphabets?.greek) ? state.alphabets.greek : [];
hebrewLetters.forEach((entry) => {
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
addScriptCharMapEntry(map, entry?.char, mapped);
});
greekLetters.forEach((entry) => {
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
addScriptCharMapEntry(map, entry?.char, mapped);
addScriptCharMapEntry(map, entry?.charLower, mapped);
addScriptCharMapEntry(map, entry?.charFinal, mapped);
});
const hebrewFinalForms = {
ך: "k",
ם: "m",
ן: "n",
ף: "p",
ץ: "t"
};
Object.entries(hebrewFinalForms).forEach(([char, mapped]) => {
if (!map.has(char) && baseAlphabet.includes(mapped)) {
addScriptCharMapEntry(map, char, mapped);
}
});
if (!map.has("ς") && baseAlphabet.includes("s")) {
addScriptCharMapEntry(map, "ς", "s");
}
return map;
}
function refreshGematriaScriptMap(baseAlphabet) {
state.gematria.scriptCharMap = buildGematriaScriptMap(baseAlphabet);
}
function sanitizeGematriaDb(db) {
const baseAlphabet = String(db?.baseAlphabet || "abcdefghijklmnopqrstuvwxyz").toLowerCase();
const ciphers = Array.isArray(db?.ciphers)
? db.ciphers
.map((cipher) => {
const id = String(cipher?.id || "").trim();
const name = String(cipher?.name || "").trim();
const values = Array.isArray(cipher?.values)
? cipher.values.map((value) => Number(value))
: [];
if (!id || !name || values.length !== baseAlphabet.length || values.some((value) => !Number.isFinite(value))) {
return null;
}
return {
id,
name,
description: String(cipher?.description || "").trim(),
values
};
})
.filter(Boolean)
: [];
if (!ciphers.length) {
return getFallbackGematriaDb();
}
return {
baseAlphabet,
ciphers
};
}
async function loadGematriaDb() {
if (state.gematria.db) {
return state.gematria.db;
}
if (state.gematria.loadingPromise) {
return state.gematria.loadingPromise;
}
state.gematria.loadingPromise = fetch("data/gematria-ciphers.json")
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to load gematria ciphers (${response.status})`);
}
return response.json();
})
.then((db) => {
state.gematria.db = sanitizeGematriaDb(db);
return state.gematria.db;
})
.catch(() => {
state.gematria.db = getFallbackGematriaDb();
return state.gematria.db;
})
.finally(() => {
state.gematria.loadingPromise = null;
});
return state.gematria.loadingPromise;
}
function getActiveGematriaCipher() {
const db = state.gematria.db || getFallbackGematriaDb();
const ciphers = Array.isArray(db.ciphers) ? db.ciphers : [];
if (!ciphers.length) {
return null;
}
const selectedId = state.gematria.activeCipherId || ciphers[0].id;
return ciphers.find((cipher) => cipher.id === selectedId) || ciphers[0];
}
function renderGematriaCipherOptions() {
if (!gematriaCipherEl) {
return;
}
const db = state.gematria.db || getFallbackGematriaDb();
const ciphers = Array.isArray(db.ciphers) ? db.ciphers : [];
gematriaCipherEl.innerHTML = "";
ciphers.forEach((cipher) => {
const option = document.createElement("option");
option.value = cipher.id;
option.textContent = cipher.name;
if (cipher.description) {
option.title = cipher.description;
}
gematriaCipherEl.appendChild(option);
});
const activeCipher = getActiveGematriaCipher();
state.gematria.activeCipherId = activeCipher?.id || "";
gematriaCipherEl.value = state.gematria.activeCipherId;
}
function computeGematria(text, cipher, baseAlphabet) {
const normalizedInput = normalizeGematriaText(text);
const scriptMap = state.gematria.scriptCharMap instanceof Map
? state.gematria.scriptCharMap
: new Map();
const letterParts = [];
let total = 0;
let count = 0;
[...normalizedInput].forEach((char) => {
const mappedLetters = baseAlphabet.includes(char)
? char
: (scriptMap.get(char) || "");
if (!mappedLetters) {
return;
}
[...mappedLetters].forEach((mappedChar) => {
const index = baseAlphabet.indexOf(mappedChar);
if (index < 0) {
return;
}
const value = Number(cipher.values[index]);
if (!Number.isFinite(value)) {
return;
}
count += 1;
total += value;
letterParts.push(`${mappedChar.toUpperCase()}(${value})`);
});
});
return {
total,
count,
breakdown: letterParts.join(" + ")
};
}
function renderGematriaResult() {
if (!gematriaResultEl || !gematriaBreakdownEl) {
return;
}
const db = state.gematria.db || getFallbackGematriaDb();
if (!(state.gematria.scriptCharMap instanceof Map) || !state.gematria.scriptCharMap.size) {
refreshGematriaScriptMap(db.baseAlphabet);
}
const cipher = getActiveGematriaCipher();
if (!cipher) {
gematriaResultEl.textContent = "Total: --";
gematriaBreakdownEl.textContent = "No ciphers available.";
return;
}
const { total, count, breakdown } = computeGematria(state.gematria.inputText, cipher, db.baseAlphabet);
gematriaResultEl.textContent = `Total: ${total}`;
if (!count) {
gematriaBreakdownEl.textContent = `Using ${cipher.name}. Enter English, Greek, or Hebrew letters to calculate.`;
return;
}
gematriaBreakdownEl.textContent = `${cipher.name} · ${count} letters · ${breakdown} = ${total}`;
}
function bindGematriaListeners() {
if (state.gematria.listenersBound || !gematriaCipherEl || !gematriaInputEl) {
return;
}
gematriaCipherEl.addEventListener("change", () => {
state.gematria.activeCipherId = String(gematriaCipherEl.value || "").trim();
renderGematriaResult();
});
gematriaInputEl.addEventListener("input", () => {
state.gematria.inputText = gematriaInputEl.value || "";
renderGematriaResult();
});
state.gematria.listenersBound = true;
}
function ensureGematriaCalculator() {
getElements();
if (!gematriaCipherEl || !gematriaInputEl || !gematriaResultEl || !gematriaBreakdownEl) {
return;
}
bindGematriaListeners();
if (gematriaInputEl.value !== state.gematria.inputText) {
gematriaInputEl.value = state.gematria.inputText;
}
void loadGematriaDb().then(() => {
refreshGematriaScriptMap((state.gematria.db || getFallbackGematriaDb()).baseAlphabet);
renderGematriaCipherOptions();
renderGematriaResult();
});
}
// ── Data helpers ─────────────────────────────────────────────────────
function getLetters() {
if (!state.alphabets) return [];
if (state.activeAlphabet === "all") {
const alphabetOrder = ["hebrew", "greek", "english", "arabic", "enochian"];
return alphabetOrder.flatMap((alphabet) => {
const rows = Array.isArray(state.alphabets?.[alphabet]) ? state.alphabets[alphabet] : [];
return rows.map((row) => ({ ...row, __alphabet: alphabet }));
});
}
return state.alphabets[state.activeAlphabet] || [];
}
function alphabetForLetter(letter) {
if (state.activeAlphabet === "all") {
return String(letter?.__alphabet || "").trim().toLowerCase();
}
return state.activeAlphabet;
}
function letterKeyByAlphabet(alphabet, letter) {
if (alphabet === "hebrew") return letter.hebrewLetterId;
if (alphabet === "greek") return letter.name;
if (alphabet === "english") return letter.letter;
if (alphabet === "arabic") return letter.name;
if (alphabet === "enochian") return letter.id;
return String(letter.index);
}
function letterKey(letter) {
// Stable unique key per alphabet + entry
const alphabet = alphabetForLetter(letter);
const key = letterKeyByAlphabet(alphabet, letter);
if (state.activeAlphabet === "all") {
return `${alphabet}:${key}`;
}
return key;
}
function displayGlyph(letter) {
const alphabet = alphabetForLetter(letter);
if (alphabet === "hebrew") return letter.char;
if (alphabet === "greek") return letter.char;
if (alphabet === "english") return letter.letter;
if (alphabet === "arabic") return letter.char;
if (alphabet === "enochian") return letter.char;
return "?";
}
function displayLabel(letter) {
const alphabet = alphabetForLetter(letter);
if (alphabet === "hebrew") return letter.name;
if (alphabet === "greek") return letter.displayName;
if (alphabet === "english") return letter.letter;
if (alphabet === "arabic") return arabicDisplayName(letter);
if (alphabet === "enochian") return letter.title;
return "?";
}
function displaySub(letter) {
const alphabet = alphabetForLetter(letter);
if (alphabet === "hebrew") return `${letter.transliteration} · ${letter.letterType} · ${letter.numerology}`;
if (alphabet === "greek") return `${letter.transliteration} · isopsephy ${letter.numerology}${letter.archaic ? " · archaic" : ""}`;
if (alphabet === "english") return `Pythagorean ${letter.pythagorean}`;
if (alphabet === "arabic") return `${letter.transliteration} · abjad ${letter.abjad} · ${letter.nameArabic}`;
if (alphabet === "enochian") return `${letter.transliteration} · ${Array.isArray(letter.englishLetters) ? letter.englishLetters.join("/") : ""} · value ${letter.numerology}`;
return "";
}
function normalizeLetterType(value) {
const key = String(value || "").trim().toLowerCase();
if (["mother", "double", "simple"].includes(key)) {
return key;
}
return "";
}
function getHebrewLetterTypeMap() {
const map = new Map();
const hebrewLetters = Array.isArray(state.alphabets?.hebrew) ? state.alphabets.hebrew : [];
hebrewLetters.forEach((entry) => {
const hebrewId = normalizeId(entry?.hebrewLetterId);
const letterType = normalizeLetterType(entry?.letterType);
if (hebrewId && letterType) {
map.set(hebrewId, letterType);
}
});
return map;
}
function resolveLetterType(letter) {
const direct = normalizeLetterType(letter?.letterType);
if (direct) {
return direct;
}
const hebrewId = normalizeId(letter?.hebrewLetterId);
if (!hebrewId) {
return "";
}
return getHebrewLetterTypeMap().get(hebrewId) || "";
}
function buildLetterSearchText(letter) {
const chunks = [];
chunks.push(String(displayLabel(letter) || ""));
chunks.push(String(displayGlyph(letter) || ""));
chunks.push(String(displaySub(letter) || ""));
chunks.push(String(letter?.transliteration || ""));
chunks.push(String(letter?.meaning || ""));
chunks.push(String(letter?.nameArabic || ""));
chunks.push(String(letter?.title || ""));
chunks.push(String(letter?.letter || ""));
chunks.push(String(letter?.displayName || ""));
chunks.push(String(letter?.name || ""));
chunks.push(String(letter?.index || ""));
chunks.push(String(resolveLetterType(letter) || ""));
return chunks
.join(" ")
.toLowerCase();
}
function getFilteredLetters() {
const letters = getLetters();
const query = String(state.filters.query || "").trim().toLowerCase();
const letterTypeFilter = normalizeLetterType(state.filters.letterType);
const numericPosition = /^\d+$/.test(query) ? Number(query) : null;
return letters.filter((letter) => {
if (letterTypeFilter) {
const entryType = resolveLetterType(letter);
if (entryType !== letterTypeFilter) {
return false;
}
}
if (!query) {
return true;
}
const index = Number(letter?.index);
if (Number.isFinite(numericPosition) && Number.isFinite(index) && index === numericPosition) {
return true;
}
return buildLetterSearchText(letter).includes(query);
});
}
function syncFilterControls() {
if (searchInputEl && searchInputEl.value !== state.filters.query) {
searchInputEl.value = state.filters.query;
}
if (typeFilterEl && typeFilterEl.value !== state.filters.letterType) {
typeFilterEl.value = state.filters.letterType;
}
if (searchClearEl) {
const hasFilter = Boolean(String(state.filters.query || "").trim()) || Boolean(state.filters.letterType);
searchClearEl.disabled = !hasFilter;
}
}
function applyFiltersAndRender() {
const filteredLetters = getFilteredLetters();
const selectedInFiltered = filteredLetters.some((letter) => letterKey(letter) === state.selectedKey);
if (!selectedInFiltered) {
state.selectedKey = filteredLetters[0] ? letterKey(filteredLetters[0]) : null;
}
renderList();
const selected = filteredLetters.find((letter) => letterKey(letter) === state.selectedKey)
|| filteredLetters[0]
|| null;
if (selected) {
renderDetail(selected);
return;
}
resetDetail();
if (detailSubEl) {
detailSubEl.textContent = "No letters match the current filter.";
}
}
function bindFilterControls() {
if (searchInputEl) {
searchInputEl.addEventListener("input", () => {
state.filters.query = String(searchInputEl.value || "");
syncFilterControls();
applyFiltersAndRender();
});
}
if (typeFilterEl) {
typeFilterEl.addEventListener("change", () => {
state.filters.letterType = normalizeLetterType(typeFilterEl.value);
syncFilterControls();
applyFiltersAndRender();
});
}
if (searchClearEl) {
searchClearEl.addEventListener("click", () => {
state.filters.query = "";
state.filters.letterType = "";
syncFilterControls();
applyFiltersAndRender();
});
}
}
function enochianGlyphKey(letter) {
return String(letter?.id || letter?.char || "").trim().toUpperCase();
}
function enochianGlyphCode(letter) {
const key = enochianGlyphKey(letter);
return key ? key.codePointAt(0) || 0 : 0;
}
function enochianGlyphUrl(letter) {
const code = enochianGlyphCode(letter);
return code ? `asset/img/enochian/char(${code}).png` : "";
}
function enochianGlyphImageHtml(letter, className) {
const src = enochianGlyphUrl(letter);
const key = enochianGlyphKey(letter) || "?";
if (!src) {
return `<span class="${className}">${key}</span>`;
}
return `<img class="${className}" src="${src}" alt="Enochian ${key}" loading="lazy" decoding="async">`;
}
// ── List rendering ────────────────────────────────────────────────────
function renderList() {
if (!listEl) return;
const allLetters = getLetters();
const letters = getFilteredLetters();
if (countEl) {
countEl.textContent = letters.length === allLetters.length
? `${letters.length}`
: `${letters.length}/${allLetters.length}`;
}
listEl.innerHTML = "";
letters.forEach((letter) => {
const key = letterKey(letter);
const item = document.createElement("div");
item.className = "planet-list-item alpha-list-item" + (key === state.selectedKey ? " is-selected" : "");
item.setAttribute("role", "option");
item.setAttribute("aria-selected", key === state.selectedKey ? "true" : "false");
item.dataset.key = key;
const glyph = document.createElement("span");
const alphabet = alphabetForLetter(letter);
const glyphVariantClass = alphabet === "arabic"
? " alpha-list-glyph--arabic"
: alphabet === "enochian"
? " alpha-list-glyph--enochian"
: "";
glyph.className = "alpha-list-glyph" + glyphVariantClass;
if (alphabet === "enochian") {
const image = document.createElement("img");
image.className = "alpha-enochian-glyph-img alpha-enochian-glyph-img--list";
image.src = enochianGlyphUrl(letter);
image.alt = `Enochian ${enochianGlyphKey(letter) || "?"}`;
image.loading = "lazy";
image.decoding = "async";
image.addEventListener("error", () => {
glyph.textContent = enochianGlyphKey(letter) || "?";
});
glyph.appendChild(image);
} else {
glyph.textContent = displayGlyph(letter);
}
const meta = document.createElement("span");
meta.className = "alpha-list-meta";
const alphaLabel = alphabet ? `${cap(alphabet)} · ` : "";
meta.innerHTML = `<strong>${alphaLabel}${displayLabel(letter)}</strong><br><span class="alpha-list-sub">${displaySub(letter)}</span>`;
item.appendChild(glyph);
item.appendChild(meta);
item.addEventListener("click", () => selectLetter(key));
listEl.appendChild(item);
});
}
// ── Detail rendering ──────────────────────────────────────────────────
function renderDetail(letter) {
if (!detailNameEl) return;
const alphabet = alphabetForLetter(letter);
detailNameEl.replaceChildren();
if (alphabet === "enochian") {
const image = document.createElement("img");
image.className = "alpha-enochian-glyph-img alpha-enochian-glyph-img--detail";
image.src = enochianGlyphUrl(letter);
image.alt = `Enochian ${enochianGlyphKey(letter) || "?"}`;
image.loading = "lazy";
image.decoding = "async";
image.addEventListener("error", () => {
detailNameEl.textContent = enochianGlyphKey(letter) || "?";
});
detailNameEl.appendChild(image);
} else {
detailNameEl.textContent = displayGlyph(letter);
}
detailNameEl.classList.add("alpha-detail-glyph");
detailNameEl.classList.toggle("alpha-detail-glyph--arabic", alphabet === "arabic");
detailNameEl.classList.toggle("alpha-detail-glyph--enochian", alphabet === "enochian");
if (alphabet === "hebrew") renderHebrewDetail(letter);
else if (alphabet === "greek") renderGreekDetail(letter);
else if (alphabet === "english") renderEnglishDetail(letter);
else if (alphabet === "arabic") renderArabicDetail(letter);
else if (alphabet === "enochian") renderEnochianDetail(letter);
}
function card(title, bodyHTML) {
return `<div class="planet-meta-card"><strong>${title}</strong><div class="planet-text">${bodyHTML}</div></div>`;
}
const PLANET_SYMBOLS = {
mercury: "☿︎", luna: "☾︎", venus: "♀︎", sol: "☉︎",
jupiter: "♃︎", mars: "♂︎", saturn: "♄︎"
};
const ZODIAC_SYMBOLS = {
aries: "♈︎", taurus: "♉︎", gemini: "♊︎", cancer: "♋︎",
leo: "♌︎", virgo: "♍︎", libra: "♎︎", scorpio: "♏︎",
sagittarius: "♐︎", capricorn: "♑︎", aquarius: "♒︎", pisces: "♓︎"
};
const HEBREW_DOUBLE_DUALITY = {
bet: { left: "Life", right: "Death" },
gimel: { left: "Peace", right: "War" },
dalet: { left: "Wisdom", right: "Folly" },
kaf: { left: "Wealth", right: "Poverty" },
pe: { left: "Beauty", right: "Ugliness" },
resh: { left: "Fruitfulness", right: "Sterility" },
tav: { left: "Dominion", right: "Slavery" }
};
function normalizeId(value) {
return String(value || "").trim().toLowerCase();
}
function normalizeSoulId(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[^a-z]/g, "");
}
function buildFourWorldLayersFromDataset(magickDataset) {
const worlds = magickDataset?.grouped?.kabbalah?.fourWorlds;
const souls = magickDataset?.grouped?.kabbalah?.souls;
const paths = Array.isArray(magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]?.paths)
? magickDataset.grouped.kabbalah["kabbalah-tree"].paths
: [];
if (!worlds || typeof worlds !== "object") {
return [];
}
const soulAliases = {
chiah: "chaya",
chaya: "chaya",
neshamah: "neshama",
neshama: "neshama",
ruach: "ruach",
nephesh: "nephesh"
};
const pathByLetterId = new Map();
paths.forEach((path) => {
const letterId = normalizeLetterId(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char);
const pathNo = Number(path?.pathNumber);
if (!letterId || !Number.isFinite(pathNo) || pathByLetterId.has(letterId)) {
return;
}
pathByLetterId.set(letterId, pathNo);
});
const worldOrder = ["atzilut", "briah", "yetzirah", "assiah"];
return worldOrder
.map((worldId) => {
const world = worlds?.[worldId];
if (!world || typeof world !== "object") {
return null;
}
const tetragrammaton = world?.tetragrammaton && typeof world.tetragrammaton === "object"
? world.tetragrammaton
: {};
const letterId = normalizeLetterId(tetragrammaton?.hebrewLetterId);
const rawSoulId = normalizeSoulId(world?.soulId);
const soulId = soulAliases[rawSoulId] || rawSoulId;
const soul = souls?.[soulId] && typeof souls[soulId] === "object"
? souls[soulId]
: null;
const slot = tetragrammaton?.isFinal
? `${String(tetragrammaton?.slot || "Heh")} (final)`
: String(tetragrammaton?.slot || "");
return {
slot,
letterChar: String(tetragrammaton?.letterChar || ""),
hebrewLetterId: letterId,
world: String(world?.name?.roman || titleCase(worldId)),
worldLayer: String(world?.worldLayer?.en || world?.desc?.en || ""),
worldDescription: String(world?.worldDescription?.en || ""),
soulLayer: String(soul?.name?.roman || titleCase(rawSoulId || soulId)),
soulTitle: String(soul?.title?.en || titleCase(soul?.name?.en || "")),
soulDescription: String(soul?.desc?.en || ""),
pathNumber: pathByLetterId.get(letterId) || null
};
})
.filter(Boolean);
}
function buildMonthReferencesByHebrew(referenceData, alphabets) {
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]));
const hebrewLetters = Array.isArray(alphabets?.hebrew) ? alphabets.hebrew : [];
const profiles = hebrewLetters
.filter((letter) => letter?.hebrewLetterId)
.map((letter) => {
const astrologyType = normalizeId(letter?.astrology?.type);
const astrologyName = normalizeId(letter?.astrology?.name);
return {
hebrewLetterId: normalizeId(letter.hebrewLetterId),
tarotTrumpNumber: Number.isFinite(Number(letter?.tarot?.trumpNumber))
? Number(letter.tarot.trumpNumber)
: null,
kabbalahPathNumber: Number.isFinite(Number(letter?.kabbalahPathNumber))
? Number(letter.kabbalahPathNumber)
: null,
planetId: astrologyType === "planet" ? astrologyName : "",
zodiacSignId: astrologyType === "zodiac" ? astrologyName : ""
};
});
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(hebrewLetterId, month, options = {}) {
if (!hebrewLetterId || !month?.id) {
return;
}
if (!map.has(hebrewLetterId)) {
map.set(hebrewLetterId, []);
}
const rows = map.get(hebrewLetterId);
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 = {}) {
if (!associations || typeof associations !== "object") {
return;
}
const assocHebrewId = normalizeId(associations.hebrewLetterId);
const assocTarotTrump = Number.isFinite(Number(associations.tarotTrumpNumber))
? Number(associations.tarotTrumpNumber)
: null;
const assocPath = Number.isFinite(Number(associations.kabbalahPathNumber))
? Number(associations.kabbalahPathNumber)
: null;
const assocPlanetId = normalizeId(associations.planetId);
const assocSignId = normalizeId(associations.zodiacSignId);
profiles.forEach((profile) => {
if (!profile.hebrewLetterId) {
return;
}
const matchesDirect = assocHebrewId && assocHebrewId === profile.hebrewLetterId;
const matchesTarot = assocTarotTrump != null && profile.tarotTrumpNumber === assocTarotTrump;
const matchesPath = assocPath != null && profile.kabbalahPathNumber === assocPath;
const matchesPlanet = profile.planetId && assocPlanetId && profile.planetId === assocPlanetId;
const matchesZodiac = profile.zodiacSignId && assocSignId && profile.zodiacSignId === assocSignId;
if (matchesDirect || matchesTarot || matchesPath || matchesPlanet || matchesZodiac) {
pushRef(profile.hebrewLetterId, 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 createEmptyCubeRefs() {
return {
hebrewPlacementById: new Map(),
signPlacementById: new Map(),
planetPlacementById: new Map(),
pathPlacementByNo: new Map()
};
}
function normalizeLetterId(value) {
const key = normalizeId(value).replace(/[^a-z]/g, "");
const aliases = {
aleph: "alef",
beth: "bet",
zain: "zayin",
cheth: "het",
chet: "het",
daleth: "dalet",
teth: "tet",
peh: "pe",
tzaddi: "tsadi",
tzadi: "tsadi",
tzade: "tsadi",
tsaddi: "tsadi",
qoph: "qof",
taw: "tav",
tau: "tav"
};
return aliases[key] || key;
}
function edgeWalls(edge) {
const explicitWalls = Array.isArray(edge?.walls)
? edge.walls.map((wallId) => normalizeId(wallId)).filter(Boolean)
: [];
if (explicitWalls.length >= 2) {
return explicitWalls.slice(0, 2);
}
return normalizeId(edge?.id)
.split("-")
.map((wallId) => normalizeId(wallId))
.filter(Boolean)
.slice(0, 2);
}
function edgeLabel(edge) {
const explicitName = String(edge?.name || "").trim();
if (explicitName) {
return explicitName;
}
return edgeWalls(edge)
.map((part) => cap(part))
.join(" ");
}
function resolveCubeDirectionLabel(wallId, edge) {
const normalizedWallId = normalizeId(wallId);
const edgeId = normalizeId(edge?.id);
if (!normalizedWallId || !edgeId) {
return "";
}
const cubeUi = window.CubeSectionUi;
if (cubeUi && typeof cubeUi.getEdgeDirectionLabelForWall === "function") {
const directionLabel = String(cubeUi.getEdgeDirectionLabelForWall(normalizedWallId, edgeId) || "").trim();
if (directionLabel) {
return directionLabel;
}
}
return edgeLabel(edge);
}
function makeCubePlacement(wall, edge = null) {
const wallId = normalizeId(wall?.id);
const edgeId = normalizeId(edge?.id);
return {
wallId,
edgeId,
wallName: wall?.name || cap(wallId),
edgeName: resolveCubeDirectionLabel(wallId, edge)
};
}
function setPlacementIfMissing(map, key, placement) {
if (!key || map.has(key) || !placement?.wallId) {
return;
}
map.set(key, placement);
}
function buildCubeReferences(magickDataset) {
const refs = createEmptyCubeRefs();
const cube = magickDataset?.grouped?.kabbalah?.cube || {};
const walls = Array.isArray(cube?.walls)
? cube.walls
: [];
const edges = Array.isArray(cube?.edges)
? cube.edges
: [];
const paths = Array.isArray(magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]?.paths)
? magickDataset.grouped.kabbalah["kabbalah-tree"].paths
: [];
const wallById = new Map(
walls.map((wall) => [normalizeId(wall?.id), wall])
);
const firstEdgeByWallId = new Map();
const pathByLetterId = new Map(
paths
.map((path) => [normalizeLetterId(path?.hebrewLetter?.transliteration), path])
.filter(([letterId]) => Boolean(letterId))
);
edges.forEach((edge) => {
edgeWalls(edge).forEach((wallId) => {
if (!firstEdgeByWallId.has(wallId)) {
firstEdgeByWallId.set(wallId, edge);
}
});
});
walls.forEach((wall) => {
// each wall has a "face" letter; when we build a cube reference for that
// letter we want the label to read “Face” rather than arbitrarily using
// the first edge we encounter on that wall. previously we always
// computed `placementEdge` from the first edge, which produced labels
// like “East Wall  North” for the dalet face. instead we create a
// custom placement object for face letters with an empty edge id and a
// fixed edgeName of “Face”.
const wallHebrewLetterId = normalizeLetterId(wall?.hebrewLetterId || wall?.associations?.hebrewLetterId);
let wallPlacement;
if (wallHebrewLetterId) {
// face letter; label should emphasise the face rather than a direction
wallPlacement = {
wallId: normalizeId(wall?.id),
edgeId: "",
wallName: wall?.name || cap(normalizeId(wall?.id)),
edgeName: "Face"
};
} else {
// fall back to normal edge-based placement
const placementEdge = firstEdgeByWallId.get(normalizeId(wall?.id)) || null;
wallPlacement = makeCubePlacement(wall, placementEdge);
}
setPlacementIfMissing(refs.hebrewPlacementById, wallHebrewLetterId, wallPlacement);
const wallPath = pathByLetterId.get(wallHebrewLetterId) || null;
const wallSignId = normalizeId(wallPath?.astrology?.type) === "zodiac"
? normalizeId(wallPath?.astrology?.name)
: "";
setPlacementIfMissing(refs.signPlacementById, wallSignId, wallPlacement);
const wallPathNo = Number(wallPath?.pathNumber);
if (Number.isFinite(wallPathNo)) {
setPlacementIfMissing(refs.pathPlacementByNo, wallPathNo, wallPlacement);
}
const wallPlanet = normalizeId(wall?.associations?.planetId);
if (wallPlanet) {
setPlacementIfMissing(refs.planetPlacementById, wallPlanet, wallPlacement);
}
});
edges.forEach((edge) => {
const wallsForEdge = edgeWalls(edge);
const primaryWallId = wallsForEdge[0];
const primaryWall = wallById.get(primaryWallId) || {
id: primaryWallId,
name: cap(primaryWallId)
};
const placement = makeCubePlacement(primaryWall, edge);
const hebrewLetterId = normalizeLetterId(edge?.hebrewLetterId || edge?.associations?.hebrewLetterId);
setPlacementIfMissing(refs.hebrewPlacementById, hebrewLetterId, placement);
const path = pathByLetterId.get(hebrewLetterId) || null;
const signId = normalizeId(path?.astrology?.type) === "zodiac"
? normalizeId(path?.astrology?.name)
: "";
setPlacementIfMissing(refs.signPlacementById, signId, placement);
const pathNo = Number(path?.pathNumber);
if (Number.isFinite(pathNo)) {
setPlacementIfMissing(refs.pathPlacementByNo, pathNo, placement);
}
});
return refs;
}
function getCubePlacementForHebrewLetter(hebrewLetterId, pathNo = null) {
const normalizedLetterId = normalizeId(hebrewLetterId);
if (normalizedLetterId && state.cubeRefs.hebrewPlacementById.has(normalizedLetterId)) {
return state.cubeRefs.hebrewPlacementById.get(normalizedLetterId);
}
const numericPath = Number(pathNo);
if (Number.isFinite(numericPath) && state.cubeRefs.pathPlacementByNo.has(numericPath)) {
return state.cubeRefs.pathPlacementByNo.get(numericPath);
}
return null;
}
function getCubePlacementForPlanet(planetId) {
const normalizedPlanetId = normalizeId(planetId);
return normalizedPlanetId ? state.cubeRefs.planetPlacementById.get(normalizedPlanetId) || null : null;
}
function getCubePlacementForSign(signId) {
const normalizedSignId = normalizeId(signId);
return normalizedSignId ? state.cubeRefs.signPlacementById.get(normalizedSignId) || null : null;
}
function cubePlacementLabel(placement) {
const wallName = placement?.wallName || "Wall";
const edgeName = placement?.edgeName || "Direction";
return `Cube: ${wallName} Wall - ${edgeName}`;
}
function cubePlacementBtn(placement, fallbackDetail = null) {
if (!placement) {
return "";
}
const detail = {
"wall-id": placement.wallId,
"edge-id": placement.edgeId
};
if (fallbackDetail && typeof fallbackDetail === "object") {
Object.entries(fallbackDetail).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== "") {
detail[key] = value;
}
});
}
return navBtn(cubePlacementLabel(placement), "nav:cube", detail);
}
function cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ""; }
function renderAstrologyCard(astrology) {
if (!astrology) return "";
const { type, name } = astrology;
const id = (name || "").toLowerCase();
if (type === "planet") {
const sym = PLANET_SYMBOLS[id] || "";
const cubePlacement = getCubePlacementForPlanet(id);
const cubeBtn = cubePlacementBtn(cubePlacement, { "planet-id": id });
return card("Astrology", `
<dl class="alpha-dl">
<dt>Type</dt><dd>Planet</dd>
<dt>Ruler</dt><dd>${sym} ${cap(id)}</dd>
</dl>
<div class="alpha-nav-btns">
<button class="alpha-nav-btn" data-event="nav:planet" data-planet-id="${id}">View ${cap(id)} ↗</button>
${cubeBtn}
</div>
`);
}
if (type === "zodiac") {
const sym = ZODIAC_SYMBOLS[id] || "";
const cubePlacement = getCubePlacementForSign(id);
const cubeBtn = cubePlacementBtn(cubePlacement, { "sign-id": id });
return card("Astrology", `
<dl class="alpha-dl">
<dt>Type</dt><dd>Zodiac Sign</dd>
<dt>Sign</dt><dd>${sym} ${cap(id)}</dd>
</dl>
<div class="alpha-nav-btns">
<button class="alpha-nav-btn" data-event="nav:zodiac" data-sign-id="${id}">View ${cap(id)} ↗</button>
${cubeBtn}
</div>
`);
}
if (type === "element") {
const elemEmoji = { air: "💨", water: "💧", fire: "🔥", earth: "🌍" };
return card("Astrology", `
<dl class="alpha-dl">
<dt>Type</dt><dd>Element</dd>
<dt>Element</dt><dd>${elemEmoji[id] || ""} ${cap(id)}</dd>
</dl>
`);
}
return card("Astrology", `
<dl class="alpha-dl">
<dt>Type</dt><dd>${cap(type)}</dd>
<dt>Name</dt><dd>${cap(name)}</dd>
</dl>
`);
}
function navBtn(label, event, detail) {
const attrs = Object.entries(detail).map(([k, v]) => `data-${k}="${v}"`).join(" ");
return `<button class="alpha-nav-btn" data-event="${event}" ${attrs}>${label} ↗</button>`;
}
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 describeDigitalRootReduction(value, digitalRoot) {
const normalized = Math.abs(Math.trunc(Number(value)));
if (!Number.isFinite(normalized) || !Number.isFinite(digitalRoot)) {
return "";
}
if (normalized < 10) {
return String(normalized);
}
return `${String(normalized).split("").join(" + ")} = ${digitalRoot}`;
}
function renderPositionDigitalRootCard(letter, alphabet, orderLabel) {
const index = Number(letter?.index);
if (!Number.isFinite(index)) {
return "";
}
const position = Math.trunc(index);
if (position <= 0) {
return "";
}
const digitalRoot = computeDigitalRoot(position);
if (!Number.isFinite(digitalRoot)) {
return "";
}
const entries = Array.isArray(state.alphabets?.[alphabet]) ? state.alphabets[alphabet] : [];
const countText = entries.length ? ` of ${entries.length}` : "";
const orderText = orderLabel ? ` (${orderLabel})` : "";
const reductionText = describeDigitalRootReduction(position, digitalRoot);
const openNumberBtn = navBtn(`View Number ${digitalRoot}`, "nav:number", { value: digitalRoot });
return card("Position Digital Root", `
<dl class="alpha-dl">
<dt>Position</dt><dd>#${position}${countText}${orderText}</dd>
<dt>Digital Root</dt><dd>${digitalRoot}${reductionText ? ` (${reductionText})` : ""}</dd>
</dl>
<div class="alpha-nav-btns">${openNumberBtn}</div>
`);
}
function monthRefsForLetter(letter) {
const hebrewLetterId = normalizeId(letter?.hebrewLetterId);
if (!hebrewLetterId) {
return [];
}
return state.monthRefsByHebrewId.get(hebrewLetterId) || [];
}
function calendarMonthsCard(monthRefs, titleLabel) {
if (!monthRefs.length) {
return "";
}
const monthButtons = monthRefs
.map((month) => navBtn(month.label || month.name, "nav:calendar-month", { "month-id": month.id }))
.join("");
return card("Calendar Months", `
<div>${titleLabel}</div>
<div class="alpha-nav-btns">${monthButtons}</div>
`);
}
function renderHebrewDualityCard(letter) {
const duality = HEBREW_DOUBLE_DUALITY[normalizeId(letter?.hebrewLetterId)];
if (!duality) {
return "";
}
return card("Duality", `
<dl class="alpha-dl">
<dt>Polarity</dt><dd>${duality.left} / ${duality.right}</dd>
</dl>
`);
}
function renderHebrewFourWorldsCard(letter) {
const letterId = normalizeLetterId(letter?.hebrewLetterId || letter?.transliteration || letter?.char);
if (!letterId) {
return "";
}
const rows = (Array.isArray(state.fourWorldLayers) ? state.fourWorldLayers : [])
.filter((entry) => entry?.hebrewLetterId === letterId);
if (!rows.length) {
return "";
}
const body = rows.map((entry) => {
const pathBtn = Number.isFinite(Number(entry?.pathNumber))
? navBtn(`View Path ${entry.pathNumber}`, "nav:kabbalah-path", { "path-no": Number(entry.pathNumber) })
: "";
return `
<div class="cal-item-row">
<div class="cal-item-head">
<span class="cal-item-name">${entry.slot}: ${entry.letterChar}${entry.world}</span>
<span class="planet-list-meta">${entry.soulLayer}</span>
</div>
<div class="planet-text">${entry.worldLayer}${entry.worldDescription ? ` · ${entry.worldDescription}` : ""}</div>
<div class="planet-text">${entry.soulLayer}${entry.soulTitle ? `${entry.soulTitle}` : ""}${entry.soulDescription ? `: ${entry.soulDescription}` : ""}</div>
<div class="alpha-nav-btns">${pathBtn}</div>
</div>
`;
}).join("");
return card("Qabalistic Worlds & Soul Layers", `<div class="cal-item-stack">${body}</div>`);
}
function normalizeLatinLetter(value) {
return String(value || "")
.trim()
.toUpperCase()
.replace(/[^A-Z]/g, "");
}
function extractEnglishLetterRefs(value) {
if (Array.isArray(value)) {
return [...new Set(value.map((entry) => normalizeLatinLetter(entry)).filter(Boolean))];
}
return [...new Set(
String(value || "")
.split(/[\s,;|\/]+/)
.map((entry) => normalizeLatinLetter(entry))
.filter(Boolean)
)];
}
function renderAlphabetEquivalentCard(activeAlphabet, letter) {
const hebrewLetters = Array.isArray(state.alphabets?.hebrew) ? state.alphabets.hebrew : [];
const greekLetters = Array.isArray(state.alphabets?.greek) ? state.alphabets.greek : [];
const englishLetters = Array.isArray(state.alphabets?.english) ? state.alphabets.english : [];
const arabicLetters = Array.isArray(state.alphabets?.arabic) ? state.alphabets.arabic : [];
const enochianLetters = Array.isArray(state.alphabets?.enochian) ? state.alphabets.enochian : [];
const linkedHebrewIds = new Set();
const linkedEnglishLetters = new Set();
const buttons = [];
function addHebrewId(value) {
const id = normalizeId(value);
if (id) {
linkedHebrewIds.add(id);
}
}
function addEnglishLetter(value) {
const code = normalizeLatinLetter(value);
if (!code) {
return;
}
linkedEnglishLetters.add(code);
englishLetters
.filter((entry) => normalizeLatinLetter(entry?.letter) === code)
.forEach((entry) => addHebrewId(entry?.hebrewLetterId));
}
if (activeAlphabet === "hebrew") {
addHebrewId(letter?.hebrewLetterId);
} else if (activeAlphabet === "greek") {
addHebrewId(letter?.hebrewLetterId);
englishLetters
.filter((entry) => normalizeId(entry?.greekEquivalent) === normalizeId(letter?.name))
.forEach((entry) => addEnglishLetter(entry?.letter));
} else if (activeAlphabet === "english") {
addEnglishLetter(letter?.letter);
addHebrewId(letter?.hebrewLetterId);
} else if (activeAlphabet === "arabic") {
addHebrewId(letter?.hebrewLetterId);
} else if (activeAlphabet === "enochian") {
extractEnglishLetterRefs(letter?.englishLetters).forEach((code) => addEnglishLetter(code));
addHebrewId(letter?.hebrewLetterId);
}
if (!linkedHebrewIds.size && !linkedEnglishLetters.size) {
return "";
}
const activeHebrewKey = normalizeId(letter?.hebrewLetterId);
const activeGreekKey = normalizeId(letter?.name);
const activeEnglishKey = normalizeLatinLetter(letter?.letter);
const activeArabicKey = normalizeId(letter?.name);
const activeEnochianKey = normalizeId(letter?.id || letter?.char || letter?.title);
hebrewLetters.forEach((heb) => {
const key = normalizeId(heb?.hebrewLetterId);
if (!key || !linkedHebrewIds.has(key)) {
return;
}
if (activeAlphabet === "hebrew" && key === activeHebrewKey) {
return;
}
buttons.push(`<button class="alpha-sister-btn" data-alpha="hebrew" data-key="${heb.hebrewLetterId}">
<span class="alpha-sister-glyph">${heb.char}</span>
<span class="alpha-sister-name">Hebrew: ${heb.name} (${heb.transliteration}) · gematria ${heb.numerology}</span>
</button>`);
});
greekLetters.forEach((grk) => {
const key = normalizeId(grk?.name);
const viaHebrew = linkedHebrewIds.has(normalizeId(grk?.hebrewLetterId));
const viaEnglish = englishLetters.some((eng) => (
linkedEnglishLetters.has(normalizeLatinLetter(eng?.letter))
&& normalizeId(eng?.greekEquivalent) === key
));
if (!(viaHebrew || viaEnglish)) {
return;
}
if (activeAlphabet === "greek" && key === activeGreekKey) {
return;
}
buttons.push(`<button class="alpha-sister-btn" data-alpha="greek" data-key="${grk.name}">
<span class="alpha-sister-glyph">${grk.char}</span>
<span class="alpha-sister-name">Greek: ${grk.displayName} (${grk.transliteration}) · isopsephy ${grk.numerology}</span>
</button>`);
});
englishLetters.forEach((eng) => {
const key = normalizeLatinLetter(eng?.letter);
const viaLetter = linkedEnglishLetters.has(key);
const viaHebrew = linkedHebrewIds.has(normalizeId(eng?.hebrewLetterId));
if (!(viaLetter || viaHebrew)) {
return;
}
if (activeAlphabet === "english" && key === activeEnglishKey) {
return;
}
buttons.push(`<button class="alpha-sister-btn" data-alpha="english" data-key="${eng.letter}">
<span class="alpha-sister-glyph">${eng.letter}</span>
<span class="alpha-sister-name">English: ${eng.letter} · pythagorean ${eng.pythagorean}</span>
</button>`);
});
arabicLetters.forEach((arb) => {
const key = normalizeId(arb?.name);
if (!linkedHebrewIds.has(normalizeId(arb?.hebrewLetterId))) {
return;
}
if (activeAlphabet === "arabic" && key === activeArabicKey) {
return;
}
buttons.push(`<button class="alpha-sister-btn" data-alpha="arabic" data-key="${arb.name}">
<span class="alpha-sister-glyph alpha-list-glyph--arabic">${arb.char}</span>
<span class="alpha-sister-name">Arabic: ${arabicDisplayName(arb)}${arb.nameArabic} (${arb.transliteration}) · abjad ${arb.abjad}</span>
</button>`);
});
enochianLetters.forEach((eno) => {
const key = normalizeId(eno?.id || eno?.char || eno?.title);
const englishRefs = extractEnglishLetterRefs(eno?.englishLetters);
const viaHebrew = linkedHebrewIds.has(normalizeId(eno?.hebrewLetterId));
const viaEnglish = englishRefs.some((code) => linkedEnglishLetters.has(code));
if (!(viaHebrew || viaEnglish)) {
return;
}
if (activeAlphabet === "enochian" && key === activeEnochianKey) {
return;
}
buttons.push(`<button class="alpha-sister-btn" data-alpha="enochian" data-key="${eno.id}">
${enochianGlyphImageHtml(eno, "alpha-enochian-glyph-img alpha-enochian-glyph-img--sister")}
<span class="alpha-sister-name">Enochian: ${eno.title} (${eno.transliteration}) · English ${englishRefs.join("/") || "—"}</span>
</button>`);
});
if (!buttons.length) {
return "";
}
return card("ALPHABET EQUIVALENT", `<div class="alpha-sister-wrap">${buttons.join("")}</div>`);
}
function renderHebrewDetail(letter) {
detailSubEl.textContent = `${letter.name}${letter.transliteration}`;
detailBodyEl.innerHTML = "";
const sections = [];
// Basics
sections.push(card("Letter Details", `
<dl class="alpha-dl">
<dt>Character</dt><dd>${letter.char}</dd>
<dt>Name</dt><dd>${letter.name}</dd>
<dt>Transliteration</dt><dd>${letter.transliteration}</dd>
<dt>Meaning</dt><dd>${letter.meaning}</dd>
<dt>Gematria Value</dt><dd>${letter.numerology}</dd>
<dt>Letter Type</dt><dd class="alpha-badge alpha-badge--${letter.letterType}">${letter.letterType}</dd>
<dt>Position</dt><dd>#${letter.index} of 22</dd>
</dl>
`));
const positionRootCard = renderPositionDigitalRootCard(letter, "hebrew");
if (positionRootCard) {
sections.push(positionRootCard);
}
if (letter.letterType === "double") {
const dualityCard = renderHebrewDualityCard(letter);
if (dualityCard) {
sections.push(dualityCard);
}
}
const fourWorldsCard = renderHebrewFourWorldsCard(letter);
if (fourWorldsCard) {
sections.push(fourWorldsCard);
}
// Astrology
if (letter.astrology) {
sections.push(renderAstrologyCard(letter.astrology));
}
// Kabbalah Path + Tarot
if (letter.kabbalahPathNumber) {
const tarotPart = letter.tarot
? `<dt>Tarot Card</dt><dd>${letter.tarot.card} (Trump ${letter.tarot.trumpNumber})</dd>`
: "";
const kabBtn = navBtn("View Kabbalah Path", "tarot:view-kab-path", { "path-number": letter.kabbalahPathNumber });
const tarotBtn = letter.tarot
? navBtn("View Tarot Card", "kab:view-trump", { "trump-number": letter.tarot.trumpNumber })
: "";
const cubePlacement = getCubePlacementForHebrewLetter(letter.hebrewLetterId, letter.kabbalahPathNumber);
const cubeBtn = cubePlacementBtn(cubePlacement, {
"hebrew-letter-id": letter.hebrewLetterId,
"path-no": letter.kabbalahPathNumber
});
sections.push(card("Kabbalah & Tarot", `
<dl class="alpha-dl">
<dt>Path Number</dt><dd>${letter.kabbalahPathNumber}</dd>
${tarotPart}
</dl>
<div class="alpha-nav-btns">${kabBtn}${tarotBtn}${cubeBtn}</div>
`));
}
const monthRefs = monthRefsForLetter(letter);
const monthCard = calendarMonthsCard(monthRefs, `Calendar correspondences linked to ${letter.name}.`);
if (monthCard) {
sections.push(monthCard);
}
const equivalentsCard = renderAlphabetEquivalentCard("hebrew", letter);
if (equivalentsCard) {
sections.push(equivalentsCard);
}
detailBodyEl.innerHTML = sections.join("");
attachDetailListeners();
}
function renderGreekDetail(letter) {
const archaicBadge = letter.archaic ? ' <span class="alpha-badge alpha-badge--archaic">archaic</span>' : "";
detailSubEl.textContent = `${letter.displayName}${letter.archaic ? " (archaic)" : ""}${letter.transliteration}`;
detailBodyEl.innerHTML = "";
const sections = [];
const charRow = letter.charFinal
? `<dt>Form (final)</dt><dd>${letter.charFinal}</dd>`
: "";
sections.push(card("Letter Details", `
<dl class="alpha-dl">
<dt>Uppercase</dt><dd>${letter.char}</dd>
<dt>Lowercase</dt><dd>${letter.charLower || "—"}</dd>
${charRow}
<dt>Name</dt><dd>${letter.displayName}${archaicBadge}</dd>
<dt>Transliteration</dt><dd>${letter.transliteration}</dd>
<dt>IPA</dt><dd>${letter.ipa || "—"}</dd>
<dt>Isopsephy Value</dt><dd>${letter.numerology}</dd>
<dt>Meaning / Origin</dt><dd>${letter.meaning || "—"}</dd>
</dl>
`));
const positionRootCard = renderPositionDigitalRootCard(letter, "greek");
if (positionRootCard) {
sections.push(positionRootCard);
}
const equivalentsCard = renderAlphabetEquivalentCard("greek", letter);
if (equivalentsCard) {
sections.push(equivalentsCard);
}
const monthRefs = monthRefsForLetter(letter);
const monthCard = calendarMonthsCard(monthRefs, `Calendar correspondences inherited via ${letter.displayName}'s Hebrew origin.`);
if (monthCard) {
sections.push(monthCard);
}
detailBodyEl.innerHTML = sections.join("");
attachDetailListeners();
}
function renderEnglishDetail(letter) {
detailSubEl.textContent = `Letter ${letter.letter} · position #${letter.index}`;
detailBodyEl.innerHTML = "";
const sections = [];
sections.push(card("Letter Details", `
<dl class="alpha-dl">
<dt>Letter</dt><dd>${letter.letter}</dd>
<dt>Position</dt><dd>#${letter.index} of 26</dd>
<dt>IPA</dt><dd>${letter.ipa || "—"}</dd>
<dt>Pythagorean Value</dt><dd>${letter.pythagorean}</dd>
</dl>
`));
const positionRootCard = renderPositionDigitalRootCard(letter, "english");
if (positionRootCard) {
sections.push(positionRootCard);
}
const equivalentsCard = renderAlphabetEquivalentCard("english", letter);
if (equivalentsCard) {
sections.push(equivalentsCard);
}
const monthRefs = monthRefsForLetter(letter);
const monthCard = calendarMonthsCard(monthRefs, `Calendar correspondences linked through this letter's Hebrew correspondence.`);
if (monthCard) {
sections.push(monthCard);
}
detailBodyEl.innerHTML = sections.join("");
attachDetailListeners();
}
function renderArabicDetail(letter) {
detailSubEl.textContent = `${arabicDisplayName(letter)}${letter.transliteration}`;
detailBodyEl.innerHTML = "";
const sections = [];
// Letter forms row
const f = letter.forms || {};
const formParts = [
f.isolated ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${f.isolated}</span><br>isolated</span>` : "",
f.final ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${f.final}</span><br>final</span>` : "",
f.medial ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${f.medial}</span><br>medial</span>` : "",
f.initial ? `<span class="alpha-arabic-form"><span class="alpha-arabic-glyph">${f.initial}</span><br>initial</span>` : ""
].filter(Boolean);
sections.push(card("Letter Details", `
<dl class="alpha-dl">
<dt>Arabic Name</dt><dd class="alpha-arabic-inline">${letter.nameArabic}</dd>
<dt>Transliteration</dt><dd>${letter.transliteration}</dd>
<dt>IPA</dt><dd>${letter.ipa || "—"}</dd>
<dt>Abjad Value</dt><dd>${letter.abjad}</dd>
<dt>Meaning</dt><dd>${letter.meaning || "—"}</dd>
<dt>Category</dt><dd class="alpha-badge alpha-badge--${letter.category}">${letter.category}</dd>
<dt>Position</dt><dd>#${letter.index} of 28 (Abjad order)</dd>
</dl>
`));
const positionRootCard = renderPositionDigitalRootCard(letter, "arabic", "Abjad order");
if (positionRootCard) {
sections.push(positionRootCard);
}
if (formParts.length) {
sections.push(card("Letter Forms", `<div class="alpha-arabic-forms">${formParts.join("")}</div>`));
}
const equivalentsCard = renderAlphabetEquivalentCard("arabic", letter);
if (equivalentsCard) {
sections.push(equivalentsCard);
}
detailBodyEl.innerHTML = sections.join("");
attachDetailListeners();
}
function renderEnochianDetail(letter) {
const englishRefs = extractEnglishLetterRefs(letter?.englishLetters);
detailSubEl.textContent = `${letter.title}${letter.transliteration}`;
detailBodyEl.innerHTML = "";
const sections = [];
sections.push(card("Letter Details", `
<dl class="alpha-dl">
<dt>Character</dt><dd>${enochianGlyphImageHtml(letter, "alpha-enochian-glyph-img alpha-enochian-glyph-img--detail-row")}</dd>
<dt>Name</dt><dd>${letter.title}</dd>
<dt>English Letters</dt><dd>${englishRefs.join(" / ") || "—"}</dd>
<dt>Transliteration</dt><dd>${letter.transliteration || "—"}</dd>
<dt>Element / Planet</dt><dd>${letter.elementOrPlanet || "—"}</dd>
<dt>Tarot</dt><dd>${letter.tarot || "—"}</dd>
<dt>Numerology</dt><dd>${letter.numerology || "—"}</dd>
<dt>Glyph Source</dt><dd>Local cache: asset/img/enochian (sourced from dCode set)</dd>
<dt>Position</dt><dd>#${letter.index} of 21</dd>
</dl>
`));
const positionRootCard = renderPositionDigitalRootCard(letter, "enochian");
if (positionRootCard) {
sections.push(positionRootCard);
}
const equivalentsCard = renderAlphabetEquivalentCard("enochian", letter);
if (equivalentsCard) {
sections.push(equivalentsCard);
}
const monthRefs = monthRefsForLetter(letter);
const monthCard = calendarMonthsCard(monthRefs, `Calendar correspondences linked through this letter's Hebrew correspondence.`);
if (monthCard) {
sections.push(monthCard);
}
detailBodyEl.innerHTML = sections.join("");
attachDetailListeners();
}
// ── Event delegation on detail body ──────────────────────────────────
function attachDetailListeners() {
if (!detailBodyEl) return;
// Nav buttons — generic: forward all data-* (except data-event) as the event detail
detailBodyEl.querySelectorAll(".alpha-nav-btn[data-event]").forEach((btn) => {
btn.addEventListener("click", () => {
const evtName = btn.dataset.event;
const detail = {};
Object.entries(btn.dataset).forEach(([key, val]) => {
if (key === "event") return;
// Convert kebab data keys to camelCase (e.g. planet-id → planetId)
const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
detail[camel] = isNaN(Number(val)) || val === "" ? val : Number(val);
});
document.dispatchEvent(new CustomEvent(evtName, { detail }));
});
});
// Sister letter cross-navigation within this section
detailBodyEl.querySelectorAll(".alpha-sister-btn[data-alpha]").forEach((btn) => {
btn.addEventListener("click", () => {
const alpha = btn.dataset.alpha;
const key = btn.dataset.key;
switchAlphabet(alpha, key);
});
});
}
// ── Selection ─────────────────────────────────────────────────────────
function selectLetter(key) {
state.selectedKey = key;
renderList();
const letters = getFilteredLetters();
const letter = letters.find((l) => letterKey(l) === key) || getLetters().find((l) => letterKey(l) === key);
if (letter) renderDetail(letter);
}
// ── Alphabet switching ────────────────────────────────────────────────
function switchAlphabet(alpha, selectKey) {
state.activeAlphabet = alpha;
state.selectedKey = selectKey || null;
updateTabs();
syncFilterControls();
renderList();
if (selectKey) {
const letters = getFilteredLetters();
const letter = letters.find((l) => letterKey(l) === selectKey) || getLetters().find((l) => letterKey(l) === selectKey);
if (letter) renderDetail(letter);
} else {
resetDetail();
}
}
function updateTabs() {
[tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian].forEach((btn) => {
if (!btn) return;
btn.classList.toggle("alpha-tab-active", btn.dataset.alpha === state.activeAlphabet);
});
}
function resetDetail() {
if (detailNameEl) {
detailNameEl.textContent = "--";
detailNameEl.classList.remove("alpha-detail-glyph");
}
if (detailSubEl) detailSubEl.textContent = "Select a letter to explore";
if (detailBodyEl) detailBodyEl.innerHTML = "";
}
// ── Public init ───────────────────────────────────────────────────────
function ensureAlphabetSection(magickDataset, referenceData = null) {
const grouped = magickDataset?.grouped || {};
const alphabetData = (grouped["alphabets"] && grouped["alphabets"]["hebrew"])
? grouped["alphabets"]
: null;
if (alphabetData) {
state.alphabets = alphabetData;
if (state.gematria.db?.baseAlphabet) {
refreshGematriaScriptMap(state.gematria.db.baseAlphabet);
}
}
state.fourWorldLayers = buildFourWorldLayersFromDataset(magickDataset);
state.cubeRefs = buildCubeReferences(magickDataset);
if (referenceData && state.alphabets) {
state.monthRefsByHebrewId = buildMonthReferencesByHebrew(referenceData, state.alphabets);
}
if (state.initialized) {
ensureGematriaCalculator();
syncFilterControls();
renderList();
const letters = getFilteredLetters();
const selected = letters.find((entry) => letterKey(entry) === state.selectedKey) || letters[0];
if (selected) {
renderDetail(selected);
} else {
resetDetail();
if (detailSubEl) {
detailSubEl.textContent = "No letters match the current filter.";
}
}
return;
}
state.initialized = true;
// alphabets.json is a top-level file → grouped["alphabets"] = the data object
getElements();
ensureGematriaCalculator();
bindFilterControls();
syncFilterControls();
if (!state.alphabets) {
if (detailSubEl) detailSubEl.textContent = "Alphabet data not loaded.";
return;
}
// Attach tab listeners
[tabAll, tabHebrew, tabGreek, tabEnglish, tabArabic, tabEnochian].forEach((btn) => {
if (!btn) return;
btn.addEventListener("click", () => {
switchAlphabet(btn.dataset.alpha, null);
});
});
switchAlphabet("all", null);
}
// ── Incoming navigation ───────────────────────────────────────────────
function selectLetterByHebrewId(hebrewLetterId) {
switchAlphabet("hebrew", hebrewLetterId);
}
function selectGreekLetterByName(name) {
switchAlphabet("greek", name);
}
function selectEnglishLetter(letter) {
switchAlphabet("english", letter);
}
function selectArabicLetter(name) {
switchAlphabet("arabic", name);
}
function selectEnochianLetter(id) {
switchAlphabet("enochian", id);
}
window.AlphabetSectionUi = {
ensureAlphabetSection,
selectLetterByHebrewId,
selectGreekLetterByName,
selectEnglishLetter,
selectArabicLetter,
selectEnochianLetter
};
})();