(function () { "use strict"; const dataService = window.TarotDataService || {}; const STORAGE_KEYS = { showVerseHeads: "tarotime.alphaText.showVerseHeads" }; function readStoredBoolean(key, fallback) { try { const value = window.localStorage?.getItem?.(key); if (value === "true") { return true; } if (value === "false") { return false; } } catch (error) { // Ignore storage failures and keep in-memory defaults. } return fallback; } function writeStoredBoolean(key, value) { try { window.localStorage?.setItem?.(key, value ? "true" : "false"); } catch (error) { // Ignore storage failures and keep in-memory state. } } const state = { initialized: false, catalog: null, selectedSourceGroupId: "", selectedSourceId: "", selectedSourceIdByGroup: {}, compareSourceIdByGroup: {}, compareModeByGroup: {}, selectedWorkId: "", selectedSectionId: "", currentPassage: null, comparePassage: null, lexiconEntry: null, lexiconRequestId: 0, lexiconOccurrenceResults: null, lexiconOccurrenceLoading: false, lexiconOccurrenceError: "", lexiconOccurrenceVisible: false, lexiconOccurrenceRequestId: 0, globalSearchQuery: "", localSearchQuery: "", activeSearchScope: "global", searchQuery: "", searchResults: null, searchLoading: false, searchError: "", searchRequestId: 0, highlightedVerseId: "", displayPreferencesBySource: {}, showVerseHeads: readStoredBoolean(STORAGE_KEYS.showVerseHeads, true) }; let sourceListEl; let sourceCountEl; let globalSearchFormEl; let globalSearchInputEl; let globalSearchClearEl; let localSearchFormEl; let localSearchInputEl; let localSearchClearEl; let translationSelectEl; let translationControlEl; let compareSelectEl; let compareControlEl; let compareToggleEl; let compareToggleControlEl; let workSelectEl; let sectionSelectEl; let detailHeadingEl; let detailNameEl; let detailSubEl; let detailHeadingToolsEl; let detailBodyEl; let textLayoutEl; let showVerseHeadsEl; let lexiconPopupEl; let lexiconPopupTitleEl; let lexiconPopupSubtitleEl; let lexiconPopupBodyEl; let lexiconPopupCloseEl; let lexiconReturnFocusEl = null; function normalizeId(value) { return String(value || "") .trim() .toLowerCase(); } function getElements() { sourceListEl = document.getElementById("alpha-text-source-list"); sourceCountEl = document.getElementById("alpha-text-source-count"); globalSearchFormEl = document.getElementById("alpha-text-global-search-form"); globalSearchInputEl = document.getElementById("alpha-text-global-search-input"); globalSearchClearEl = document.getElementById("alpha-text-global-search-clear"); localSearchFormEl = document.getElementById("alpha-text-local-search-form"); localSearchInputEl = document.getElementById("alpha-text-local-search-input"); localSearchClearEl = document.getElementById("alpha-text-local-search-clear"); showVerseHeadsEl = document.getElementById("alpha-text-show-verse-heads"); translationSelectEl = document.getElementById("alpha-text-translation-select"); translationControlEl = translationSelectEl?.closest?.(".alpha-text-control") || null; compareSelectEl = document.getElementById("alpha-text-compare-select"); compareControlEl = compareSelectEl?.closest?.(".alpha-text-control") || null; compareToggleEl = document.getElementById("alpha-text-compare-toggle"); compareToggleControlEl = document.getElementById("alpha-text-compare-toggle-control"); workSelectEl = document.getElementById("alpha-text-work-select"); sectionSelectEl = document.getElementById("alpha-text-section-select"); detailHeadingEl = document.querySelector("#alphabet-text-section .alpha-text-detail-heading"); detailNameEl = document.getElementById("alpha-text-detail-name"); detailSubEl = document.getElementById("alpha-text-detail-sub"); detailHeadingToolsEl = document.querySelector("#alphabet-text-section .alpha-text-heading-tools"); detailBodyEl = document.getElementById("alpha-text-detail-body"); textLayoutEl = sourceListEl?.closest?.(".planet-layout") || detailBodyEl?.closest?.(".planet-layout") || null; syncReaderDisplayControls(); ensureLexiconPopup(); } function syncReaderDisplayControls() { if (showVerseHeadsEl instanceof HTMLInputElement) { showVerseHeadsEl.checked = Boolean(state.showVerseHeads); } if (textLayoutEl instanceof HTMLElement) { textLayoutEl.classList.toggle("alpha-text-hide-verse-heads", !state.showVerseHeads); textLayoutEl.setAttribute("data-show-verse-heads", state.showVerseHeads ? "true" : "false"); } } function setGlobalSearchHeadingMode(isGlobalSearchOnly) { if (textLayoutEl instanceof HTMLElement) { textLayoutEl.classList.toggle("alpha-text-global-search-only", Boolean(isGlobalSearchOnly)); textLayoutEl.setAttribute("data-global-search-only", isGlobalSearchOnly ? "true" : "false"); } if (detailHeadingEl instanceof HTMLElement) { detailHeadingEl.hidden = Boolean(isGlobalSearchOnly); detailHeadingEl.setAttribute("aria-hidden", isGlobalSearchOnly ? "true" : "false"); } if (!(detailHeadingToolsEl instanceof HTMLElement)) { return; } detailHeadingToolsEl.hidden = Boolean(isGlobalSearchOnly); detailHeadingToolsEl.setAttribute("aria-hidden", isGlobalSearchOnly ? "true" : "false"); } function showDetailOnlyMode() { if (!(textLayoutEl instanceof HTMLElement)) { return; } window.TarotChromeUi?.initializeSidebarPopouts?.(); window.TarotChromeUi?.initializeDetailPopouts?.(); window.TarotChromeUi?.initializeSidebarAutoCollapse?.(); window.TarotChromeUi?.showDetailOnly?.(textLayoutEl); } function showSidebarOnlyMode(persist = false) { if (!(textLayoutEl instanceof HTMLElement)) { return; } window.TarotChromeUi?.initializeSidebarPopouts?.(); window.TarotChromeUi?.initializeDetailPopouts?.(); window.TarotChromeUi?.showSidebarOnly?.(textLayoutEl, persist); } function ensureLexiconPopup() { if (lexiconPopupEl instanceof HTMLElement) { return; } const popup = document.createElement("div"); popup.className = "alpha-text-lexicon-popup"; popup.hidden = true; popup.setAttribute("aria-hidden", "true"); const backdrop = document.createElement("div"); backdrop.className = "alpha-text-lexicon-popup-backdrop"; backdrop.addEventListener("click", closeLexiconEntry); const card = document.createElement("section"); card.className = "alpha-text-lexicon-popup-card"; card.setAttribute("role", "dialog"); card.setAttribute("aria-modal", "true"); card.setAttribute("aria-labelledby", "alpha-text-lexicon-popup-title"); card.setAttribute("tabindex", "-1"); const header = document.createElement("div"); header.className = "alpha-text-lexicon-popup-header"; const headingWrap = document.createElement("div"); headingWrap.className = "alpha-text-lexicon-popup-heading"; const title = document.createElement("h3"); title.id = "alpha-text-lexicon-popup-title"; title.textContent = "Lexicon Entry"; const subtitle = document.createElement("p"); subtitle.className = "alpha-text-lexicon-popup-subtitle"; subtitle.textContent = "Strong's definition"; headingWrap.append(title, subtitle); const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.className = "alpha-text-lexicon-popup-close"; closeButton.textContent = "Close"; closeButton.addEventListener("click", closeLexiconEntry); header.append(headingWrap, closeButton); const body = document.createElement("div"); body.className = "alpha-text-lexicon-popup-body"; card.append(header, body); popup.append(backdrop, card); document.body.appendChild(popup); lexiconPopupEl = popup; lexiconPopupTitleEl = title; lexiconPopupSubtitleEl = subtitle; lexiconPopupBodyEl = body; lexiconPopupCloseEl = closeButton; } function getSources() { return Array.isArray(state.catalog?.sources) ? state.catalog.sources : []; } function getSourceGroupId(source) { const metadata = getSourceMetadata(source); return normalizeId(metadata.workKey || source?.id || source?.title); } function buildSourceGroups(sources) { const groupsById = new Map(); (Array.isArray(sources) ? sources : []).forEach((source, index) => { const groupId = getSourceGroupId(source) || `source-group-${index + 1}`; if (!groupsById.has(groupId)) { groupsById.set(groupId, { id: groupId, title: normalizeTextValue(source?.title) || normalizeTextValue(source?.shortTitle) || "Untitled Source", order: index, variants: [] }); } groupsById.get(groupId).variants.push(source); }); return [...groupsById.values()].sort((left, right) => left.order - right.order); } function getSourceGroups() { return Array.isArray(state.catalog?.sourceGroups) ? state.catalog.sourceGroups : []; } function findById(entries, value) { const needle = normalizeId(value); return (Array.isArray(entries) ? entries : []).find((entry) => normalizeId(entry?.id) === needle) || null; } function getSelectedSourceGroup() { return findById(getSourceGroups(), state.selectedSourceGroupId); } function getSourceVariants(group = getSelectedSourceGroup()) { return Array.isArray(group?.variants) ? group.variants : []; } function getSourceForGroup(group = getSelectedSourceGroup(), sourceId = state.selectedSourceId) { return findById(getSourceVariants(group), sourceId) || getSourceVariants(group)[0] || null; } function findSourceGroupBySourceId(sourceId) { const needle = normalizeId(sourceId); return getSourceGroups().find((group) => getSourceVariants(group).some((source) => normalizeId(source?.id) === needle)) || null; } function rememberSelectedSource(group, sourceId) { const groupId = normalizeId(group?.id); const normalizedSourceId = normalizeTextValue(sourceId); if (!groupId || !normalizedSourceId) { return; } state.selectedSourceIdByGroup[groupId] = normalizedSourceId; } function rememberCompareSource(group, sourceId) { const groupId = normalizeId(group?.id); const normalizedSourceId = normalizeTextValue(sourceId); if (!groupId || !normalizedSourceId) { return; } state.compareSourceIdByGroup[groupId] = normalizedSourceId; } function isCompareAvailable(group = getSelectedSourceGroup()) { return getSourceVariants(group).length > 1; } function isCompareModeEnabled(group = getSelectedSourceGroup()) { const groupId = normalizeId(group?.id); return Boolean(groupId && state.compareModeByGroup[groupId] && isCompareAvailable(group)); } function setCompareModeEnabled(group, isEnabled) { const groupId = normalizeId(group?.id); if (!groupId) { return; } state.compareModeByGroup[groupId] = Boolean(isEnabled); } function getCompareCandidates(group = getSelectedSourceGroup()) { const activeSourceId = normalizeId(state.selectedSourceId); return getSourceVariants(group).filter((source) => normalizeId(source?.id) !== activeSourceId); } function getCompareSource(group = getSelectedSourceGroup()) { const groupId = normalizeId(group?.id); const candidates = getCompareCandidates(group); const rememberedSourceId = groupId ? state.compareSourceIdByGroup[groupId] : ""; return findById(candidates, rememberedSourceId) || candidates[0] || null; } function syncCompareSelection(group = getSelectedSourceGroup()) { const groupId = normalizeId(group?.id); if (!groupId) { return; } if (!isCompareAvailable(group)) { delete state.compareSourceIdByGroup[groupId]; delete state.compareModeByGroup[groupId]; return; } const compareSource = getCompareSource(group); if (compareSource?.id) { rememberCompareSource(group, compareSource.id); } } function getSelectedSource() { return getSourceForGroup(getSelectedSourceGroup(), state.selectedSourceId) || findById(getSources(), state.selectedSourceId); } function getSelectedWork(source = getSelectedSource()) { return findById(source?.works, state.selectedWorkId); } function getSelectedSection(source = getSelectedSource(), work = getSelectedWork(source)) { return findById(work?.sections, state.selectedSectionId); } function normalizeTextValue(value) { return String(value || "").trim(); } function buildTranslationOptionLabel(source) { const metadata = getSourceMetadata(source); return normalizeTextValue(metadata.translator) || normalizeTextValue(metadata.versionLabel || metadata.version) || normalizeTextValue(source?.shortTitle) || normalizeTextValue(source?.title) || "Translation"; } function getSourceMetadata(source) { return source?.metadata && typeof source.metadata === "object" ? source.metadata : {}; } function includesNormalizedText(container, value) { const containerText = normalizeTextValue(container).toLowerCase(); const valueText = normalizeTextValue(value).toLowerCase(); return Boolean(containerText && valueText && containerText.includes(valueText)); } function formatCountLabel(count, label) { const normalizedCount = Number(count) || 0; const baseLabel = normalizeTextValue(label) || "item"; if (normalizedCount === 1) { return `${normalizedCount} ${baseLabel}`; } if (/[^aeiou]y$/i.test(baseLabel)) { return `${normalizedCount} ${baseLabel.slice(0, -1)}ies`; } return `${normalizedCount} ${baseLabel.endsWith("s") ? baseLabel : `${baseLabel}s`}`; } function getSourceEditionLabel(source) { const metadata = getSourceMetadata(source); const version = normalizeTextValue(metadata.versionLabel || metadata.version); const translator = normalizeTextValue(metadata.translator); if ( version && translator && normalizeId(version) !== normalizeId(translator) && !includesNormalizedText(version, translator) && !includesNormalizedText(translator, version) ) { return `${version} · ${translator}`; } return version || translator; } function buildSourceListMeta(source) { const shortTitle = normalizeTextValue(source?.shortTitle); const title = normalizeTextValue(source?.title); const editionLabel = getSourceEditionLabel(source); const parts = []; if (shortTitle && normalizeId(shortTitle) !== normalizeId(title)) { parts.push(shortTitle); } if (editionLabel && !parts.some((part) => includesNormalizedText(part, editionLabel) || includesNormalizedText(editionLabel, part))) { parts.push(editionLabel); } parts.push(formatCountLabel(source?.stats?.workCount, source?.workLabel || "Work")); parts.push(formatCountLabel(source?.stats?.sectionCount, source?.sectionLabel || "Section")); return parts.join(" · "); } function buildSourceGroupListMeta(group) { const activeSource = getSourceForGroup(group); if (!group || getSourceVariants(group).length <= 1) { return buildSourceListMeta(activeSource); } const translators = Array.from(new Set( getSourceVariants(group) .map((source) => normalizeTextValue(getSourceMetadata(source).translator)) .filter(Boolean) )); const parts = []; if (translators.length) { parts.push(translators.join(" / ")); } parts.push(formatCountLabel(getSourceVariants(group).length, "translation")); parts.push(formatCountLabel(activeSource?.stats?.sectionCount, activeSource?.sectionLabel || "Section")); return parts.join(" · "); } function buildSourceDetailSubtitle(source, work) { const parts = [normalizeTextValue(source?.title) || "--"]; const editionLabel = getSourceEditionLabel(source); const workTitle = normalizeTextValue(work?.title); if (editionLabel) { parts.push(editionLabel); } if (workTitle && normalizeId(workTitle) !== normalizeId(source?.title)) { parts.push(workTitle); } return parts.join(" · "); } function buildCompareCardTitle(passage) { const source = passage?.source || getSelectedSource(); const section = passage?.section || getSelectedSection(source, getSelectedWork(source)); return `${buildTranslationOptionLabel(source)} · ${section?.title || section?.label || "--"}`; } function extractVerseCountText(verse, source, displayPreferences, translationText = "") { const mode = displayPreferences?.textMode || "translation"; const originalText = normalizeTextValue(verse?.originalText); const transliterationText = getVerseTransliteration(verse, source); if (mode === "original") { return originalText || normalizeTextValue(translationText); } if (mode === "transliteration") { return transliterationText || normalizeTextValue(translationText); } return normalizeTextValue(translationText) || originalText || transliterationText; } function getTextCounts(value) { const normalized = String(value || "") .normalize("NFD") .replace(/[\u0300-\u036f]/g, ""); const words = normalized.match(/[\p{L}\p{N}]+(?:['’-][\p{L}\p{N}]+)*/gu) || []; const letters = normalized.match(/\p{L}/gu) || []; const vowels = normalized.match(/[AEIOUYaeiouy]/g) || []; const consonants = letters.length - vowels.length; return { words: words.length, letters: letters.length, consonants: Math.max(0, consonants), vowels: vowels.length }; } function formatCountSummary(counts) { return `W:${counts.words} L:${counts.letters} C:${counts.consonants} V:${counts.vowels}`; } function sumPassageCounts(passage, source, displayPreferences) { const verses = Array.isArray(passage?.verses) ? passage.verses : []; return verses.reduce((totals, verse) => { const translationText = source?.features?.hasTokenAnnotations ? buildTokenTranslationText(verse?.tokens, verse?.text) : verse?.text; const counts = getTextCounts(extractVerseCountText(verse, source, displayPreferences, translationText)); totals.words += counts.words; totals.letters += counts.letters; totals.consonants += counts.consonants; totals.vowels += counts.vowels; return totals; }, { words: 0, letters: 0, consonants: 0, vowels: 0 }); } const GREEK_TRANSLITERATION_MAP = { α: "a", β: "b", γ: "g", δ: "d", ε: "e", ζ: "z", η: "e", θ: "th", ι: "i", κ: "k", λ: "l", μ: "m", ν: "n", ξ: "x", ο: "o", π: "p", ρ: "r", σ: "s", ς: "s", τ: "t", υ: "u", φ: "ph", χ: "ch", ψ: "ps", ω: "o" }; const HEBREW_TRANSLITERATION_MAP = { א: "a", ב: "b", ג: "g", ד: "d", ה: "h", ו: "v", ז: "z", ח: "ch", ט: "t", י: "y", כ: "k", ך: "k", ל: "l", מ: "m", ם: "m", נ: "n", ן: "n", ס: "s", ע: "a", פ: "p", ף: "p", צ: "ts", ץ: "ts", ק: "q", ר: "r", ש: "sh", ת: "t" }; function stripSourceScriptMarks(value) { return String(value || "") .normalize("NFD") .replace(/[\u0300-\u036f\u0591-\u05c7]/g, ""); } function transliterateSourceScriptText(value) { const stripped = stripSourceScriptMarks(value); let result = ""; for (const character of stripped) { const lowerCharacter = character.toLowerCase(); const mapped = GREEK_TRANSLITERATION_MAP[lowerCharacter] || HEBREW_TRANSLITERATION_MAP[character] || HEBREW_TRANSLITERATION_MAP[lowerCharacter]; result += mapped != null ? mapped : character; } return result .replace(/\s+([,.;:!?])/g, "$1") .replace(/\s+/g, " ") .trim(); } function buildTokenDerivedTransliteration(verse) { const tokenText = (Array.isArray(verse?.tokens) ? verse.tokens : []) .map((token) => normalizeTextValue(token?.original)) .filter(Boolean) .join(" ") .replace(/\s+([,.;:!?])/g, "$1") .trim(); return tokenText ? transliterateSourceScriptText(tokenText) : ""; } function getVerseTransliteration(verse, source = null) { const metadata = verse?.metadata && typeof verse.metadata === "object" ? verse.metadata : {}; const explicit = [ verse?.transliteration, verse?.xlit, metadata?.transliteration, metadata?.transliterationText, metadata?.xlit, metadata?.romanizedText, metadata?.romanized ].map(normalizeTextValue).find(Boolean) || ""; if (explicit) { return explicit; } if (source?.features?.hasTokenAnnotations) { return buildTokenDerivedTransliteration(verse); } return ""; } function getSearchInput(scope) { return scope === "source" ? localSearchInputEl : globalSearchInputEl; } function getStoredSearchQuery(scope) { return scope === "source" ? state.localSearchQuery : state.globalSearchQuery; } function setStoredSearchQuery(scope, value) { if (scope === "source") { state.localSearchQuery = value; return; } state.globalSearchQuery = value; } function updateSearchControls() { const globalQuery = String(globalSearchInputEl?.value || state.globalSearchQuery || "").trim(); const localQuery = String(localSearchInputEl?.value || state.localSearchQuery || "").trim(); const hasGlobalSearch = Boolean(globalQuery) || (state.activeSearchScope === "global" && Boolean(state.searchQuery)); const hasLocalSearch = Boolean(localQuery) || (state.activeSearchScope === "source" && Boolean(state.searchQuery)); if (globalSearchClearEl instanceof HTMLButtonElement) { globalSearchClearEl.disabled = !hasGlobalSearch; } if (localSearchClearEl instanceof HTMLButtonElement) { localSearchClearEl.disabled = !hasLocalSearch; } } function clearActiveSearchUi(options = {}) { const preserveHighlight = options.preserveHighlight === true; const scope = state.activeSearchScope === "source" ? "source" : "global"; setStoredSearchQuery(scope, ""); const input = getSearchInput(scope); if (input instanceof HTMLInputElement) { input.value = ""; } state.searchQuery = ""; state.searchResults = null; state.searchLoading = false; state.searchError = ""; state.searchRequestId += 1; if (!preserveHighlight) { state.highlightedVerseId = ""; } updateSearchControls(); } function getSourceDisplayCapabilities(source, passage) { const verses = Array.isArray(passage?.verses) ? passage.verses : []; const hasOriginal = verses.some((verse) => normalizeTextValue(verse?.originalText)); const hasTransliteration = verses.some((verse) => getVerseTransliteration(verse, source)); const hasInterlinear = Boolean(source?.features?.hasTokenAnnotations); const textModeCount = 1 + (hasOriginal ? 1 : 0) + (hasTransliteration ? 1 : 0); return { hasTranslation: true, hasOriginal, hasTransliteration, hasInterlinear, hasAnyExtras: hasOriginal || hasTransliteration || hasInterlinear, supportsAllTextMode: textModeCount > 1 }; } function getDefaultTextDisplayMode(capabilities) { if (capabilities?.hasTranslation) { return "translation"; } if (capabilities?.hasOriginal) { return "original"; } if (capabilities?.hasTransliteration) { return "transliteration"; } return "translation"; } function getAvailableTextDisplayModes(capabilities) { const modes = []; if (capabilities?.hasTranslation) { modes.push("translation"); } if (capabilities?.hasOriginal) { modes.push("original"); } if (capabilities?.hasTransliteration) { modes.push("transliteration"); } if (capabilities?.supportsAllTextMode) { modes.push("all"); } return modes; } function getSourceDisplayPreferences(source, passage) { const sourceId = normalizeId(source?.id); const capabilities = getSourceDisplayCapabilities(source, passage); const availableTextModes = getAvailableTextDisplayModes(capabilities); const stored = sourceId ? state.displayPreferencesBySource[sourceId] : null; let textMode = stored?.textMode; if (!availableTextModes.includes(textMode)) { textMode = getDefaultTextDisplayMode(capabilities); } const preferences = { textMode, showInterlinear: capabilities.hasInterlinear ? Boolean(stored?.showInterlinear) : false }; if (sourceId) { state.displayPreferencesBySource[sourceId] = preferences; } return { ...preferences, capabilities, availableTextModes }; } function updateSourceDisplayPreferences(source, patch) { const sourceId = normalizeId(source?.id); if (!sourceId) { return; } const current = state.displayPreferencesBySource[sourceId] || {}; state.displayPreferencesBySource[sourceId] = { ...current, ...patch }; } function formatTextDisplayModeLabel(mode) { switch (mode) { case "translation": return "Translation"; case "original": return "Original"; case "transliteration": return "Transliteration"; case "all": return "All"; default: return "Display"; } } function clearSearchState() { state.searchQuery = ""; state.searchResults = null; state.searchLoading = false; state.searchError = ""; state.highlightedVerseId = ""; state.searchRequestId += 1; updateSearchControls(); } function clearScopedSearch(scope) { setStoredSearchQuery(scope, ""); const input = getSearchInput(scope); if (input instanceof HTMLInputElement) { input.value = ""; } if (state.activeSearchScope === scope) { clearSearchState(); } else { updateSearchControls(); } } function escapeRegExp(value) { return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function buildWholeWordMatcher(query, flags = "iu") { const normalizedQuery = String(query || "").trim(); if (!normalizedQuery) { return null; } return new RegExp(`(^|[^\\p{L}\\p{N}])(${escapeRegExp(normalizedQuery)})(?=$|[^\\p{L}\\p{N}])`, flags); } function appendHighlightedText(target, text, query) { if (!(target instanceof HTMLElement)) { return; } const sourceText = String(text || ""); const normalizedQuery = String(query || "").trim(); target.replaceChildren(); if (!normalizedQuery) { target.textContent = sourceText; return; } const matcher = buildWholeWordMatcher(normalizedQuery, "giu"); if (!matcher) { target.textContent = sourceText; return; } let lastIndex = 0; let match = matcher.exec(sourceText); while (match) { const prefixLength = String(match[1] || "").length; const matchedText = String(match[2] || ""); const matchStart = match.index + prefixLength; const matchEnd = matchStart + matchedText.length; if (matchStart > lastIndex) { target.appendChild(document.createTextNode(sourceText.slice(lastIndex, matchStart))); } const mark = document.createElement("mark"); mark.className = "alpha-text-mark"; mark.textContent = sourceText.slice(matchStart, matchEnd); target.appendChild(mark); lastIndex = matchEnd; match = matcher.exec(sourceText); } if (lastIndex < sourceText.length) { target.appendChild(document.createTextNode(sourceText.slice(lastIndex))); } } function isHighlightedVerse(verse) { return normalizeId(verse?.id) && normalizeId(verse?.id) === normalizeId(state.highlightedVerseId); } function scrollHighlightedVerseIntoView() { const highlightedVerse = detailBodyEl?.querySelector?.(".alpha-text-verse.is-highlighted"); const detailPanel = highlightedVerse?.closest?.(".planet-detail-panel"); if (!(highlightedVerse instanceof HTMLElement) || !(detailPanel instanceof HTMLElement)) { return; } const verseRect = highlightedVerse.getBoundingClientRect(); const panelRect = detailPanel.getBoundingClientRect(); const targetTop = detailPanel.scrollTop + (verseRect.top - panelRect.top) - (detailPanel.clientHeight / 2) + (verseRect.height / 2); detailPanel.scrollTo({ top: Math.max(0, targetTop), behavior: "smooth" }); } function createCard(title) { const card = document.createElement("div"); card.className = "detail-meta-card planet-meta-card"; if (title) { const heading = document.createElement("strong"); heading.textContent = title; card.appendChild(heading); } return card; } function createEmptyMessage(text) { const message = document.createElement("div"); message.className = "alpha-text-empty"; message.textContent = text; return message; } function renderPlaceholder(title, subtitle, message) { if (detailNameEl) { detailNameEl.textContent = title; } if (detailSubEl) { detailSubEl.textContent = subtitle; } if (!detailBodyEl) { return; } detailBodyEl.replaceChildren(); const card = createCard("Text Reader"); card.appendChild(createEmptyMessage(message)); detailBodyEl.appendChild(card); } function navigateToPassageTarget(target) { if (!target) { return; } state.selectedWorkId = target.workId; state.selectedSectionId = target.sectionId; state.lexiconEntry = null; renderSelectors(); void loadSelectedPassage(); } function getPassageLocationLabel(passage) { const source = passage?.source || getSelectedSource(); const work = passage?.work || getSelectedWork(source); const section = passage?.section || getSelectedSection(source, work); return `${work?.title || "--"} · ${section?.title || section?.label || "--"}`; } function syncSelectionForSource(source) { const works = Array.isArray(source?.works) ? source.works : []; if (!works.length) { state.selectedWorkId = ""; state.selectedSectionId = ""; return; } if (!findById(works, state.selectedWorkId)) { state.selectedWorkId = works[0].id; } const work = getSelectedWork(source); const sections = Array.isArray(work?.sections) ? work.sections : []; if (!findById(sections, state.selectedSectionId)) { state.selectedSectionId = sections[0]?.id || ""; } } function syncSelectionForGroup(group = getSelectedSourceGroup()) { const variants = getSourceVariants(group); if (!variants.length) { state.selectedSourceGroupId = ""; state.selectedSourceId = ""; state.selectedWorkId = ""; state.selectedSectionId = ""; return; } state.selectedSourceGroupId = group.id; const rememberedSourceId = state.selectedSourceIdByGroup[normalizeId(group.id)] || ""; const source = findById(variants, state.selectedSourceId) || findById(variants, rememberedSourceId) || variants[0]; state.selectedSourceId = source?.id || ""; rememberSelectedSource(group, state.selectedSourceId); syncSelectionForSource(source); syncCompareSelection(group); } async function ensureCatalogLoaded(forceRefresh = false) { if (!forceRefresh && state.catalog) { return state.catalog; } const payload = await dataService.loadTextLibrary?.(forceRefresh); state.catalog = payload && typeof payload === "object" ? payload : { meta: {}, sources: [], lexicons: [] }; state.catalog.sourceGroups = buildSourceGroups(getSources()); if (!state.selectedSourceGroupId && state.selectedSourceId) { state.selectedSourceGroupId = findSourceGroupBySourceId(state.selectedSourceId)?.id || ""; } if (!state.selectedSourceGroupId) { state.selectedSourceGroupId = getSourceGroups()[0]?.id || ""; } syncSelectionForGroup(getSelectedSourceGroup()); return state.catalog; } function fillSelect(selectEl, entries, selectedValue, labelBuilder) { if (!(selectEl instanceof HTMLSelectElement)) { return; } selectEl.replaceChildren(); (Array.isArray(entries) ? entries : []).forEach((entry) => { const option = document.createElement("option"); option.value = entry.id; option.textContent = typeof labelBuilder === "function" ? labelBuilder(entry) : String(entry?.label || entry?.title || entry?.id || ""); option.selected = normalizeId(entry.id) === normalizeId(selectedValue); selectEl.appendChild(option); }); selectEl.disabled = !selectEl.options.length; } function renderSourceList() { if (!sourceListEl) { return; } sourceListEl.replaceChildren(); const sourceGroups = getSourceGroups(); sourceGroups.forEach((group) => { const source = getSourceForGroup(group); const button = document.createElement("button"); button.type = "button"; button.className = "planet-list-item alpha-text-source-btn"; button.dataset.sourceGroupId = group.id; button.setAttribute("role", "option"); const isSelected = normalizeId(group.id) === normalizeId(state.selectedSourceGroupId); button.classList.toggle("is-selected", isSelected); button.setAttribute("aria-selected", isSelected ? "true" : "false"); const name = document.createElement("span"); name.className = "planet-list-name"; name.textContent = group.title; const meta = document.createElement("span"); meta.className = "alpha-text-source-meta"; meta.textContent = buildSourceGroupListMeta(group); button.append(name, meta); button.addEventListener("click", () => { if (normalizeId(group.id) === normalizeId(state.selectedSourceGroupId)) { showDetailOnlyMode(); return; } state.selectedSourceGroupId = group.id; state.currentPassage = null; state.lexiconEntry = null; state.highlightedVerseId = ""; syncSelectionForGroup(group); renderSourceList(); renderSelectors(); showDetailOnlyMode(); if (state.searchQuery && state.activeSearchScope === "source") { void Promise.all([loadSelectedPassage(), runSearch("source")]); return; } void loadSelectedPassage(); }); sourceListEl.appendChild(button); }); if (!sourceGroups.length) { sourceListEl.appendChild(createEmptyMessage("No text sources are available.")); } if (sourceCountEl) { sourceCountEl.textContent = `${sourceGroups.length} sources`; } } function renderSelectors() { const group = getSelectedSourceGroup(); const source = getSelectedSource(); const work = getSelectedWork(source); const variants = getSourceVariants(group); const compareCandidates = getCompareCandidates(group); const compareSource = getCompareSource(group); const compareEnabled = isCompareModeEnabled(group); const works = Array.isArray(source?.works) ? source.works : []; const sections = Array.isArray(work?.sections) ? work.sections : []; fillSelect(translationSelectEl, variants, state.selectedSourceId, (entry) => buildTranslationOptionLabel(entry)); fillSelect(compareSelectEl, compareCandidates, compareSource?.id || "", (entry) => buildTranslationOptionLabel(entry)); fillSelect(workSelectEl, works, state.selectedWorkId, (entry) => `${entry.title} (${formatCountLabel(entry.sectionCount, String(source?.sectionLabel || "section").toLowerCase())})`); fillSelect(sectionSelectEl, sections, state.selectedSectionId, (entry) => `${entry.label} · ${entry.verseCount} verses`); if (translationSelectEl instanceof HTMLSelectElement) { translationSelectEl.disabled = variants.length <= 1; } if (translationControlEl instanceof HTMLElement) { translationControlEl.hidden = variants.length <= 1; } if (compareToggleEl instanceof HTMLButtonElement) { compareToggleEl.textContent = compareEnabled ? "On" : "Off"; compareToggleEl.setAttribute("aria-pressed", compareEnabled ? "true" : "false"); compareToggleEl.classList.toggle("is-selected", compareEnabled); } if (compareToggleControlEl instanceof HTMLElement) { compareToggleControlEl.hidden = !isCompareAvailable(group); } if (compareSelectEl instanceof HTMLSelectElement) { compareSelectEl.disabled = !compareEnabled || compareCandidates.length === 0; } if (compareControlEl instanceof HTMLElement) { compareControlEl.hidden = !compareEnabled || compareCandidates.length === 0; } } function closeLexiconEntry() { dismissLexiconEntry(); } function clearLexiconOccurrenceState() { state.lexiconOccurrenceResults = null; state.lexiconOccurrenceLoading = false; state.lexiconOccurrenceError = ""; state.lexiconOccurrenceVisible = false; state.lexiconOccurrenceRequestId += 1; } function dismissLexiconEntry(options = {}) { const shouldRestoreFocus = options.restoreFocus !== false; state.lexiconRequestId += 1; state.lexiconEntry = null; clearLexiconOccurrenceState(); renderLexiconPopup(); const returnFocusEl = lexiconReturnFocusEl; lexiconReturnFocusEl = null; if (shouldRestoreFocus && returnFocusEl instanceof HTMLElement && returnFocusEl.isConnected) { requestAnimationFrame(() => { if (returnFocusEl.isConnected) { returnFocusEl.focus(); } }); } } async function toggleLexiconOccurrences() { const lexiconId = state.lexiconEntry?.lexicon?.id || state.lexiconEntry?.lexiconId || ""; const entryId = state.lexiconEntry?.entryId || ""; if (!lexiconId || !entryId) { return; } if (state.lexiconOccurrenceVisible && !state.lexiconOccurrenceLoading) { state.lexiconOccurrenceVisible = false; renderLexiconPopup(); return; } state.lexiconOccurrenceVisible = true; if (state.lexiconOccurrenceResults || state.lexiconOccurrenceError) { renderLexiconPopup(); return; } const requestId = state.lexiconOccurrenceRequestId + 1; state.lexiconOccurrenceRequestId = requestId; state.lexiconOccurrenceLoading = true; state.lexiconOccurrenceError = ""; renderLexiconPopup(); try { const payload = await dataService.loadTextLexiconOccurrences?.(lexiconId, entryId, { limit: 100 }); if (requestId !== state.lexiconOccurrenceRequestId) { return; } state.lexiconOccurrenceResults = payload; state.lexiconOccurrenceLoading = false; renderLexiconPopup(); } catch (error) { if (requestId !== state.lexiconOccurrenceRequestId) { return; } state.lexiconOccurrenceLoading = false; state.lexiconOccurrenceError = error?.message || "Unable to load verse occurrences for this Strong's entry."; renderLexiconPopup(); } } async function openLexiconOccurrence(result) { dismissLexiconEntry({ restoreFocus: false }); await openSearchResult(result); } function appendLexiconOccurrencePreview(target, result) { if (!(target instanceof HTMLElement)) { return; } target.replaceChildren(); const previewTokens = Array.isArray(result?.previewTokens) ? result.previewTokens : []; if (!previewTokens.length) { target.textContent = result?.preview || result?.reference || ""; return; } previewTokens.forEach((token, index) => { const text = String(token?.text || "").trim(); if (!text) { return; } const previousText = String(previewTokens[index - 1]?.text || "").trim(); if (index > 0 && text !== "..." && previousText !== "...") { target.appendChild(document.createTextNode(" ")); } if (token?.isMatch) { const mark = document.createElement("mark"); mark.className = "alpha-text-mark alpha-text-mark--lexicon"; mark.textContent = text; target.appendChild(mark); return; } target.appendChild(document.createTextNode(text)); }); } function renderLexiconPopup() { ensureLexiconPopup(); if (!(lexiconPopupEl instanceof HTMLElement) || !(lexiconPopupBodyEl instanceof HTMLElement)) { return; } const payload = state.lexiconEntry; const wasHidden = lexiconPopupEl.hidden; if (!payload) { lexiconPopupEl.hidden = true; lexiconPopupEl.setAttribute("aria-hidden", "true"); lexiconPopupTitleEl.textContent = "Lexicon Entry"; lexiconPopupSubtitleEl.textContent = "Strong's definition"; lexiconPopupBodyEl.replaceChildren(); return; } lexiconPopupTitleEl.textContent = payload.entryId ? `Strong's ${payload.entryId}` : "Lexicon Entry"; lexiconPopupSubtitleEl.textContent = payload.loading ? "Loading definition..." : "Strong's definition"; lexiconPopupBodyEl.replaceChildren(); if (payload.loading) { lexiconPopupBodyEl.appendChild(createEmptyMessage(`Loading ${payload.entryId}...`)); } else if (payload.error) { lexiconPopupBodyEl.appendChild(createEmptyMessage(payload.error)); } else { const entry = payload.entry || {}; const head = document.createElement("div"); head.className = "alpha-text-lexicon-head"; const idPill = document.createElement("button"); idPill.type = "button"; idPill.className = "alpha-text-lexicon-id alpha-text-lexicon-id--button"; idPill.textContent = payload.entryId || "--"; idPill.setAttribute("aria-expanded", state.lexiconOccurrenceVisible ? "true" : "false"); idPill.addEventListener("click", () => { void toggleLexiconOccurrences(); }); head.appendChild(idPill); if (entry.lemma) { const lemma = document.createElement("span"); lemma.className = "alpha-text-token-original"; lemma.textContent = entry.lemma; head.appendChild(lemma); } lexiconPopupBodyEl.appendChild(head); const rows = [ ["Transliteration", entry.xlit], ["Pronunciation", entry.pron], ["Derivation", entry.derivation], ["Strong's Definition", entry.strongs_def], ["KJV Definition", entry.kjv_def] ].filter(([, value]) => String(value || "").trim()); if (rows.length) { const dl = document.createElement("dl"); dl.className = "alpha-dl"; rows.forEach(([label, value]) => { const dt = document.createElement("dt"); dt.textContent = label; const dd = document.createElement("dd"); dd.textContent = String(value || "").trim(); dl.append(dt, dd); }); lexiconPopupBodyEl.appendChild(dl); } const occurrenceHint = document.createElement("p"); occurrenceHint.className = "alpha-text-lexicon-hint"; occurrenceHint.textContent = "Click the Strong's number to show verses that use this entry."; lexiconPopupBodyEl.appendChild(occurrenceHint); if (state.lexiconOccurrenceVisible) { const occurrenceSection = document.createElement("section"); occurrenceSection.className = "alpha-text-lexicon-occurrences"; const occurrenceTitle = document.createElement("strong"); occurrenceTitle.textContent = "Verse Occurrences"; occurrenceSection.appendChild(occurrenceTitle); if (state.lexiconOccurrenceLoading) { occurrenceSection.appendChild(createEmptyMessage(`Loading verses for ${payload.entryId}...`)); } else if (state.lexiconOccurrenceError) { occurrenceSection.appendChild(createEmptyMessage(state.lexiconOccurrenceError)); } else { const occurrencePayload = state.lexiconOccurrenceResults; const totalMatches = Number(occurrencePayload?.totalMatches) || 0; const summary = document.createElement("p"); summary.className = "alpha-text-search-summary"; summary.textContent = totalMatches ? `${totalMatches} verses use ${payload.entryId}.${occurrencePayload?.truncated ? ` Showing the first ${occurrencePayload.resultCount} results.` : ""}` : `No verses found for ${payload.entryId}.`; occurrenceSection.appendChild(summary); if (Array.isArray(occurrencePayload?.results) && occurrencePayload.results.length) { const occurrenceList = document.createElement("div"); occurrenceList.className = "alpha-text-lexicon-occurrence-list"; occurrencePayload.results.forEach((result) => { const button = document.createElement("button"); button.type = "button"; button.className = "alpha-text-lexicon-occurrence"; const headRow = document.createElement("div"); headRow.className = "alpha-text-search-result-head"; const reference = document.createElement("span"); reference.className = "alpha-text-search-reference"; reference.textContent = result.reference || `${result.workTitle} ${result.sectionLabel}:${result.verseNumber}`; const location = document.createElement("span"); location.className = "alpha-text-search-location"; location.textContent = `${result.sourceShortTitle || result.sourceTitle} · ${result.workTitle} · ${result.sectionLabel}`; const preview = document.createElement("p"); preview.className = "alpha-text-search-preview alpha-text-search-preview--compact"; appendLexiconOccurrencePreview(preview, result); button.addEventListener("click", () => { void openLexiconOccurrence(result); }); headRow.append(reference, location); button.append(headRow, preview); occurrenceList.appendChild(button); }); occurrenceSection.appendChild(occurrenceList); } } lexiconPopupBodyEl.appendChild(occurrenceSection); } } lexiconPopupEl.hidden = false; lexiconPopupEl.setAttribute("aria-hidden", "false"); if (wasHidden && lexiconPopupCloseEl instanceof HTMLButtonElement) { requestAnimationFrame(() => { lexiconPopupCloseEl.focus(); }); } } async function loadLexiconEntry(lexiconId, entryId, triggerElement) { if (!lexiconId || !entryId) { return; } if (triggerElement instanceof HTMLElement) { lexiconReturnFocusEl = triggerElement; } const requestId = state.lexiconRequestId + 1; state.lexiconRequestId = requestId; clearLexiconOccurrenceState(); state.lexiconEntry = { loading: true, lexiconId, entryId: String(entryId).toUpperCase() }; renderDetail(); try { const payload = await dataService.loadTextLexiconEntry?.(lexiconId, entryId); if (requestId !== state.lexiconRequestId) { return; } state.lexiconEntry = payload; renderDetail(); } catch (error) { if (requestId !== state.lexiconRequestId) { return; } state.lexiconEntry = { error: error?.message || "Unable to load lexicon entry.", lexiconId, entryId: String(entryId).toUpperCase() }; renderDetail(); } } function createMetaGrid(passage) { const sourceGroup = getSelectedSourceGroup(); const source = passage?.source || getSelectedSource(); const work = passage?.work || getSelectedWork(source); const section = passage?.section || getSelectedSection(source, work); const metadata = getSourceMetadata(source); const version = normalizeTextValue(metadata.versionLabel || metadata.version); const translator = normalizeTextValue(metadata.translator); const compareSource = getCompareSource(sourceGroup); const displayPreferences = getSourceDisplayPreferences(source, passage); const metaGrid = document.createElement("div"); metaGrid.className = "alpha-text-meta-grid"; const overviewCard = createCard("Source Overview"); overviewCard.innerHTML += `
Source
${source?.title || "--"}
${version ? `
Version
${version}
` : ""} ${translator ? `
Translator
${translator}
` : ""} ${getSourceVariants(sourceGroup).length > 1 ? `
Translations
${getSourceVariants(sourceGroup).map((entry) => buildTranslationOptionLabel(entry)).join(" / ")}
` : ""} ${isCompareModeEnabled(sourceGroup) && compareSource ? `
Compare
${buildTranslationOptionLabel(compareSource)}
` : ""}
Tradition
${source?.tradition || "--"}
Language
${source?.language || "--"}
Script
${source?.script || "--"}
${source?.workLabel || "Work"}
${work?.title || "--"}
${source?.sectionLabel || "Section"}
${section?.label || "--"}
`; metaGrid.appendChild(overviewCard); const totalsCard = createCard("Entry Totals"); const totals = sumPassageCounts(passage, source, displayPreferences); totalsCard.innerHTML += `
Words
${totals.words}
Letters
${totals.letters}
Consonants
${totals.consonants}
Vowels
${totals.vowels}
`; metaGrid.appendChild(totalsCard); if (displayPreferences.capabilities.hasAnyExtras) { const extraCard = createCard("Extra"); extraCard.classList.add("alpha-text-extra-card"); if (displayPreferences.availableTextModes.length > 1) { const displayGroup = document.createElement("div"); displayGroup.className = "alpha-text-extra-group"; const displayLabel = document.createElement("span"); displayLabel.className = "alpha-text-extra-label"; displayLabel.textContent = "Display"; const displayButtons = document.createElement("div"); displayButtons.className = "alpha-nav-btns alpha-text-extra-actions"; displayPreferences.availableTextModes.forEach((mode) => { const button = document.createElement("button"); button.type = "button"; button.className = "alpha-nav-btn"; button.textContent = formatTextDisplayModeLabel(mode); button.setAttribute("aria-pressed", displayPreferences.textMode === mode ? "true" : "false"); button.classList.toggle("is-selected", displayPreferences.textMode === mode); button.addEventListener("click", () => { updateSourceDisplayPreferences(source, { textMode: mode }); renderDetail(); }); displayButtons.appendChild(button); }); displayGroup.append(displayLabel, displayButtons); extraCard.appendChild(displayGroup); } if (displayPreferences.capabilities.hasInterlinear) { const interlinearGroup = document.createElement("div"); interlinearGroup.className = "alpha-text-extra-group"; const interlinearLabel = document.createElement("span"); interlinearLabel.className = "alpha-text-extra-label"; interlinearLabel.textContent = "Reader"; const interlinearButtons = document.createElement("div"); interlinearButtons.className = "alpha-nav-btns alpha-text-extra-actions"; const interlinearButton = document.createElement("button"); interlinearButton.type = "button"; interlinearButton.className = "alpha-nav-btn"; interlinearButton.textContent = "Interlinear"; interlinearButton.setAttribute("aria-pressed", displayPreferences.showInterlinear ? "true" : "false"); interlinearButton.classList.toggle("is-selected", displayPreferences.showInterlinear); interlinearButton.addEventListener("click", () => { updateSourceDisplayPreferences(source, { showInterlinear: !displayPreferences.showInterlinear }); renderDetail(); }); interlinearButtons.appendChild(interlinearButton); interlinearGroup.append(interlinearLabel, interlinearButtons); extraCard.appendChild(interlinearGroup); } metaGrid.appendChild(extraCard); } if (source?.features?.hasTokenAnnotations) { const noteCard = createCard("Reader Mode"); noteCard.appendChild(createEmptyMessage("This source is tokenized. Click a Strong's code chip to open its lexicon entry.")); metaGrid.appendChild(noteCard); } return metaGrid; } function createPlainVerse(verse, source, displayPreferences, options = {}) { const translationText = verse.text || ""; const verseCounts = getTextCounts(extractVerseCountText(verse, source, displayPreferences, translationText)); const isHighlighted = options.highlight !== false && isHighlightedVerse(verse); const article = document.createElement("article"); article.className = "alpha-text-verse"; article.classList.toggle("is-highlighted", isHighlighted); const head = document.createElement("div"); head.className = "alpha-text-verse-head"; const reference = document.createElement("span"); reference.className = "alpha-text-verse-reference"; reference.textContent = verse.reference || (verse.number ? `Verse ${verse.number}` : ""); const stats = document.createElement("span"); stats.className = "alpha-text-verse-counts"; stats.textContent = formatCountSummary(verseCounts); head.append(reference, stats); article.append(head); appendVerseTextLines(article, verse, source, displayPreferences, translationText, isHighlighted ? state.searchQuery : ""); return article; } function buildTokenTranslationText(tokens, fallbackText) { const glossText = (Array.isArray(tokens) ? tokens : []) .map((token) => String(token?.gloss || "").trim()) .filter(Boolean) .join(" ") .replace(/\s+([,.;:!?])/g, "$1") .trim(); return glossText || String(fallbackText || "").trim(); } function appendVerseTextLines(target, verse, source, displayPreferences, translationText, highlightQuery = "") { if (!(target instanceof HTMLElement)) { return; } const mode = displayPreferences?.textMode || "translation"; const originalText = normalizeTextValue(verse?.originalText); const transliterationText = getVerseTransliteration(verse, source); const lines = []; const appendLine = (text, variant) => { const normalizedText = normalizeTextValue(text); if (!normalizedText || lines.some((entry) => entry.text === normalizedText)) { return; } lines.push({ text: normalizedText, variant }); }; if (mode === "all") { appendLine(translationText, "translation"); appendLine(originalText, "original"); appendLine(transliterationText, "transliteration"); } else if (mode === "original") { appendLine(originalText || translationText, originalText ? "original" : "translation"); } else if (mode === "transliteration") { appendLine(transliterationText || translationText, transliterationText ? "transliteration" : "translation"); } else { appendLine(translationText, "translation"); } if (!lines.length) { appendLine(translationText, "translation"); } lines.forEach((line) => { const text = document.createElement("p"); text.className = `alpha-text-verse-text alpha-text-verse-text--${line.variant}`; appendHighlightedText(text, line.text, highlightQuery); target.appendChild(text); }); } function createTokenVerse(verse, lexiconId, displayPreferences, source, options = {}) { const translationText = buildTokenTranslationText(verse?.tokens, verse?.text); const verseCounts = getTextCounts(extractVerseCountText(verse, source, displayPreferences, translationText)); const isHighlighted = options.highlight !== false && isHighlightedVerse(verse); const article = document.createElement("article"); article.className = "alpha-text-verse"; article.classList.toggle("alpha-text-verse--interlinear", Boolean(displayPreferences?.showInterlinear)); article.classList.toggle("is-highlighted", isHighlighted); const head = document.createElement("div"); head.className = "alpha-text-verse-head"; const reference = document.createElement("span"); reference.className = "alpha-text-verse-reference"; reference.textContent = verse.reference || (verse.number ? `Verse ${verse.number}` : ""); const stats = document.createElement("span"); stats.className = "alpha-text-verse-counts"; stats.textContent = formatCountSummary(verseCounts); const tokenGrid = document.createElement("div"); tokenGrid.className = "alpha-text-token-grid"; (Array.isArray(verse?.tokens) ? verse.tokens : []).forEach((token) => { const strongId = Array.isArray(token?.strongs) ? token.strongs[0] : ""; const tokenEl = document.createElement(strongId ? "button" : "div"); tokenEl.className = `alpha-text-token${strongId ? " alpha-text-token--interactive" : ""}`; if (tokenEl instanceof HTMLButtonElement) { tokenEl.type = "button"; tokenEl.addEventListener("click", () => { void loadLexiconEntry(lexiconId, strongId, tokenEl); }); } const glossEl = document.createElement("span"); glossEl.className = "alpha-text-token-gloss"; glossEl.textContent = token?.gloss || "—"; const originalEl = document.createElement("span"); originalEl.className = "alpha-text-token-original"; originalEl.textContent = token?.original || "—"; tokenEl.append(glossEl, originalEl); if (strongId) { const strongsEl = document.createElement("span"); strongsEl.className = "alpha-text-token-strongs"; strongsEl.textContent = Array.isArray(token.strongs) ? token.strongs.join(" · ") : strongId; tokenEl.appendChild(strongsEl); } tokenGrid.appendChild(tokenEl); }); head.append(reference, stats); article.append(head); appendVerseTextLines(article, verse, source, displayPreferences, translationText, isHighlighted ? state.searchQuery : ""); if (displayPreferences?.showInterlinear) { article.appendChild(tokenGrid); } return article; } function createReaderNavigation(passage) { const navigation = document.createElement("div"); navigation.className = "alpha-text-reader-navigation"; if (passage?.navigation?.previous) { const previousButton = document.createElement("button"); previousButton.type = "button"; previousButton.className = "alpha-nav-btn alpha-text-reader-nav-btn"; previousButton.textContent = "← Previous"; previousButton.addEventListener("click", () => { navigateToPassageTarget(passage.navigation.previous); }); navigation.appendChild(previousButton); } if (passage?.navigation?.next) { const nextButton = document.createElement("button"); nextButton.type = "button"; nextButton.className = "alpha-nav-btn alpha-text-reader-nav-btn alpha-text-reader-nav-btn--next"; nextButton.textContent = "Next →"; nextButton.addEventListener("click", () => { navigateToPassageTarget(passage.navigation.next); }); navigation.appendChild(nextButton); } return navigation.childElementCount ? navigation : null; } function createReaderCard(passage, options = {}) { const source = passage?.source || getSelectedSource(); const displayPreferences = getSourceDisplayPreferences(source, passage); const card = createCard(options.title || getPassageLocationLabel(passage)); card.classList.add("alpha-text-reader-card"); if (options.compare) { card.classList.add("alpha-text-reader-card--compare"); } const reader = document.createElement("div"); reader.className = "alpha-text-reader"; if (passage?.errorMessage) { reader.appendChild(createEmptyMessage(passage.errorMessage)); card.appendChild(reader); return card; } const verses = Array.isArray(passage?.verses) ? passage.verses : []; if (!verses.length) { reader.appendChild(createEmptyMessage("No verses were found for this section.")); card.appendChild(reader); return card; } verses.forEach((verse) => { const verseEl = source?.features?.hasTokenAnnotations ? createTokenVerse(verse, source.features.lexiconIds?.[0] || "", displayPreferences, source, options) : createPlainVerse(verse, source, displayPreferences, options); reader.appendChild(verseEl); }); card.appendChild(reader); const navigation = options.showNavigation === false ? null : createReaderNavigation(passage); if (navigation) { card.appendChild(navigation); } return card; } function createCompareReaderGrid(primaryPassage, comparePassage) { const wrapper = document.createElement("div"); wrapper.className = "alpha-text-reader-compare"; wrapper.appendChild(createReaderCard(primaryPassage, { title: buildCompareCardTitle(primaryPassage), showNavigation: false })); if (comparePassage) { wrapper.appendChild(createReaderCard(comparePassage, { title: buildCompareCardTitle(comparePassage), compare: true, highlight: false, showNavigation: false })); } return wrapper; } function createSearchCard() { const hasSearchState = state.searchLoading || state.searchError || state.searchResults || state.searchQuery; if (!hasSearchState) { return null; } const card = createCard("Search Results"); const scopeLabel = state.activeSearchScope === "global" ? "all texts" : (state.searchResults?.scope?.source?.title || getSelectedSource()?.title || "current source"); const summary = document.createElement("p"); summary.className = "alpha-text-search-summary"; const actions = document.createElement("div"); actions.className = "alpha-text-search-actions"; const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.className = "alpha-nav-btn"; closeButton.textContent = "Close Search"; closeButton.addEventListener("click", () => { clearScopedSearch(state.activeSearchScope === "source" ? "source" : "global"); renderDetail(); }); actions.appendChild(closeButton); if (state.searchLoading) { summary.textContent = `Searching ${scopeLabel} for \"${state.searchQuery}\"...`; card.append(summary, actions); return card; } if (state.searchError) { summary.textContent = `Search scope: ${scopeLabel}`; card.append(summary, actions, createEmptyMessage(state.searchError)); return card; } const payload = state.searchResults; const totalMatches = Number(payload?.totalMatches) || 0; const truncatedNote = payload?.truncated ? ` Showing the first ${payload.resultCount} results.` : ""; summary.textContent = `${totalMatches} matches in ${scopeLabel}.${truncatedNote}`; card.append(summary, actions); if (!Array.isArray(payload?.results) || !payload.results.length) { card.appendChild(createEmptyMessage(`No matches found for \"${state.searchQuery}\".`)); return card; } const resultsEl = document.createElement("div"); resultsEl.className = "alpha-text-search-results"; payload.results.forEach((result) => { const button = document.createElement("button"); button.type = "button"; button.className = "alpha-text-search-result"; button.classList.toggle( "is-active", normalizeId(result?.sourceId) === normalizeId(state.selectedSourceId) && normalizeId(result?.workId) === normalizeId(state.selectedWorkId) && normalizeId(result?.sectionId) === normalizeId(state.selectedSectionId) && normalizeId(result?.verseId) === normalizeId(state.highlightedVerseId) ); const head = document.createElement("div"); head.className = "alpha-text-search-result-head"; const reference = document.createElement("span"); reference.className = "alpha-text-search-reference"; reference.textContent = result.reference || `${result.workTitle} ${result.sectionLabel}:${result.verseNumber}`; const location = document.createElement("span"); location.className = "alpha-text-search-location"; location.textContent = state.activeSearchScope === "global" ? `${result.sourceShortTitle || result.sourceTitle} · ${result.workTitle} · ${result.sectionLabel}` : `${result.workTitle} · ${result.sectionLabel}`; const preview = document.createElement("p"); preview.className = "alpha-text-search-preview"; appendHighlightedText(preview, result.preview || result.reference || "", state.searchQuery); button.addEventListener("click", () => { void openSearchResult(result); }); head.append(reference, location); button.append(head, preview); resultsEl.appendChild(button); }); card.appendChild(resultsEl); return card; } function isGlobalSearchOnlyMode() { return (state.activeSearchScope === "global" || state.activeSearchScope === "source") && Boolean(state.searchQuery) && !state.highlightedVerseId; } function renderDetail() { const source = getSelectedSource(); const work = getSelectedWork(source); const section = getSelectedSection(source, work); const compareEnabled = isCompareModeEnabled(getSelectedSourceGroup()); const globalSearchOnlyMode = isGlobalSearchOnlyMode(); setGlobalSearchHeadingMode(globalSearchOnlyMode); if (!source || !work || !section) { renderPlaceholder("Text Reader", "Select a source to begin", "Choose a text source and section from the left panel."); renderLexiconPopup(); return; } if (detailNameEl) { detailNameEl.textContent = globalSearchOnlyMode ? `Global Search${state.searchQuery ? `: ${state.searchQuery}` : ""}` : (state.currentPassage?.section?.title || section.title); } if (detailSubEl) { detailSubEl.textContent = globalSearchOnlyMode ? "All text sources" : buildSourceDetailSubtitle(source, work); } if (!detailBodyEl) { return; } detailBodyEl.replaceChildren(); const searchCard = createSearchCard(); if (searchCard) { detailBodyEl.appendChild(searchCard); } if (globalSearchOnlyMode) { renderLexiconPopup(); return; } if (!state.currentPassage) { const loadingCard = createCard("Text Reader"); loadingCard.appendChild(createEmptyMessage("Loading section…")); detailBodyEl.appendChild(loadingCard); renderLexiconPopup(); return; } detailBodyEl.appendChild(createMetaGrid(state.currentPassage)); if (compareEnabled && state.comparePassage) { detailBodyEl.appendChild(createCompareReaderGrid(state.currentPassage, state.comparePassage)); const compareNavigation = createReaderNavigation(state.currentPassage); if (compareNavigation) { detailBodyEl.appendChild(compareNavigation); } } else { detailBodyEl.appendChild(createReaderCard(state.currentPassage)); } renderLexiconPopup(); } function getComparableWork(source, work) { const works = Array.isArray(source?.works) ? source.works : []; return findById(works, work?.id) || works.find((entry) => normalizeId(entry?.title) === normalizeId(work?.title)) || works[0] || null; } function getComparableSection(work, section) { const sections = Array.isArray(work?.sections) ? work.sections : []; return findById(sections, section?.id) || sections.find((entry) => Number(entry?.number || 0) === Number(section?.number || 0)) || sections.find((entry) => normalizeId(entry?.title) === normalizeId(section?.title)) || sections.find((entry) => normalizeId(entry?.label) === normalizeId(section?.label)) || sections[0] || null; } function buildPassageLoadError(source, work, section, message) { return { source, work, section, verses: [], errorMessage: message }; } async function loadComparablePassage(compareSource, currentWork, currentSection) { const compareWork = getComparableWork(compareSource, currentWork); const compareSection = getComparableSection(compareWork, currentSection); if (!compareWork || !compareSection) { return buildPassageLoadError(compareSource, compareWork, compareSection, "Unable to align this comparison section."); } try { return await dataService.loadTextSection?.(compareSource.id, compareWork.id, compareSection.id); } catch (error) { return buildPassageLoadError(compareSource, compareWork, compareSection, error?.message || "Unable to load the comparison translation."); } } async function loadSelectedPassage() { const source = getSelectedSource(); const work = getSelectedWork(source); const section = getSelectedSection(source, work); const compareSource = isCompareModeEnabled(getSelectedSourceGroup()) ? getCompareSource() : null; if (!source || !work || !section) { state.currentPassage = null; state.comparePassage = null; renderDetail(); return; } state.currentPassage = null; state.comparePassage = null; renderDetail(); const [primaryResult, compareResult] = await Promise.allSettled([ dataService.loadTextSection?.(source.id, work.id, section.id), compareSource ? loadComparablePassage(compareSource, work, section) : Promise.resolve(null) ]); if (primaryResult.status === "fulfilled") { state.currentPassage = primaryResult.value; } else { state.currentPassage = buildPassageLoadError(source, work, section, primaryResult.reason?.message || "Unable to load this section."); } if (compareResult.status === "fulfilled") { state.comparePassage = compareResult.value; } else if (compareSource) { const compareWork = getComparableWork(compareSource, work); const compareSection = getComparableSection(compareWork, section); state.comparePassage = buildPassageLoadError(compareSource, compareWork, compareSection, compareResult.reason?.message || "Unable to load the comparison translation."); } renderDetail(); if (state.highlightedVerseId) { requestAnimationFrame(scrollHighlightedVerseIntoView); } } async function runSearch(scope, forceRefresh = false) { const searchFn = dataService.searchTextLibrary; if (typeof searchFn !== "function") { state.searchError = "Text search is unavailable."; state.searchLoading = false; state.searchResults = null; renderDetail(); return; } const normalizedScope = scope === "source" ? "source" : "global"; const query = String(getSearchInput(normalizedScope)?.value || getStoredSearchQuery(normalizedScope) || "").trim(); setStoredSearchQuery(normalizedScope, query); state.activeSearchScope = normalizedScope; state.searchQuery = query; state.searchError = ""; state.searchResults = null; state.highlightedVerseId = ""; updateSearchControls(); if (!query) { clearSearchState(); renderDetail(); return; } if (normalizedScope === "global" || normalizedScope === "source") { showDetailOnlyMode(); } const requestId = state.searchRequestId + 1; state.searchRequestId = requestId; state.searchLoading = true; renderDetail(); try { const payload = await searchFn(query, { sourceId: normalizedScope === "source" ? state.selectedSourceId : "", limit: 50 }, forceRefresh); if (requestId !== state.searchRequestId) { return; } state.searchResults = payload; state.searchLoading = false; renderDetail(); } catch (error) { if (requestId !== state.searchRequestId) { return; } state.searchLoading = false; state.searchError = error?.message || "Unable to search this text library."; renderDetail(); } } async function openSearchResult(result) { if (!result) { return; } const sourceGroup = findSourceGroupBySourceId(result.sourceId); state.selectedSourceGroupId = sourceGroup?.id || ""; state.selectedSourceId = result.sourceId; rememberSelectedSource(sourceGroup, result.sourceId); state.selectedWorkId = result.workId; state.selectedSectionId = result.sectionId; state.highlightedVerseId = result.verseId; dismissLexiconEntry({ restoreFocus: false }); syncSelectionForSource(getSelectedSource()); renderSourceList(); renderSelectors(); showDetailOnlyMode(); await loadSelectedPassage(); clearActiveSearchUi({ preserveHighlight: true }); renderDetail(); } function bindControls() { if (state.initialized) { return; } if (globalSearchFormEl instanceof HTMLFormElement) { globalSearchFormEl.addEventListener("submit", (event) => { event.preventDefault(); void runSearch("global"); }); } if (globalSearchInputEl instanceof HTMLInputElement) { globalSearchInputEl.addEventListener("input", () => { state.globalSearchQuery = String(globalSearchInputEl.value || "").trim(); updateSearchControls(); if (!state.globalSearchQuery && state.activeSearchScope === "global" && state.searchQuery) { clearSearchState(); renderDetail(); } }); } if (globalSearchClearEl instanceof HTMLButtonElement) { globalSearchClearEl.addEventListener("click", () => { clearScopedSearch("global"); renderDetail(); }); } if (localSearchFormEl instanceof HTMLFormElement) { localSearchFormEl.addEventListener("submit", (event) => { event.preventDefault(); void runSearch("source"); }); } if (localSearchClearEl instanceof HTMLButtonElement) { localSearchClearEl.addEventListener("click", () => { clearScopedSearch("source"); renderDetail(); }); } if (localSearchInputEl instanceof HTMLInputElement) { localSearchInputEl.addEventListener("input", () => { state.localSearchQuery = String(localSearchInputEl.value || "").trim(); updateSearchControls(); if (!state.localSearchQuery && state.activeSearchScope === "source" && state.searchQuery) { clearSearchState(); renderDetail(); } }); } if (showVerseHeadsEl instanceof HTMLInputElement) { showVerseHeadsEl.addEventListener("change", () => { state.showVerseHeads = Boolean(showVerseHeadsEl.checked); writeStoredBoolean(STORAGE_KEYS.showVerseHeads, state.showVerseHeads); syncReaderDisplayControls(); }); } if (translationSelectEl instanceof HTMLSelectElement) { translationSelectEl.addEventListener("change", () => { const sourceGroup = getSelectedSourceGroup(); state.selectedSourceId = String(translationSelectEl.value || ""); rememberSelectedSource(sourceGroup, state.selectedSourceId); syncSelectionForSource(getSelectedSource()); state.currentPassage = null; state.comparePassage = null; state.lexiconEntry = null; state.highlightedVerseId = ""; syncCompareSelection(sourceGroup); renderSourceList(); renderSelectors(); showDetailOnlyMode(); if (state.searchQuery && state.activeSearchScope === "source") { void Promise.all([loadSelectedPassage(), runSearch("source")]); return; } void loadSelectedPassage(); }); } if (compareToggleEl instanceof HTMLButtonElement) { compareToggleEl.addEventListener("click", () => { const sourceGroup = getSelectedSourceGroup(); setCompareModeEnabled(sourceGroup, !isCompareModeEnabled(sourceGroup)); syncCompareSelection(sourceGroup); state.comparePassage = null; renderSelectors(); void loadSelectedPassage(); }); } if (compareSelectEl instanceof HTMLSelectElement) { compareSelectEl.addEventListener("change", () => { const sourceGroup = getSelectedSourceGroup(); rememberCompareSource(sourceGroup, String(compareSelectEl.value || "")); state.comparePassage = null; renderSelectors(); void loadSelectedPassage(); }); } if (workSelectEl) { workSelectEl.addEventListener("change", () => { state.selectedWorkId = String(workSelectEl.value || ""); const source = getSelectedSource(); syncSelectionForSource(source); state.currentPassage = null; state.comparePassage = null; state.lexiconEntry = null; state.highlightedVerseId = ""; renderSelectors(); void loadSelectedPassage(); }); } if (sectionSelectEl) { sectionSelectEl.addEventListener("change", () => { state.selectedSectionId = String(sectionSelectEl.value || ""); state.currentPassage = null; state.comparePassage = null; state.lexiconEntry = null; state.highlightedVerseId = ""; void loadSelectedPassage(); }); } document.addEventListener("keydown", (event) => { if (event.key === "Escape" && state.lexiconEntry) { closeLexiconEntry(); } }); state.initialized = true; } async function ensureAlphabetTextSection() { getElements(); bindControls(); window.TarotChromeUi?.initializeSidebarPopouts?.(); window.TarotChromeUi?.initializeDetailPopouts?.(); if (!sourceListEl || !detailBodyEl) { return; } await ensureCatalogLoaded(); syncReaderDisplayControls(); renderSourceList(); renderSelectors(); updateSearchControls(); showSidebarOnlyMode(false); if (!state.currentPassage) { await loadSelectedPassage(); return; } renderDetail(); } function resetState() { state.catalog = null; state.currentPassage = null; state.comparePassage = null; state.lexiconEntry = null; state.selectedSourceGroupId = ""; state.selectedSourceId = ""; state.selectedSourceIdByGroup = {}; state.compareSourceIdByGroup = {}; state.compareModeByGroup = {}; state.selectedWorkId = ""; state.selectedSectionId = ""; state.lexiconRequestId = 0; state.lexiconOccurrenceResults = null; state.lexiconOccurrenceLoading = false; state.lexiconOccurrenceError = ""; state.lexiconOccurrenceVisible = false; state.lexiconOccurrenceRequestId = 0; state.globalSearchQuery = ""; state.localSearchQuery = ""; state.activeSearchScope = "global"; state.searchQuery = ""; state.searchResults = null; state.searchLoading = false; state.searchError = ""; state.searchRequestId = 0; state.highlightedVerseId = ""; lexiconReturnFocusEl = null; if (globalSearchInputEl instanceof HTMLInputElement) { globalSearchInputEl.value = ""; } if (localSearchInputEl instanceof HTMLInputElement) { localSearchInputEl.value = ""; } updateSearchControls(); renderLexiconPopup(); } document.addEventListener("connection:updated", resetState); window.AlphabetTextUi = { ensureAlphabetTextSection }; })();