MediaWiki:Common.js: Difference between revisions
From Encyclopedium Universum
No edit summary |
Refresh citation JSON loading after divine-name correction |
||
| (8 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/* Any JavaScript here will be loaded for all users on every page load. */ | /* Any JavaScript here will be loaded for all users on every page load. */ | ||
function | (function(){ | ||
const | // Tanakh verse counts | ||
const verseCounts = { | |||
"Genesis": [31,25,24,26,32,22,24,22,29,32,32,20,18,24,21,16,27,33,38,18,34,24,20,67,34,35,46,22,35,43,55,32,20,31,29,43,36,30,23,23,57,38,34,34,31,22,33,26,22,25], | |||
"Exodus": [22,25,22,31,23,30,25,32,35,29,10,51,22,31,27,36,16,27,25,26,37,30,33,18,40,37,21,43,46,38,18,35,23,35,35,38,29,31,43,38], | |||
"Leviticus": [17,16,17,35,19,30,38,36,24,20,47,8,59,57,33,34,16,30,37,27,24,33,44,23,55,46,34], | |||
"Numbers": [54,34,51,49,31,27,89,26,23,36,35,16,33,45,41,50,13,32,22,29,35,41,30,25,18,65,23,31,40,16,54,42,56,29,34,13], | |||
"Deuteronomy": [46,37,29,49,33,25,26,20,29,22,32,32,18,29,23,22,20,22,21,20,23,30,25,22,19,19,26,68,29,20,30,52,29,12], | |||
// Add rest of Tanakh... | |||
}; | |||
const title = mw.config.get('wgTitle'); // current wiki page title | |||
const match = title.match(/^(.*?) \((.*?)\)(?: (\d+))?$/); | |||
if (!match) return; // doesn't match expected format | |||
const translitName = match[1]; | |||
const englishName = match[2]; | |||
const chapterNum = match[3] ? parseInt(match[3], 10) : null; | |||
if (!verseCounts[englishName]) return; // no data for this book | |||
const container = document.getElementById('tanakh-selector'); | |||
if (!container) return; | |||
const chapters = verseCounts[englishName]; | |||
// Title | |||
const h2 = document.createElement('h2'); | |||
h2.textContent = `${translitName} (${englishName}) — Select Chapter & Verse`; | |||
container.appendChild(h2); | |||
// Chapter buttons grid | |||
const chaptersDiv = document.createElement('div'); | |||
chaptersDiv.style.display = 'grid'; | |||
chaptersDiv.style.gridTemplateColumns = 'repeat(auto-fit, minmax(72px, 1fr))'; | |||
chaptersDiv.style.gap = '8px'; | |||
container.appendChild(chaptersDiv); | |||
// Verse panel | |||
const versesPanel = document.createElement('div'); | |||
versesPanel.style.display = 'none'; | |||
versesPanel.style.marginTop = '12px'; | |||
container.appendChild(versesPanel); | |||
const versesHeader = document.createElement('div'); | |||
versesHeader.style.display = 'flex'; | |||
versesHeader.style.justifyContent = 'space-between'; | |||
versesHeader.style.alignItems = 'center'; | |||
versesPanel.appendChild(versesHeader); | |||
const currentChLabel = document.createElement('strong'); | |||
versesHeader.appendChild(currentChLabel); | |||
const closeBtn = document.createElement('button'); | |||
closeBtn.textContent = 'Close'; | |||
closeBtn.style.cssText = 'background:#eee;border:1px solid #ccc;padding:4px 8px;border-radius:6px;cursor:pointer;'; | |||
versesHeader.appendChild(closeBtn); | |||
const versesDiv = document.createElement('div'); | |||
versesDiv.style.display = 'flex'; | |||
versesDiv.style.flexWrap = 'wrap'; | |||
versesDiv.style.gap = '6px'; | |||
versesDiv.style.marginTop = '8px'; | |||
versesPanel.appendChild(versesDiv); | |||
// Function to open chapter | |||
function openChapter(chNum) { | |||
const verseCount = chapters[chNum - 1]; | |||
currentChLabel.textContent = `Chapter ${chNum} — ${verseCount} verses`; | |||
versesDiv.innerHTML = ''; | |||
for (let v = 1; v <= verseCount; v++) { | |||
const a = document.createElement('a'); | |||
const pageName = `${translitName} (${englishName}) ${chNum}`.replace(/ /g, '_'); | |||
a.href = `/wiki/${pageName}#v${v}`; | |||
a.textContent = v; | |||
a.style.cssText = 'display:inline-flex;align-items:center;justify-content:center;padding:6px 8px;border-radius:6px;border:1px solid #e0e0e0;text-decoration:none;min-width:36px;text-align:center;'; | |||
versesDiv.appendChild(a); | |||
} | |||
versesPanel.style.display = 'block'; | |||
versesPanel.scrollIntoView({behavior:'smooth', block:'nearest'}); | |||
} | |||
// Build chapter buttons | |||
chapters.forEach((verseCount, i) => { | |||
const chNum = i + 1; | |||
const btn = document.createElement('button'); | |||
btn.type = 'button'; | |||
btn.textContent = chNum; | |||
btn.style.cssText = 'padding:10px 8px;border-radius:8px;border:1px solid #d0d0d0;background:#fff;cursor:pointer;'; | |||
btn.addEventListener('click', () => openChapter(chNum)); | |||
chaptersDiv.appendChild(btn); | |||
}); | |||
closeBtn.addEventListener('click', () => { | |||
versesPanel.style.display = 'none'; | |||
}); | |||
// Auto-open chapter if we are on a chapter page | |||
if (chapterNum) { | |||
openChapter(chapterNum); | |||
} | |||
})(); | |||
/* UKOY Torah citation popups: Chicago-note references backed by Pealim Tanakh.v2++ and KJV++ exports. */ | |||
(function () { | |||
const DATA_URL = "/resources/ukoy/verse-citations.json"; | |||
let dataPromise = null; | |||
let modal = null; | |||
function fetchData() { | |||
if (!dataPromise) { | |||
dataPromise = fetch(DATA_URL + "?v=20260524a", { credentials: "same-origin", cache: "no-store" }).then(function (response) { | |||
if (!response.ok) throw new Error("Unable to load citation data"); | |||
return response.json(); | |||
}); | |||
} | } | ||
} | return dataPromise; | ||
} | |||
function ensureModal() { | |||
if (modal) return modal; | |||
modal = document.createElement("div"); | |||
modal.className = "ukoy-verse-modal"; | |||
modal.innerHTML = "<div class='ukoy-verse-modal-card' role='dialog' aria-modal='true' aria-label='Torah citation'><button type='button' class='ukoy-verse-modal-close' aria-label='Close'>×</button><div class='ukoy-verse-modal-body'></div></div>"; | |||
document.body.appendChild(modal); | |||
modal.addEventListener("click", function (event) { | |||
if (event.target === modal || event.target.classList.contains("ukoy-verse-modal-close")) closeModal(); | |||
}); | |||
document.addEventListener("keydown", function (event) { if (event.key === "Escape") closeModal(); }); | |||
return modal; | |||
} | |||
function closeModal() { if (modal) modal.classList.remove("is-open"); } | |||
function | function escapeHtml(value) { | ||
const | return String(value || "").replace(/[&<>'"]/g, function (char) { | ||
if ( | return { "&": "&", "<": "<", ">": ">", "'": "'", '"': """ }[char]; | ||
}); | |||
} | |||
function showModal(html) { | |||
const m = ensureModal(); | |||
m.querySelector(".ukoy-verse-modal-body").innerHTML = html; | |||
m.classList.add("is-open"); | |||
} | |||
function renderCitation(entry) { | |||
if (!entry) return "<div class='ukoy-verse-error'>Citation data not found.</div>"; | |||
const verses = entry.verses || []; | |||
return "<div class='ukoy-verse-heading'>" + escapeHtml(entry.label) + "</div>" + | |||
verses.map(function (verse) { | |||
return "<section class='ukoy-verse-entry'>" + | |||
"<div class='ukoy-verse-ref'>" + escapeHtml(verse.reference) + "</div>" + | |||
"<div class='ukoy-verse-label'>Tanakh.v2++ interlinear</div>" + | |||
(verse.tanakhInterlinearHtml || "<div class='ukoy-missing'>Tanakh.v2++ verse not available.</div>") + | |||
"<div class='ukoy-verse-label'>KJV++ plain English rendering</div>" + | |||
"<div class='ukoy-kjv-text'>" + escapeHtml(verse.kjvText || "") + "</div>" + | |||
"</section>"; | |||
}).join(""); | |||
} | |||
function renderEntry(kind, key, entry) { | |||
const label = kind === "strong" ? "Strong's / BDB Lexicon" : "Morphology"; | |||
if (!entry) { | |||
return "<div class='ukoy-verse-heading'>" + escapeHtml(label + " " + key) + "</div><div class='ukoy-verse-error'>Entry not found in the loaded Pealim data.</div>"; | |||
} | } | ||
} | let body = entry.html || entry.definitionHtml || entry.content || ""; | ||
body = body.replace(/<style[\s\S]*?<\/style>/gi, ""); | |||
return "<div class='ukoy-verse-heading'>" + escapeHtml(label + " " + key) + "</div>" + | |||
"<section class='ukoy-verse-entry ukoy-entry-popup'><div class='ukoy-entry-html'>" + body + "</div></section>"; | |||
} | |||
function openCitation(ref) { | |||
showModal("<div class='ukoy-loading'>Loading citation…</div>"); | |||
fetchData() | |||
.then(function (data) { | |||
const citations = data.citations || data; | |||
showModal(renderCitation(citations[ref])); | |||
}) | |||
.catch(function (error) { showModal("<div class='ukoy-verse-error'>" + escapeHtml(error.message) + "</div>"); }); | |||
} | |||
function openEntry(kind, key) { | |||
showModal("<div class='ukoy-loading'>Loading entry…</div>"); | |||
fetchData() | |||
.then(function (data) { | |||
const entries = (data.entries && data.entries[kind]) || {}; | |||
showModal(renderEntry(kind, key, entries[key])); | |||
}) | |||
.catch(function (error) { showModal("<div class='ukoy-verse-error'>" + escapeHtml(error.message) + "</div>"); }); | |||
} | |||
document.addEventListener("click", function (event) { | |||
const citation = event.target.closest(".ukoy-cite"); | |||
if (citation) { | |||
event.preventDefault(); | |||
openCitation(citation.getAttribute("data-ref")); | |||
return; | |||
} | |||
const strong = event.target.closest(".ukoy-token-link.ukoy-strong"); | |||
if (strong) { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
openEntry("strong", strong.getAttribute("data-key")); | |||
return; | |||
} | |||
const morph = event.target.closest(".ukoy-token-link.ukoy-morph"); | |||
if (morph) { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
openEntry("morph", morph.getAttribute("data-key")); | |||
} | |||
}); | |||
})(); | |||
/* UKOY final citation + grammar override: data-ref superscripts, stacked popups, grammar-book links. */ | |||
(function () { | |||
const VERSE_URL = "/resources/ukoy/verse-citations.json"; | |||
const GRAMMAR_URL = "/resources/ukoy/grammar-entries.json"; | |||
let verseDataPromise = null; | |||
let grammarDataPromise = null; | |||
let overlay = null; | |||
const stack = []; | |||
function esc(v) { | |||
return String(v || "").replace(/[&<>"']/g, function (c) { | |||
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; | |||
}); | |||
} | |||
function verseData() { | |||
if (!verseDataPromise) verseDataPromise = fetch(VERSE_URL + "?v=20260524a", { credentials: "same-origin", cache: "no-store" }).then(function (r) { if (!r.ok) throw new Error("Unable to load verse data"); return r.json(); }); | |||
return verseDataPromise; | |||
} | |||
function grammarData() { | |||
if (!grammarDataPromise) grammarDataPromise = fetch(GRAMMAR_URL, { credentials: "same-origin" }).then(function (r) { if (!r.ok) throw new Error("Unable to load grammar data"); return r.json(); }); | |||
return grammarDataPromise; | |||
} | |||
function ensureOverlay() { | |||
if (overlay) return overlay; | |||
overlay = document.createElement("div"); | |||
overlay.className = "ukoy-modal-stack-overlay"; | |||
document.body.appendChild(overlay); | |||
overlay.addEventListener("click", function (e) { if (e.target === overlay) closeTop(); }); | |||
document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeTop(); }); | |||
return overlay; | |||
} | |||
function lockScroll() { document.documentElement.classList.add("ukoy-modal-open"); document.body.classList.add("ukoy-modal-open"); } | |||
function unlockIfEmpty() { if (!stack.length) { document.documentElement.classList.remove("ukoy-modal-open"); document.body.classList.remove("ukoy-modal-open"); } } | |||
function closeTop() { | |||
const top = stack.pop(); | |||
if (top && top.parentNode) top.parentNode.removeChild(top); | |||
if (stack.length) stack[stack.length - 1].classList.add("is-top"); | |||
else if (overlay) overlay.classList.remove("is-open"); | |||
unlockIfEmpty(); | |||
} | |||
function openLayer(html) { | |||
const ov = ensureOverlay(); | |||
stack.forEach(function (c) { c.classList.remove("is-top"); }); | |||
const card = document.createElement("div"); | |||
card.className = "ukoy-verse-modal-card is-top"; | |||
card.style.zIndex = String(100000 + stack.length + 1); | |||
card.innerHTML = "<button type='button' class='ukoy-verse-modal-close' aria-label='Close'>×</button><div class='ukoy-verse-modal-body'></div>"; | |||
card.querySelector(".ukoy-verse-modal-body").innerHTML = html; | |||
card.querySelector(".ukoy-verse-modal-close").addEventListener("click", function (e) { e.preventDefault(); closeTop(); }); | |||
ov.appendChild(card); | |||
ov.classList.add("is-open"); | |||
stack.push(card); | |||
lockScroll(); | |||
return card; | |||
} | |||
function replaceTop(html) { if (!stack.length) return openLayer(html); stack[stack.length - 1].querySelector(".ukoy-verse-modal-body").innerHTML = html; } | |||
function stripOccurrences(html) { | |||
html = String(html || "").replace(/<style[\s\S]*?<\/style>/gi, ""); | |||
return html.replace(/<p><strong>Total KJV Occurrences:[\s\S]*$/i, "").replace(/<strong>Total KJV Occurrences:[\s\S]*$/i, "").replace(/Total KJV Occurrences:[\s\S]*$/i, ""); | |||
} | |||
function renderCitation(entry) { | |||
if (!entry) return "<div class='ukoy-verse-error'>Citation data not found.</div>"; | |||
return "<div class='ukoy-verse-heading'>" + esc(entry.label) + "</div>" + (entry.verses || []).map(function (verse) { | |||
return "<section class='ukoy-verse-entry'><div class='ukoy-verse-ref'>" + esc(verse.reference) + "</div>" + | |||
"<div class='ukoy-verse-label'>Tanakh.v2++ interlinear</div>" + (verse.tanakhInterlinearHtml || "") + | |||
"<div class='ukoy-verse-label'>KJV++ plain English rendering</div><div class='ukoy-kjv-text'>" + esc(verse.kjvText || "") + "</div></section>"; | |||
}).join(""); | |||
} | |||
function refsFromNote(note) { | |||
if (note.dataset.refs) return note.dataset.refs.split(";").filter(Boolean); | |||
const n = (note.id || "").replace("ukoy-note-ref-", ""); | |||
const box = document.getElementById("ukoy-note-" + n); | |||
return box ? Array.from(box.querySelectorAll(".ukoy-cite")).map(function (x) { return x.getAttribute("data-ref"); }).filter(Boolean) : []; | |||
} | |||
function openRefs(refs) { | |||
openLayer("<div class='ukoy-loading'>Loading citation…</div>"); | |||
verseData().then(function (data) { | |||
const citations = data.citations || data; | |||
replaceTop(refs.map(function (r) { return renderCitation(citations[r]); }).join("<hr class='ukoy-citation-separator'>")); | |||
}).catch(function (e) { replaceTop("<div class='ukoy-verse-error'>" + esc(e.message) + "</div>"); }); | |||
} | |||
function tokenContext(button) { | |||
const token = button.closest(".ukoy-vtok"); | |||
const verse = button.closest(".ukoy-verse-entry"); | |||
return { | |||
surface: token && token.querySelector(".ukoy-vtok-surface") ? token.querySelector(".ukoy-vtok-surface").textContent : "", | |||
strong: token && token.querySelector(".ukoy-strong") ? token.querySelector(".ukoy-strong").getAttribute("data-key") : "", | |||
morph: Array.from(token ? token.querySelectorAll(".ukoy-morph") : []).map(function (m) { return m.textContent; }).join(" "), | |||
ref: verse && verse.querySelector(".ukoy-verse-ref") ? verse.querySelector(".ukoy-verse-ref").textContent : "" | |||
}; | |||
} | |||
function renderLex(kind, key, entry, ctx) { | |||
const title = kind === "strong" ? "Strong's / BDB Lexicon" : "Morphology"; | |||
const context = "<div class='ukoy-token-context'>" + | |||
(ctx.surface ? "<div><span>Word</span> <b class='ukoy-context-surface'>" + esc(ctx.surface) + "</b></div>" : "") + | |||
(ctx.ref ? "<div><span>Verse</span> " + esc(ctx.ref) + "</div>" : "") + | |||
(ctx.strong ? "<div><span>Strong</span> " + esc(ctx.strong) + "</div>" : "") + | |||
(ctx.morph ? "<div><span>Morphology</span> " + esc(ctx.morph) + "</div>" : "") + "</div>"; | |||
if (!entry) return "<div class='ukoy-verse-heading'>" + esc(title + " " + key) + "</div>" + context + "<div class='ukoy-verse-error'>Entry not found.</div>"; | |||
const raw = entry.html || entry.definitionHtml || entry.content || ""; | |||
const trimmed = stripOccurrences(raw); | |||
return "<div class='ukoy-verse-heading'>" + esc(title + " " + key) + "</div>" + context + "<section class='ukoy-verse-entry ukoy-entry-popup'><div class='ukoy-entry-html'>" + trimmed + "</div></section>" + (raw !== trimmed ? "<details class='ukoy-see-all-entry'><summary>See full lexicon detail</summary><div class='ukoy-entry-html'>" + raw + "</div></details>" : ""); | |||
} | |||
function openLex(kind, key, ctx) { | |||
openLayer("<div class='ukoy-loading'>Loading entry…</div>"); | |||
verseData().then(function (data) { | |||
const entries = (data.entries && data.entries[kind]) || {}; | |||
replaceTop(renderLex(kind, key, entries[key], ctx || {})); | |||
}).catch(function (e) { replaceTop("<div class='ukoy-verse-error'>" + esc(e.message) + "</div>"); }); | |||
} | |||
function grammarKeys(href, label) { | |||
let key = decodeURIComponent(String(href || "").replace(/_/g, " ").trim()); | |||
const txt = String(label || "").trim(); | |||
const title = key.indexOf("k-HebrewGrammar ") === 0 ? key.replace("k-HebrewGrammar ", "") : (key.indexOf("k ") === 0 ? key.slice(2) : txt); | |||
const cap = title.replace(/\b\w/g, function (m) { return m.toUpperCase(); }); | |||
return [key, key.toLowerCase(), "k " + title.toLowerCase(), "k-HebrewGrammar " + cap, cap, txt]; | |||
} | |||
function openGrammar(href, label) { | |||
const keys = grammarKeys(href, label); | |||
openLayer("<div class='ukoy-loading'>Loading grammar entry…</div>"); | |||
grammarData().then(function (data) { | |||
const entries = data.entries || {}; | |||
let entry = null; | |||
for (const k of keys) { if (entries[k]) { entry = entries[k]; break; } } | |||
if (!entry) return replaceTop("<div class='ukoy-verse-heading'>Grammar Entry</div><div class='ukoy-verse-error'>No grammar entry found for " + esc(keys[0]) + ".</div>"); | |||
replaceTop("<div class='ukoy-verse-heading'>" + esc(entry.title) + "</div><section class='ukoy-verse-entry ukoy-entry-popup ukoy-grammar-entry'><div class='ukoy-entry-html'>" + (entry.html || "") + "</div></section>"); | |||
}).catch(function (e) { replaceTop("<div class='ukoy-verse-error'>" + esc(e.message) + "</div>"); }); | |||
} | |||
document.addEventListener("click", function (event) { | |||
const grammar = event.target.closest(".ukoy-entry-html a[href^='k'], .ukoy-entry-html a[href^='k-HebrewGrammar']"); | |||
if (grammar) { event.preventDefault(); event.stopImmediatePropagation(); openGrammar(grammar.getAttribute("href"), grammar.textContent); return; } | |||
const note = event.target.closest(".ukoy-cite-note"); | |||
if (note) { event.preventDefault(); event.stopImmediatePropagation(); const refs = refsFromNote(note); if (refs.length) openRefs(refs); return; } | |||
const citation = event.target.closest(".ukoy-cite"); | |||
if (citation) { event.preventDefault(); event.stopImmediatePropagation(); openRefs([citation.getAttribute("data-ref")]); return; } | |||
const strong = event.target.closest(".ukoy-token-link.ukoy-strong"); | |||
if (strong) { event.preventDefault(); event.stopImmediatePropagation(); openLex("strong", strong.getAttribute("data-key"), tokenContext(strong)); return; } | |||
const morph = event.target.closest(".ukoy-token-link.ukoy-morph"); | |||
if (morph) { event.preventDefault(); event.stopImmediatePropagation(); openLex("morph", morph.getAttribute("data-key"), tokenContext(morph)); } | |||
}, true); | |||
})(); | |||
Latest revision as of 10:31, 24 May 2026
/* Any JavaScript here will be loaded for all users on every page load. */
(function(){
// Tanakh verse counts
const verseCounts = {
"Genesis": [31,25,24,26,32,22,24,22,29,32,32,20,18,24,21,16,27,33,38,18,34,24,20,67,34,35,46,22,35,43,55,32,20,31,29,43,36,30,23,23,57,38,34,34,31,22,33,26,22,25],
"Exodus": [22,25,22,31,23,30,25,32,35,29,10,51,22,31,27,36,16,27,25,26,37,30,33,18,40,37,21,43,46,38,18,35,23,35,35,38,29,31,43,38],
"Leviticus": [17,16,17,35,19,30,38,36,24,20,47,8,59,57,33,34,16,30,37,27,24,33,44,23,55,46,34],
"Numbers": [54,34,51,49,31,27,89,26,23,36,35,16,33,45,41,50,13,32,22,29,35,41,30,25,18,65,23,31,40,16,54,42,56,29,34,13],
"Deuteronomy": [46,37,29,49,33,25,26,20,29,22,32,32,18,29,23,22,20,22,21,20,23,30,25,22,19,19,26,68,29,20,30,52,29,12],
// Add rest of Tanakh...
};
const title = mw.config.get('wgTitle'); // current wiki page title
const match = title.match(/^(.*?) \((.*?)\)(?: (\d+))?$/);
if (!match) return; // doesn't match expected format
const translitName = match[1];
const englishName = match[2];
const chapterNum = match[3] ? parseInt(match[3], 10) : null;
if (!verseCounts[englishName]) return; // no data for this book
const container = document.getElementById('tanakh-selector');
if (!container) return;
const chapters = verseCounts[englishName];
// Title
const h2 = document.createElement('h2');
h2.textContent = `${translitName} (${englishName}) — Select Chapter & Verse`;
container.appendChild(h2);
// Chapter buttons grid
const chaptersDiv = document.createElement('div');
chaptersDiv.style.display = 'grid';
chaptersDiv.style.gridTemplateColumns = 'repeat(auto-fit, minmax(72px, 1fr))';
chaptersDiv.style.gap = '8px';
container.appendChild(chaptersDiv);
// Verse panel
const versesPanel = document.createElement('div');
versesPanel.style.display = 'none';
versesPanel.style.marginTop = '12px';
container.appendChild(versesPanel);
const versesHeader = document.createElement('div');
versesHeader.style.display = 'flex';
versesHeader.style.justifyContent = 'space-between';
versesHeader.style.alignItems = 'center';
versesPanel.appendChild(versesHeader);
const currentChLabel = document.createElement('strong');
versesHeader.appendChild(currentChLabel);
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Close';
closeBtn.style.cssText = 'background:#eee;border:1px solid #ccc;padding:4px 8px;border-radius:6px;cursor:pointer;';
versesHeader.appendChild(closeBtn);
const versesDiv = document.createElement('div');
versesDiv.style.display = 'flex';
versesDiv.style.flexWrap = 'wrap';
versesDiv.style.gap = '6px';
versesDiv.style.marginTop = '8px';
versesPanel.appendChild(versesDiv);
// Function to open chapter
function openChapter(chNum) {
const verseCount = chapters[chNum - 1];
currentChLabel.textContent = `Chapter ${chNum} — ${verseCount} verses`;
versesDiv.innerHTML = '';
for (let v = 1; v <= verseCount; v++) {
const a = document.createElement('a');
const pageName = `${translitName} (${englishName}) ${chNum}`.replace(/ /g, '_');
a.href = `/wiki/${pageName}#v${v}`;
a.textContent = v;
a.style.cssText = 'display:inline-flex;align-items:center;justify-content:center;padding:6px 8px;border-radius:6px;border:1px solid #e0e0e0;text-decoration:none;min-width:36px;text-align:center;';
versesDiv.appendChild(a);
}
versesPanel.style.display = 'block';
versesPanel.scrollIntoView({behavior:'smooth', block:'nearest'});
}
// Build chapter buttons
chapters.forEach((verseCount, i) => {
const chNum = i + 1;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = chNum;
btn.style.cssText = 'padding:10px 8px;border-radius:8px;border:1px solid #d0d0d0;background:#fff;cursor:pointer;';
btn.addEventListener('click', () => openChapter(chNum));
chaptersDiv.appendChild(btn);
});
closeBtn.addEventListener('click', () => {
versesPanel.style.display = 'none';
});
// Auto-open chapter if we are on a chapter page
if (chapterNum) {
openChapter(chapterNum);
}
})();
/* UKOY Torah citation popups: Chicago-note references backed by Pealim Tanakh.v2++ and KJV++ exports. */
(function () {
const DATA_URL = "/resources/ukoy/verse-citations.json";
let dataPromise = null;
let modal = null;
function fetchData() {
if (!dataPromise) {
dataPromise = fetch(DATA_URL + "?v=20260524a", { credentials: "same-origin", cache: "no-store" }).then(function (response) {
if (!response.ok) throw new Error("Unable to load citation data");
return response.json();
});
}
return dataPromise;
}
function ensureModal() {
if (modal) return modal;
modal = document.createElement("div");
modal.className = "ukoy-verse-modal";
modal.innerHTML = "<div class='ukoy-verse-modal-card' role='dialog' aria-modal='true' aria-label='Torah citation'><button type='button' class='ukoy-verse-modal-close' aria-label='Close'>×</button><div class='ukoy-verse-modal-body'></div></div>";
document.body.appendChild(modal);
modal.addEventListener("click", function (event) {
if (event.target === modal || event.target.classList.contains("ukoy-verse-modal-close")) closeModal();
});
document.addEventListener("keydown", function (event) { if (event.key === "Escape") closeModal(); });
return modal;
}
function closeModal() { if (modal) modal.classList.remove("is-open"); }
function escapeHtml(value) {
return String(value || "").replace(/[&<>'"]/g, function (char) {
return { "&": "&", "<": "<", ">": ">", "'": "'", '"': """ }[char];
});
}
function showModal(html) {
const m = ensureModal();
m.querySelector(".ukoy-verse-modal-body").innerHTML = html;
m.classList.add("is-open");
}
function renderCitation(entry) {
if (!entry) return "<div class='ukoy-verse-error'>Citation data not found.</div>";
const verses = entry.verses || [];
return "<div class='ukoy-verse-heading'>" + escapeHtml(entry.label) + "</div>" +
verses.map(function (verse) {
return "<section class='ukoy-verse-entry'>" +
"<div class='ukoy-verse-ref'>" + escapeHtml(verse.reference) + "</div>" +
"<div class='ukoy-verse-label'>Tanakh.v2++ interlinear</div>" +
(verse.tanakhInterlinearHtml || "<div class='ukoy-missing'>Tanakh.v2++ verse not available.</div>") +
"<div class='ukoy-verse-label'>KJV++ plain English rendering</div>" +
"<div class='ukoy-kjv-text'>" + escapeHtml(verse.kjvText || "") + "</div>" +
"</section>";
}).join("");
}
function renderEntry(kind, key, entry) {
const label = kind === "strong" ? "Strong's / BDB Lexicon" : "Morphology";
if (!entry) {
return "<div class='ukoy-verse-heading'>" + escapeHtml(label + " " + key) + "</div><div class='ukoy-verse-error'>Entry not found in the loaded Pealim data.</div>";
}
let body = entry.html || entry.definitionHtml || entry.content || "";
body = body.replace(/<style[\s\S]*?<\/style>/gi, "");
return "<div class='ukoy-verse-heading'>" + escapeHtml(label + " " + key) + "</div>" +
"<section class='ukoy-verse-entry ukoy-entry-popup'><div class='ukoy-entry-html'>" + body + "</div></section>";
}
function openCitation(ref) {
showModal("<div class='ukoy-loading'>Loading citation…</div>");
fetchData()
.then(function (data) {
const citations = data.citations || data;
showModal(renderCitation(citations[ref]));
})
.catch(function (error) { showModal("<div class='ukoy-verse-error'>" + escapeHtml(error.message) + "</div>"); });
}
function openEntry(kind, key) {
showModal("<div class='ukoy-loading'>Loading entry…</div>");
fetchData()
.then(function (data) {
const entries = (data.entries && data.entries[kind]) || {};
showModal(renderEntry(kind, key, entries[key]));
})
.catch(function (error) { showModal("<div class='ukoy-verse-error'>" + escapeHtml(error.message) + "</div>"); });
}
document.addEventListener("click", function (event) {
const citation = event.target.closest(".ukoy-cite");
if (citation) {
event.preventDefault();
openCitation(citation.getAttribute("data-ref"));
return;
}
const strong = event.target.closest(".ukoy-token-link.ukoy-strong");
if (strong) {
event.preventDefault();
event.stopPropagation();
openEntry("strong", strong.getAttribute("data-key"));
return;
}
const morph = event.target.closest(".ukoy-token-link.ukoy-morph");
if (morph) {
event.preventDefault();
event.stopPropagation();
openEntry("morph", morph.getAttribute("data-key"));
}
});
})();
/* UKOY final citation + grammar override: data-ref superscripts, stacked popups, grammar-book links. */
(function () {
const VERSE_URL = "/resources/ukoy/verse-citations.json";
const GRAMMAR_URL = "/resources/ukoy/grammar-entries.json";
let verseDataPromise = null;
let grammarDataPromise = null;
let overlay = null;
const stack = [];
function esc(v) {
return String(v || "").replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function verseData() {
if (!verseDataPromise) verseDataPromise = fetch(VERSE_URL + "?v=20260524a", { credentials: "same-origin", cache: "no-store" }).then(function (r) { if (!r.ok) throw new Error("Unable to load verse data"); return r.json(); });
return verseDataPromise;
}
function grammarData() {
if (!grammarDataPromise) grammarDataPromise = fetch(GRAMMAR_URL, { credentials: "same-origin" }).then(function (r) { if (!r.ok) throw new Error("Unable to load grammar data"); return r.json(); });
return grammarDataPromise;
}
function ensureOverlay() {
if (overlay) return overlay;
overlay = document.createElement("div");
overlay.className = "ukoy-modal-stack-overlay";
document.body.appendChild(overlay);
overlay.addEventListener("click", function (e) { if (e.target === overlay) closeTop(); });
document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeTop(); });
return overlay;
}
function lockScroll() { document.documentElement.classList.add("ukoy-modal-open"); document.body.classList.add("ukoy-modal-open"); }
function unlockIfEmpty() { if (!stack.length) { document.documentElement.classList.remove("ukoy-modal-open"); document.body.classList.remove("ukoy-modal-open"); } }
function closeTop() {
const top = stack.pop();
if (top && top.parentNode) top.parentNode.removeChild(top);
if (stack.length) stack[stack.length - 1].classList.add("is-top");
else if (overlay) overlay.classList.remove("is-open");
unlockIfEmpty();
}
function openLayer(html) {
const ov = ensureOverlay();
stack.forEach(function (c) { c.classList.remove("is-top"); });
const card = document.createElement("div");
card.className = "ukoy-verse-modal-card is-top";
card.style.zIndex = String(100000 + stack.length + 1);
card.innerHTML = "<button type='button' class='ukoy-verse-modal-close' aria-label='Close'>×</button><div class='ukoy-verse-modal-body'></div>";
card.querySelector(".ukoy-verse-modal-body").innerHTML = html;
card.querySelector(".ukoy-verse-modal-close").addEventListener("click", function (e) { e.preventDefault(); closeTop(); });
ov.appendChild(card);
ov.classList.add("is-open");
stack.push(card);
lockScroll();
return card;
}
function replaceTop(html) { if (!stack.length) return openLayer(html); stack[stack.length - 1].querySelector(".ukoy-verse-modal-body").innerHTML = html; }
function stripOccurrences(html) {
html = String(html || "").replace(/<style[\s\S]*?<\/style>/gi, "");
return html.replace(/<p><strong>Total KJV Occurrences:[\s\S]*$/i, "").replace(/<strong>Total KJV Occurrences:[\s\S]*$/i, "").replace(/Total KJV Occurrences:[\s\S]*$/i, "");
}
function renderCitation(entry) {
if (!entry) return "<div class='ukoy-verse-error'>Citation data not found.</div>";
return "<div class='ukoy-verse-heading'>" + esc(entry.label) + "</div>" + (entry.verses || []).map(function (verse) {
return "<section class='ukoy-verse-entry'><div class='ukoy-verse-ref'>" + esc(verse.reference) + "</div>" +
"<div class='ukoy-verse-label'>Tanakh.v2++ interlinear</div>" + (verse.tanakhInterlinearHtml || "") +
"<div class='ukoy-verse-label'>KJV++ plain English rendering</div><div class='ukoy-kjv-text'>" + esc(verse.kjvText || "") + "</div></section>";
}).join("");
}
function refsFromNote(note) {
if (note.dataset.refs) return note.dataset.refs.split(";").filter(Boolean);
const n = (note.id || "").replace("ukoy-note-ref-", "");
const box = document.getElementById("ukoy-note-" + n);
return box ? Array.from(box.querySelectorAll(".ukoy-cite")).map(function (x) { return x.getAttribute("data-ref"); }).filter(Boolean) : [];
}
function openRefs(refs) {
openLayer("<div class='ukoy-loading'>Loading citation…</div>");
verseData().then(function (data) {
const citations = data.citations || data;
replaceTop(refs.map(function (r) { return renderCitation(citations[r]); }).join("<hr class='ukoy-citation-separator'>"));
}).catch(function (e) { replaceTop("<div class='ukoy-verse-error'>" + esc(e.message) + "</div>"); });
}
function tokenContext(button) {
const token = button.closest(".ukoy-vtok");
const verse = button.closest(".ukoy-verse-entry");
return {
surface: token && token.querySelector(".ukoy-vtok-surface") ? token.querySelector(".ukoy-vtok-surface").textContent : "",
strong: token && token.querySelector(".ukoy-strong") ? token.querySelector(".ukoy-strong").getAttribute("data-key") : "",
morph: Array.from(token ? token.querySelectorAll(".ukoy-morph") : []).map(function (m) { return m.textContent; }).join(" "),
ref: verse && verse.querySelector(".ukoy-verse-ref") ? verse.querySelector(".ukoy-verse-ref").textContent : ""
};
}
function renderLex(kind, key, entry, ctx) {
const title = kind === "strong" ? "Strong's / BDB Lexicon" : "Morphology";
const context = "<div class='ukoy-token-context'>" +
(ctx.surface ? "<div><span>Word</span> <b class='ukoy-context-surface'>" + esc(ctx.surface) + "</b></div>" : "") +
(ctx.ref ? "<div><span>Verse</span> " + esc(ctx.ref) + "</div>" : "") +
(ctx.strong ? "<div><span>Strong</span> " + esc(ctx.strong) + "</div>" : "") +
(ctx.morph ? "<div><span>Morphology</span> " + esc(ctx.morph) + "</div>" : "") + "</div>";
if (!entry) return "<div class='ukoy-verse-heading'>" + esc(title + " " + key) + "</div>" + context + "<div class='ukoy-verse-error'>Entry not found.</div>";
const raw = entry.html || entry.definitionHtml || entry.content || "";
const trimmed = stripOccurrences(raw);
return "<div class='ukoy-verse-heading'>" + esc(title + " " + key) + "</div>" + context + "<section class='ukoy-verse-entry ukoy-entry-popup'><div class='ukoy-entry-html'>" + trimmed + "</div></section>" + (raw !== trimmed ? "<details class='ukoy-see-all-entry'><summary>See full lexicon detail</summary><div class='ukoy-entry-html'>" + raw + "</div></details>" : "");
}
function openLex(kind, key, ctx) {
openLayer("<div class='ukoy-loading'>Loading entry…</div>");
verseData().then(function (data) {
const entries = (data.entries && data.entries[kind]) || {};
replaceTop(renderLex(kind, key, entries[key], ctx || {}));
}).catch(function (e) { replaceTop("<div class='ukoy-verse-error'>" + esc(e.message) + "</div>"); });
}
function grammarKeys(href, label) {
let key = decodeURIComponent(String(href || "").replace(/_/g, " ").trim());
const txt = String(label || "").trim();
const title = key.indexOf("k-HebrewGrammar ") === 0 ? key.replace("k-HebrewGrammar ", "") : (key.indexOf("k ") === 0 ? key.slice(2) : txt);
const cap = title.replace(/\b\w/g, function (m) { return m.toUpperCase(); });
return [key, key.toLowerCase(), "k " + title.toLowerCase(), "k-HebrewGrammar " + cap, cap, txt];
}
function openGrammar(href, label) {
const keys = grammarKeys(href, label);
openLayer("<div class='ukoy-loading'>Loading grammar entry…</div>");
grammarData().then(function (data) {
const entries = data.entries || {};
let entry = null;
for (const k of keys) { if (entries[k]) { entry = entries[k]; break; } }
if (!entry) return replaceTop("<div class='ukoy-verse-heading'>Grammar Entry</div><div class='ukoy-verse-error'>No grammar entry found for " + esc(keys[0]) + ".</div>");
replaceTop("<div class='ukoy-verse-heading'>" + esc(entry.title) + "</div><section class='ukoy-verse-entry ukoy-entry-popup ukoy-grammar-entry'><div class='ukoy-entry-html'>" + (entry.html || "") + "</div></section>");
}).catch(function (e) { replaceTop("<div class='ukoy-verse-error'>" + esc(e.message) + "</div>"); });
}
document.addEventListener("click", function (event) {
const grammar = event.target.closest(".ukoy-entry-html a[href^='k'], .ukoy-entry-html a[href^='k-HebrewGrammar']");
if (grammar) { event.preventDefault(); event.stopImmediatePropagation(); openGrammar(grammar.getAttribute("href"), grammar.textContent); return; }
const note = event.target.closest(".ukoy-cite-note");
if (note) { event.preventDefault(); event.stopImmediatePropagation(); const refs = refsFromNote(note); if (refs.length) openRefs(refs); return; }
const citation = event.target.closest(".ukoy-cite");
if (citation) { event.preventDefault(); event.stopImmediatePropagation(); openRefs([citation.getAttribute("data-ref")]); return; }
const strong = event.target.closest(".ukoy-token-link.ukoy-strong");
if (strong) { event.preventDefault(); event.stopImmediatePropagation(); openLex("strong", strong.getAttribute("data-key"), tokenContext(strong)); return; }
const morph = event.target.closest(".ukoy-token-link.ukoy-morph");
if (morph) { event.preventDefault(); event.stopImmediatePropagation(); openLex("morph", morph.getAttribute("data-key"), tokenContext(morph)); }
}, true);
})();
