353 lines
9.1 KiB
JavaScript
353 lines
9.1 KiB
JavaScript
|
|
(function () {
|
|||
|
|
"use strict";
|
|||
|
|
|
|||
|
|
let config = {
|
|||
|
|
getAlphabets: () => null,
|
|||
|
|
getGematriaElements: () => ({
|
|||
|
|
cipherEl: null,
|
|||
|
|
inputEl: null,
|
|||
|
|
resultEl: null,
|
|||
|
|
breakdownEl: null
|
|||
|
|
})
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const state = {
|
|||
|
|
loadingPromise: null,
|
|||
|
|
db: null,
|
|||
|
|
listenersBound: false,
|
|||
|
|
activeCipherId: "",
|
|||
|
|
inputText: "",
|
|||
|
|
scriptCharMap: new Map()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function getAlphabets() {
|
|||
|
|
return config.getAlphabets?.() || null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getElements() {
|
|||
|
|
return config.getGematriaElements?.() || {
|
|||
|
|
cipherEl: null,
|
|||
|
|
inputEl: null,
|
|||
|
|
resultEl: null,
|
|||
|
|
breakdownEl: null
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 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 = 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.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 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 renderGematriaResult() {
|
|||
|
|
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.inputText, 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 bindGematriaListeners() {
|
|||
|
|
const { cipherEl, inputEl } = getElements();
|
|||
|
|
if (state.listenersBound || !cipherEl || !inputEl) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cipherEl.addEventListener("change", () => {
|
|||
|
|
state.activeCipherId = String(cipherEl.value || "").trim();
|
|||
|
|
renderGematriaResult();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
inputEl.addEventListener("input", () => {
|
|||
|
|
state.inputText = inputEl.value || "";
|
|||
|
|
renderGematriaResult();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
state.listenersBound = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ensureCalculator() {
|
|||
|
|
const { cipherEl, inputEl, resultEl, breakdownEl } = getElements();
|
|||
|
|
if (!cipherEl || !inputEl || !resultEl || !breakdownEl) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bindGematriaListeners();
|
|||
|
|
|
|||
|
|
if (inputEl.value !== state.inputText) {
|
|||
|
|
inputEl.value = state.inputText;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void loadGematriaDb().then(() => {
|
|||
|
|
refreshScriptMap((state.db || getFallbackGematriaDb()).baseAlphabet);
|
|||
|
|
renderGematriaCipherOptions();
|
|||
|
|
renderGematriaResult();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function init(nextConfig = {}) {
|
|||
|
|
config = {
|
|||
|
|
...config,
|
|||
|
|
...nextConfig
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
window.AlphabetGematriaUi = {
|
|||
|
|
...(window.AlphabetGematriaUi || {}),
|
|||
|
|
init,
|
|||
|
|
refreshScriptMap,
|
|||
|
|
ensureCalculator
|
|||
|
|
};
|
|||
|
|
})();
|