Files
TaroTime/app/ui-alphabet-gematria.js
2026-03-09 03:07:02 -07:00

638 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
(function () {
"use strict";
let config = {
getAlphabets: () => null,
getGematriaDb: () => null,
getGematriaElements: () => ({
cipherEl: null,
inputEl: null,
resultEl: null,
breakdownEl: null,
modeToggleEl: null,
matchesEl: null,
inputLabelEl: null,
cipherLabelEl: null
})
};
const state = {
loadingPromise: null,
db: null,
listenersBound: false,
activeCipherId: "",
forwardInputText: "",
reverseInputText: "",
activeMode: "forward",
scriptCharMap: new Map(),
reverseLookupCache: new Map(),
reverseRequestId: 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,
modeToggleEl: null,
matchesEl: null,
inputLabelEl: null,
cipherLabelEl: null
};
}
function isReverseMode() {
return state.activeMode === "reverse";
}
function getCurrentInputText() {
return isReverseMode()
? state.reverseInputText
: 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 updateModeUi() {
const {
cipherEl,
inputEl,
modeToggleEl,
matchesEl,
inputLabelEl,
cipherLabelEl
} = getElements();
const reverseMode = isReverseMode();
if (modeToggleEl) {
modeToggleEl.checked = reverseMode;
}
if (inputLabelEl) {
inputLabelEl.textContent = reverseMode ? "Value" : "Text";
}
if (cipherLabelEl) {
cipherLabelEl.textContent = reverseMode ? "Cipher (disabled in reverse mode)" : "Cipher";
}
if (cipherEl) {
cipherEl.disabled = reverseMode;
cipherEl.closest(".alpha-gematria-field")?.classList.toggle("is-disabled", reverseMode);
}
if (inputEl) {
inputEl.placeholder = reverseMode ? "Enter a whole number, e.g. 33" : "Type or paste text";
inputEl.inputMode = reverseMode ? "numeric" : "text";
inputEl.spellcheck = !reverseMode;
const nextValue = getCurrentInputText();
if (inputEl.value !== nextValue) {
inputEl.value = nextValue;
}
}
if (!reverseMode) {
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;
}
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 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;
}
renderForwardGematriaResult();
}
function bindGematriaListeners() {
const { cipherEl, inputEl, modeToggleEl } = 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 {
state.forwardInputText = inputEl.value || "";
}
renderGematriaResult();
});
modeToggleEl?.addEventListener("change", () => {
state.activeMode = modeToggleEl.checked ? "reverse" : "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
};
})();