(function () { "use strict"; let config = { getAlphabets: () => null, getGematriaDb: () => null, getGematriaElements: () => ({ cipherEl: null, inputEl: null, resultEl: null, breakdownEl: null, modeEls: [], matchesEl: null, inputLabelEl: null, cipherLabelEl: null }) }; const state = { loadingPromise: null, db: null, listenersBound: false, activeCipherId: "", forwardInputText: "", reverseInputText: "", anagramInputText: "", activeMode: "forward", scriptCharMap: new Map(), reverseLookupCache: new Map(), anagramLookupCache: new Map(), reverseRequestId: 0, anagramRequestId: 0 }; function getAlphabets() { return config.getAlphabets?.() || null; } function getConfiguredGematriaDb() { return config.getGematriaDb?.() || null; } function getElements() { return config.getGematriaElements?.() || { cipherEl: null, inputEl: null, resultEl: null, breakdownEl: null, modeEls: [], matchesEl: null, inputLabelEl: null, cipherLabelEl: null }; } function isReverseMode() { return state.activeMode === "reverse"; } function isAnagramMode() { return state.activeMode === "anagram"; } function getCurrentInputText() { if (isReverseMode()) { return state.reverseInputText; } if (isAnagramMode()) { return state.anagramInputText; } return state.forwardInputText; } function formatCount(value) { const numericValue = Number(value); if (!Number.isFinite(numericValue)) { return "0"; } return numericValue.toLocaleString(); } 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) }, { id: "decadic-cipher", name: "Decadic Cipher", description: "A=1 ... I=9, J=10 ... R=90, S=100 ... Z=800", values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800] } ] }; } 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 alphabets = getAlphabets() || {}; const hebrewLetters = Array.isArray(alphabets.hebrew) ? alphabets.hebrew : []; const greekLetters = Array.isArray(alphabets.greek) ? 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 refreshScriptMap(baseAlphabetOverride = "") { const db = state.db || getFallbackGematriaDb(); const baseAlphabet = String(baseAlphabetOverride || db.baseAlphabet || "abcdefghijklmnopqrstuvwxyz").toLowerCase(); state.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.db) { return state.db; } if (state.loadingPromise) { return state.loadingPromise; } state.loadingPromise = Promise.resolve() .then(async () => { const configuredDb = getConfiguredGematriaDb(); if (configuredDb) { return configuredDb; } const referenceData = await window.TarotDataService?.loadReferenceData?.(); return referenceData?.gematriaCiphers || null; }) .then((db) => { if (!db) { throw new Error("Gematria cipher data unavailable from API."); } state.db = sanitizeGematriaDb(db); return state.db; }) .catch(() => { state.db = getFallbackGematriaDb(); return state.db; }) .finally(() => { state.loadingPromise = null; }); return state.loadingPromise; } function getActiveGematriaCipher() { const db = state.db || getFallbackGematriaDb(); const ciphers = Array.isArray(db.ciphers) ? db.ciphers : []; if (!ciphers.length) { return null; } const selectedId = state.activeCipherId || ciphers[0].id; return ciphers.find((cipher) => cipher.id === selectedId) || ciphers[0]; } function renderGematriaCipherOptions() { const { cipherEl } = getElements(); if (!cipherEl) { return; } const db = state.db || getFallbackGematriaDb(); const ciphers = Array.isArray(db.ciphers) ? db.ciphers : []; cipherEl.innerHTML = ""; ciphers.forEach((cipher) => { const option = document.createElement("option"); option.value = cipher.id; option.textContent = cipher.name; if (cipher.description) { option.title = cipher.description; } cipherEl.appendChild(option); }); const activeCipher = getActiveGematriaCipher(); state.activeCipherId = activeCipher?.id || ""; cipherEl.value = state.activeCipherId; } function setMatchesMessage(matchesEl, message) { if (!matchesEl) { return; } matchesEl.replaceChildren(); const emptyEl = document.createElement("div"); emptyEl.className = "alpha-gematria-match-empty"; emptyEl.textContent = message; matchesEl.appendChild(emptyEl); } function clearReverseMatches(matchesEl) { if (!matchesEl) { return; } matchesEl.replaceChildren(); matchesEl.hidden = true; } function getModeElements(modeEls) { return Array.isArray(modeEls) ? modeEls.filter((element) => element instanceof HTMLInputElement) : []; } function updateModeUi() { const { cipherEl, inputEl, modeEls, matchesEl, inputLabelEl, cipherLabelEl } = getElements(); const reverseMode = isReverseMode(); const anagramMode = isAnagramMode(); const radioEls = getModeElements(modeEls); radioEls.forEach((element) => { element.checked = String(element.value || "") === state.activeMode; }); if (inputLabelEl) { inputLabelEl.textContent = reverseMode ? "Value" : (anagramMode ? "Letters" : "Text"); } if (cipherLabelEl) { cipherLabelEl.textContent = (reverseMode || anagramMode) ? "Cipher (not used in this mode)" : "Cipher"; } if (cipherEl) { const disableCipher = reverseMode || anagramMode; cipherEl.disabled = disableCipher; cipherEl.closest(".alpha-gematria-field")?.classList.toggle("is-disabled", disableCipher); } if (inputEl) { inputEl.placeholder = reverseMode ? "Enter a whole number, e.g. 33" : (anagramMode ? "Type letters or a word, e.g. listen" : "Type or paste text"); inputEl.inputMode = reverseMode ? "numeric" : "text"; inputEl.spellcheck = !(reverseMode || anagramMode); const nextValue = getCurrentInputText(); if (inputEl.value !== nextValue) { inputEl.value = nextValue; } } if (!reverseMode && !anagramMode) { clearReverseMatches(matchesEl); } } function parseReverseLookupValue(rawValue) { const normalizedValue = String(rawValue || "").trim(); if (!normalizedValue) { return null; } if (!/^\d+$/.test(normalizedValue)) { return Number.NaN; } const numericValue = Number(normalizedValue); if (!Number.isSafeInteger(numericValue)) { return Number.NaN; } return numericValue; } async function loadReverseLookup(value) { const cacheKey = String(value); if (state.reverseLookupCache.has(cacheKey)) { return state.reverseLookupCache.get(cacheKey); } const payload = await window.TarotDataService?.loadGematriaWordsByValue?.(value); state.reverseLookupCache.set(cacheKey, payload); return payload; } async function loadAnagramLookup(text) { const cacheKey = String(text || "").trim().toLowerCase(); if (state.anagramLookupCache.has(cacheKey)) { return state.anagramLookupCache.get(cacheKey); } const payload = await window.TarotDataService?.loadWordAnagrams?.(text); state.anagramLookupCache.set(cacheKey, payload); return payload; } function renderReverseLookupMatches(payload, numericValue) { const { resultEl, breakdownEl, matchesEl } = getElements(); if (!resultEl || !breakdownEl || !matchesEl) { return; } const matches = Array.isArray(payload?.matches) ? payload.matches : []; const count = Number(payload?.count); const displayCount = Number.isFinite(count) ? count : matches.length; const ciphers = Array.isArray(payload?.ciphers) ? payload.ciphers : []; const cipherCount = Number(payload?.cipherCount); const displayCipherCount = Number.isFinite(cipherCount) ? cipherCount : ciphers.length; const visibleMatches = matches.slice(0, 120); resultEl.textContent = `Value: ${formatCount(numericValue)}`; if (!displayCount) { breakdownEl.textContent = "No words matched this reverse gematria value."; matchesEl.hidden = false; setMatchesMessage(matchesEl, "No matches found in the reverse gematria index."); return; } const topCipherSummary = ciphers .slice(0, 6) .map((cipher) => `${String(cipher?.name || cipher?.id || "Unknown")} ${formatCount(cipher?.count)}`) .join(" · "); breakdownEl.textContent = `Found ${formatCount(displayCount)} matches across ${formatCount(displayCipherCount)} ciphers.${topCipherSummary ? ` Top ciphers: ${topCipherSummary}.` : ""}${displayCount > visibleMatches.length ? ` Showing first ${formatCount(visibleMatches.length)}.` : ""}`; const fragment = document.createDocumentFragment(); visibleMatches.forEach((match) => { const cardEl = document.createElement("article"); cardEl.className = "alpha-gematria-match"; const wordEl = document.createElement("div"); wordEl.className = "alpha-gematria-match-word"; wordEl.textContent = String(match?.word || "--"); cardEl.appendChild(wordEl); const definition = String(match?.definition || "").trim(); if (definition) { const definitionEl = document.createElement("div"); definitionEl.className = "alpha-gematria-match-definition"; definitionEl.textContent = definition; cardEl.appendChild(definitionEl); } const ciphersEl = document.createElement("div"); ciphersEl.className = "alpha-gematria-match-ciphers"; const matchCiphers = Array.isArray(match?.ciphers) ? match.ciphers : []; matchCiphers.slice(0, 6).forEach((cipher) => { const chipEl = document.createElement("span"); chipEl.className = "alpha-gematria-match-cipher"; chipEl.textContent = String(cipher?.name || cipher?.id || "Unknown"); ciphersEl.appendChild(chipEl); }); if (matchCiphers.length > 6) { const extraEl = document.createElement("span"); extraEl.className = "alpha-gematria-match-cipher"; extraEl.textContent = `+${matchCiphers.length - 6} more`; ciphersEl.appendChild(extraEl); } cardEl.appendChild(ciphersEl); fragment.appendChild(cardEl); }); matchesEl.replaceChildren(fragment); matchesEl.hidden = false; } async function renderReverseLookupResult() { const { resultEl, breakdownEl, matchesEl } = getElements(); if (!resultEl || !breakdownEl || !matchesEl) { return; } const rawValue = state.reverseInputText; if (!String(rawValue || "").trim()) { resultEl.textContent = "Value: --"; breakdownEl.textContent = "Enter a whole number to find words across all available ciphers."; matchesEl.hidden = false; setMatchesMessage(matchesEl, "Reverse lookup searches the API-backed gematria word index."); return; } const numericValue = parseReverseLookupValue(rawValue); if (!Number.isFinite(numericValue)) { resultEl.textContent = "Value: --"; breakdownEl.textContent = "Enter digits only to search the reverse gematria index."; matchesEl.hidden = false; setMatchesMessage(matchesEl, "Use a whole number such as 33 or 418."); return; } const requestId = state.reverseRequestId + 1; state.reverseRequestId = requestId; resultEl.textContent = `Value: ${formatCount(numericValue)}`; breakdownEl.textContent = "Searching reverse gematria index..."; matchesEl.hidden = false; setMatchesMessage(matchesEl, "Loading matching words..."); try { const payload = await loadReverseLookup(numericValue); if (requestId !== state.reverseRequestId || !isReverseMode()) { return; } renderReverseLookupMatches(payload, numericValue); } catch { if (requestId !== state.reverseRequestId || !isReverseMode()) { return; } resultEl.textContent = `Value: ${formatCount(numericValue)}`; breakdownEl.textContent = "Reverse lookup is unavailable right now."; matchesEl.hidden = false; setMatchesMessage(matchesEl, "Unable to load reverse gematria words from the API."); } } function renderAnagramMatches(payload) { const { resultEl, breakdownEl, matchesEl } = getElements(); if (!resultEl || !breakdownEl || !matchesEl) { return; } const matches = Array.isArray(payload?.matches) ? payload.matches : []; const count = Number(payload?.count); const displayCount = Number.isFinite(count) ? count : matches.length; const visibleMatches = matches.slice(0, 120); const letterCount = Number(payload?.letterCount); resultEl.textContent = `Anagrams: ${formatCount(displayCount)}`; if (!displayCount) { breakdownEl.textContent = "No exact dictionary anagrams matched these letters."; matchesEl.hidden = false; setMatchesMessage(matchesEl, "Try another letter set or a different spelling."); return; } breakdownEl.textContent = `Found ${formatCount(displayCount)} anagrams for ${formatCount(letterCount)} letters.${displayCount > visibleMatches.length ? ` Showing first ${formatCount(visibleMatches.length)}.` : ""}`; const fragment = document.createDocumentFragment(); visibleMatches.forEach((match) => { const cardEl = document.createElement("article"); cardEl.className = "alpha-gematria-match"; const wordEl = document.createElement("div"); wordEl.className = "alpha-gematria-match-word"; wordEl.textContent = String(match?.word || "--"); cardEl.appendChild(wordEl); const definition = String(match?.definition || "").trim(); if (definition) { const definitionEl = document.createElement("div"); definitionEl.className = "alpha-gematria-match-definition"; definitionEl.textContent = definition; cardEl.appendChild(definitionEl); } fragment.appendChild(cardEl); }); matchesEl.replaceChildren(fragment); matchesEl.hidden = false; } async function renderAnagramResult() { const { resultEl, breakdownEl, matchesEl } = getElements(); if (!resultEl || !breakdownEl || !matchesEl) { return; } const rawText = state.anagramInputText; if (!String(rawText || "").trim()) { resultEl.textContent = "Anagrams: --"; breakdownEl.textContent = "Enter letters to search for exact dictionary anagrams."; matchesEl.hidden = false; setMatchesMessage(matchesEl, "Anagram Maker finds exact word anagrams from the API word index."); return; } const requestId = state.anagramRequestId + 1; state.anagramRequestId = requestId; resultEl.textContent = "Anagrams: --"; breakdownEl.textContent = "Searching anagram index..."; matchesEl.hidden = false; setMatchesMessage(matchesEl, "Loading anagrams..."); try { const payload = await loadAnagramLookup(rawText); if (requestId !== state.anagramRequestId || !isAnagramMode()) { return; } renderAnagramMatches(payload); } catch { if (requestId !== state.anagramRequestId || !isAnagramMode()) { return; } resultEl.textContent = "Anagrams: --"; breakdownEl.textContent = "Anagram lookup is unavailable right now."; matchesEl.hidden = false; setMatchesMessage(matchesEl, "Unable to load anagrams from the API."); } } function computeGematria(text, cipher, baseAlphabet) { const normalizedInput = normalizeGematriaText(text); const scriptMap = state.scriptCharMap instanceof Map ? state.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 renderForwardGematriaResult() { const { resultEl, breakdownEl } = getElements(); if (!resultEl || !breakdownEl) { return; } const db = state.db || getFallbackGematriaDb(); if (!(state.scriptCharMap instanceof Map) || !state.scriptCharMap.size) { refreshScriptMap(db.baseAlphabet); } const cipher = getActiveGematriaCipher(); if (!cipher) { resultEl.textContent = "Total: --"; breakdownEl.textContent = "No ciphers available."; return; } const { total, count, breakdown } = computeGematria(state.forwardInputText, cipher, db.baseAlphabet); resultEl.textContent = `Total: ${total}`; if (!count) { breakdownEl.textContent = `Using ${cipher.name}. Enter English, Greek, or Hebrew letters to calculate.`; return; } breakdownEl.textContent = `${cipher.name} · ${count} letters · ${breakdown} = ${total}`; } function renderGematriaResult() { updateModeUi(); if (isReverseMode()) { void renderReverseLookupResult(); return; } if (isAnagramMode()) { void renderAnagramResult(); return; } renderForwardGematriaResult(); } function bindGematriaListeners() { const { cipherEl, inputEl, modeEls } = getElements(); if (state.listenersBound || !cipherEl || !inputEl) { return; } cipherEl.addEventListener("change", () => { state.activeCipherId = String(cipherEl.value || "").trim(); renderGematriaResult(); }); inputEl.addEventListener("input", () => { if (isReverseMode()) { state.reverseInputText = inputEl.value || ""; } else if (isAnagramMode()) { state.anagramInputText = inputEl.value || ""; } else { state.forwardInputText = inputEl.value || ""; } renderGematriaResult(); }); getModeElements(modeEls).forEach((modeEl) => { modeEl.addEventListener("change", () => { if (!modeEl.checked) { return; } state.activeMode = String(modeEl.value || "forward").trim() || "forward"; updateModeUi(); renderGematriaResult(); }); }); state.listenersBound = true; } function ensureCalculator() { const { cipherEl, inputEl, resultEl, breakdownEl } = getElements(); if (!cipherEl || !inputEl || !resultEl || !breakdownEl) { return; } bindGematriaListeners(); updateModeUi(); void loadGematriaDb().then(() => { refreshScriptMap((state.db || getFallbackGematriaDb()).baseAlphabet); renderGematriaCipherOptions(); renderGematriaResult(); }); } function init(nextConfig = {}) { config = { ...config, ...nextConfig }; const configuredDb = getConfiguredGematriaDb(); if (configuredDb) { state.db = sanitizeGematriaDb(configuredDb); } } window.AlphabetGematriaUi = { ...(window.AlphabetGematriaUi || {}), init, refreshScriptMap, ensureCalculator }; })();