2026-03-09 23:27:03 -07:00
( function () {
"use strict" ;
const dataService = window . TarotDataService || {};
2026-03-15 16:20:43 -07:00
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.
}
}
2026-03-09 23:27:03 -07:00
const state = {
initialized : false ,
catalog : null ,
2026-03-14 00:45:15 -07:00
selectedSourceGroupId : "" ,
2026-03-09 23:27:03 -07:00
selectedSourceId : "" ,
2026-03-14 00:45:15 -07:00
selectedSourceIdByGroup : {},
compareSourceIdByGroup : {},
compareModeByGroup : {},
2026-03-09 23:27:03 -07:00
selectedWorkId : "" ,
selectedSectionId : "" ,
currentPassage : null ,
2026-03-14 00:45:15 -07:00
comparePassage : null ,
2026-03-09 23:27:03 -07:00
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 ,
2026-03-10 14:55:01 -07:00
highlightedVerseId : "" ,
2026-03-15 16:20:43 -07:00
displayPreferencesBySource : {},
showVerseHeads : readStoredBoolean ( STORAGE_KEYS . showVerseHeads , true )
2026-03-09 23:27:03 -07:00
};
let sourceListEl ;
let sourceCountEl ;
let globalSearchFormEl ;
let globalSearchInputEl ;
2026-03-15 16:20:43 -07:00
let globalSearchClearEl ;
2026-03-09 23:27:03 -07:00
let localSearchFormEl ;
let localSearchInputEl ;
2026-03-15 16:20:43 -07:00
let localSearchClearEl ;
2026-03-14 00:45:15 -07:00
let translationSelectEl ;
let translationControlEl ;
let compareSelectEl ;
let compareControlEl ;
let compareToggleEl ;
let compareToggleControlEl ;
2026-03-09 23:27:03 -07:00
let workSelectEl ;
let sectionSelectEl ;
2026-03-12 04:37:26 -07:00
let detailHeadingEl ;
2026-03-09 23:27:03 -07:00
let detailNameEl ;
let detailSubEl ;
2026-03-12 04:37:26 -07:00
let detailHeadingToolsEl ;
2026-03-09 23:27:03 -07:00
let detailBodyEl ;
2026-03-12 02:35:02 -07:00
let textLayoutEl ;
2026-03-15 16:20:43 -07:00
let showVerseHeadsEl ;
2026-03-09 23:27:03 -07:00
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" );
2026-03-15 16:20:43 -07:00
globalSearchClearEl = document . getElementById ( "alpha-text-global-search-clear" );
2026-03-09 23:27:03 -07:00
localSearchFormEl = document . getElementById ( "alpha-text-local-search-form" );
localSearchInputEl = document . getElementById ( "alpha-text-local-search-input" );
2026-03-15 16:20:43 -07:00
localSearchClearEl = document . getElementById ( "alpha-text-local-search-clear" );
showVerseHeadsEl = document . getElementById ( "alpha-text-show-verse-heads" );
2026-03-14 00:45:15 -07:00
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" );
2026-03-09 23:27:03 -07:00
workSelectEl = document . getElementById ( "alpha-text-work-select" );
sectionSelectEl = document . getElementById ( "alpha-text-section-select" );
2026-03-12 04:37:26 -07:00
detailHeadingEl = document . querySelector ( "#alphabet-text-section .alpha-text-detail-heading" );
2026-03-09 23:27:03 -07:00
detailNameEl = document . getElementById ( "alpha-text-detail-name" );
detailSubEl = document . getElementById ( "alpha-text-detail-sub" );
2026-03-12 04:37:26 -07:00
detailHeadingToolsEl = document . querySelector ( "#alphabet-text-section .alpha-text-heading-tools" );
2026-03-09 23:27:03 -07:00
detailBodyEl = document . getElementById ( "alpha-text-detail-body" );
2026-03-12 02:35:02 -07:00
textLayoutEl = sourceListEl ? . closest ? .( ".planet-layout" ) || detailBodyEl ? . closest ? .( ".planet-layout" ) || null ;
2026-03-15 16:20:43 -07:00
syncReaderDisplayControls ();
2026-03-09 23:27:03 -07:00
ensureLexiconPopup ();
}
2026-03-15 16:20:43 -07:00
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" );
}
}
2026-03-12 04:37:26 -07:00
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" );
}
2026-03-12 02:35:02 -07:00
function showDetailOnlyMode () {
if ( ! ( textLayoutEl instanceof HTMLElement )) {
return ;
}
window . TarotChromeUi ? . initializeSidebarPopouts ? .();
window . TarotChromeUi ? . initializeDetailPopouts ? .();
window . TarotChromeUi ? . initializeSidebarAutoCollapse ? .();
window . TarotChromeUi ? . showDetailOnly ? .( textLayoutEl );
}
2026-03-15 16:20:43 -07:00
function showSidebarOnlyMode ( persist = false ) {
if ( ! ( textLayoutEl instanceof HTMLElement )) {
return ;
}
window . TarotChromeUi ? . initializeSidebarPopouts ? .();
window . TarotChromeUi ? . initializeDetailPopouts ? .();
window . TarotChromeUi ? . showSidebarOnly ? .( textLayoutEl , persist );
}
2026-03-09 23:27:03 -07:00
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 : [];
}
2026-03-14 00:45:15 -07:00
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 : [];
}
2026-03-09 23:27:03 -07:00
function findById ( entries , value ) {
const needle = normalizeId ( value );
return ( Array . isArray ( entries ) ? entries : []). find (( entry ) => normalizeId ( entry ? . id ) === needle ) || null ;
}
2026-03-14 00:45:15 -07:00
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 );
}
}
2026-03-09 23:27:03 -07:00
function getSelectedSource () {
2026-03-14 00:45:15 -07:00
return getSourceForGroup ( getSelectedSourceGroup (), state . selectedSourceId )
|| findById ( getSources (), state . selectedSourceId );
2026-03-09 23:27:03 -07:00
}
function getSelectedWork ( source = getSelectedSource ()) {
return findById ( source ? . works , state . selectedWorkId );
}
function getSelectedSection ( source = getSelectedSource (), work = getSelectedWork ( source )) {
return findById ( work ? . sections , state . selectedSectionId );
}
2026-03-10 14:55:01 -07:00
function normalizeTextValue ( value ) {
return String ( value || "" ). trim ();
}
2026-03-14 00:45:15 -07:00
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 } ` ;
}
2026-03-15 16:20:43 -07:00
if ( /[^aeiou]y$/i . test ( baseLabel )) {
return ` ${ normalizedCount } ${ baseLabel . slice ( 0 , - 1 ) } ies` ;
}
2026-03-14 00:45:15 -07:00
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 || "--" } ` ;
}
2026-03-12 04:18:26 -07:00
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 ) => {
2026-05-14 21:53:24 -07:00
const translationText = getVerseTranslationText ( verse );
2026-03-12 04:18:26 -07:00
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
});
}
2026-03-10 14:55:01 -07:00
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 "" ;
}
2026-03-09 23:27:03 -07:00
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 () {
2026-03-15 16:20:43 -07:00
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 ;
}
2026-03-10 14:55:01 -07:00
}
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 = "" ;
2026-03-09 23:27:03 -07:00
}
2026-03-10 14:55:01 -07:00
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" ;
2026-03-09 23:27:03 -07:00
}
}
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 , "\\$&" );
}
2026-03-15 16:20:43 -07:00
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 );
}
2026-03-09 23:27:03 -07:00
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 ;
}
2026-03-15 16:20:43 -07:00
const matcher = buildWholeWordMatcher ( normalizedQuery , "giu" );
if ( ! matcher ) {
target . textContent = sourceText ;
return ;
}
2026-03-09 23:27:03 -07:00
let lastIndex = 0 ;
let match = matcher . exec ( sourceText );
while ( match ) {
2026-03-15 16:20:43 -07:00
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 )));
2026-03-09 23:27:03 -07:00
}
const mark = document . createElement ( "mark" );
mark . className = "alpha-text-mark" ;
2026-03-15 16:20:43 -07:00
mark . textContent = sourceText . slice ( matchStart , matchEnd );
2026-03-09 23:27:03 -07:00
target . appendChild ( mark );
2026-03-15 16:20:43 -07:00
lastIndex = matchEnd ;
2026-03-09 23:27:03 -07:00
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" );
2026-03-10 14:55:01 -07:00
card . className = "detail-meta-card planet-meta-card" ;
2026-03-09 23:27:03 -07:00
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 );
}
2026-03-10 14:55:01 -07:00
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 || "--" } ` ;
}
2026-03-09 23:27:03 -07:00
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 || "" ;
}
}
2026-03-14 00:45:15 -07:00
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 );
}
2026-03-09 23:27:03 -07:00
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 : [] };
2026-03-14 00:45:15 -07:00
state . catalog . sourceGroups = buildSourceGroups ( getSources ());
if ( ! state . selectedSourceGroupId && state . selectedSourceId ) {
state . selectedSourceGroupId = findSourceGroupBySourceId ( state . selectedSourceId ) ? . id || "" ;
2026-03-09 23:27:03 -07:00
}
2026-03-14 00:45:15 -07:00
if ( ! state . selectedSourceGroupId ) {
state . selectedSourceGroupId = getSourceGroups ()[ 0 ] ? . id || "" ;
}
syncSelectionForGroup ( getSelectedSourceGroup ());
2026-03-09 23:27:03 -07:00
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 ();
2026-03-14 00:45:15 -07:00
const sourceGroups = getSourceGroups ();
sourceGroups . forEach (( group ) => {
const source = getSourceForGroup ( group );
2026-03-09 23:27:03 -07:00
const button = document . createElement ( "button" );
button . type = "button" ;
button . className = "planet-list-item alpha-text-source-btn" ;
2026-03-14 00:45:15 -07:00
button . dataset . sourceGroupId = group . id ;
2026-03-09 23:27:03 -07:00
button . setAttribute ( "role" , "option" );
2026-03-14 00:45:15 -07:00
const isSelected = normalizeId ( group . id ) === normalizeId ( state . selectedSourceGroupId );
2026-03-09 23:27:03 -07:00
button . classList . toggle ( "is-selected" , isSelected );
button . setAttribute ( "aria-selected" , isSelected ? "true" : "false" );
const name = document . createElement ( "span" );
name . className = "planet-list-name" ;
2026-03-14 00:45:15 -07:00
name . textContent = group . title ;
2026-03-09 23:27:03 -07:00
const meta = document . createElement ( "span" );
meta . className = "alpha-text-source-meta" ;
2026-03-14 00:45:15 -07:00
meta . textContent = buildSourceGroupListMeta ( group );
2026-03-09 23:27:03 -07:00
button . append ( name , meta );
button . addEventListener ( "click" , () => {
2026-03-14 00:45:15 -07:00
if ( normalizeId ( group . id ) === normalizeId ( state . selectedSourceGroupId )) {
2026-03-12 02:35:02 -07:00
showDetailOnlyMode ();
2026-03-09 23:27:03 -07:00
return ;
}
2026-03-14 00:45:15 -07:00
state . selectedSourceGroupId = group . id ;
2026-03-09 23:27:03 -07:00
state . currentPassage = null ;
state . lexiconEntry = null ;
state . highlightedVerseId = "" ;
2026-03-14 00:45:15 -07:00
syncSelectionForGroup ( group );
2026-03-09 23:27:03 -07:00
renderSourceList ();
renderSelectors ();
2026-03-12 02:35:02 -07:00
showDetailOnlyMode ();
2026-03-09 23:27:03 -07:00
if ( state . searchQuery && state . activeSearchScope === "source" ) {
void Promise . all ([ loadSelectedPassage (), runSearch ( "source" )]);
return ;
}
void loadSelectedPassage ();
});
sourceListEl . appendChild ( button );
});
2026-03-14 00:45:15 -07:00
if ( ! sourceGroups . length ) {
2026-03-09 23:27:03 -07:00
sourceListEl . appendChild ( createEmptyMessage ( "No text sources are available." ));
}
if ( sourceCountEl ) {
2026-03-14 00:45:15 -07:00
sourceCountEl . textContent = ` ${ sourceGroups . length } sources` ;
2026-03-09 23:27:03 -07:00
}
}
function renderSelectors () {
2026-03-14 00:45:15 -07:00
const group = getSelectedSourceGroup ();
2026-03-09 23:27:03 -07:00
const source = getSelectedSource ();
const work = getSelectedWork ( source );
2026-03-14 00:45:15 -07:00
const variants = getSourceVariants ( group );
const compareCandidates = getCompareCandidates ( group );
const compareSource = getCompareSource ( group );
const compareEnabled = isCompareModeEnabled ( group );
2026-03-09 23:27:03 -07:00
const works = Array . isArray ( source ? . works ) ? source . works : [];
const sections = Array . isArray ( work ? . sections ) ? work . sections : [];
2026-03-14 00:45:15 -07:00
fillSelect ( translationSelectEl , variants , state . selectedSourceId , ( entry ) => buildTranslationOptionLabel ( entry ));
fillSelect ( compareSelectEl , compareCandidates , compareSource ? . id || "" , ( entry ) => buildTranslationOptionLabel ( entry ));
2026-03-15 16:20:43 -07:00
fillSelect ( workSelectEl , works , state . selectedWorkId , ( entry ) => ` ${ entry . title } ( ${ formatCountLabel ( entry . sectionCount , String ( source ? . sectionLabel || "section" ). toLowerCase ()) } )` );
2026-03-09 23:27:03 -07:00
fillSelect ( sectionSelectEl , sections , state . selectedSectionId , ( entry ) => ` ${ entry . label } · ${ entry . verseCount } verses` );
2026-03-14 00:45:15 -07:00
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 ;
}
2026-03-09 23:27:03 -07:00
}
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 ) {
2026-03-14 00:45:15 -07:00
const sourceGroup = getSelectedSourceGroup ();
2026-03-09 23:27:03 -07:00
const source = passage ? . source || getSelectedSource ();
const work = passage ? . work || getSelectedWork ( source );
const section = passage ? . section || getSelectedSection ( source , work );
2026-03-14 00:45:15 -07:00
const metadata = getSourceMetadata ( source );
const version = normalizeTextValue ( metadata . versionLabel || metadata . version );
const translator = normalizeTextValue ( metadata . translator );
const compareSource = getCompareSource ( sourceGroup );
2026-03-10 14:55:01 -07:00
const displayPreferences = getSourceDisplayPreferences ( source , passage );
2026-03-09 23:27:03 -07:00
const metaGrid = document . createElement ( "div" );
metaGrid . className = "alpha-text-meta-grid" ;
const overviewCard = createCard ( "Source Overview" );
overviewCard . innerHTML += `
<dl class="alpha-dl">
<dt>Source</dt><dd> ${ source ? . title || "--" } </dd>
2026-03-14 00:45:15 -07:00
${ version ? `<dt>Version</dt><dd> ${ version } </dd>` : "" }
${ translator ? `<dt>Translator</dt><dd> ${ translator } </dd>` : "" }
${ getSourceVariants ( sourceGroup ). length > 1 ? `<dt>Translations</dt><dd> ${ getSourceVariants ( sourceGroup ). map (( entry ) => buildTranslationOptionLabel ( entry )). join ( " / " ) } </dd>` : "" }
${ isCompareModeEnabled ( sourceGroup ) && compareSource ? `<dt>Compare</dt><dd> ${ buildTranslationOptionLabel ( compareSource ) } </dd>` : "" }
2026-03-09 23:27:03 -07:00
<dt>Tradition</dt><dd> ${ source ? . tradition || "--" } </dd>
<dt>Language</dt><dd> ${ source ? . language || "--" } </dd>
<dt>Script</dt><dd> ${ source ? . script || "--" } </dd>
<dt> ${ source ? . workLabel || "Work" } </dt><dd> ${ work ? . title || "--" } </dd>
<dt> ${ source ? . sectionLabel || "Section" } </dt><dd> ${ section ? . label || "--" } </dd>
</dl>
` ;
metaGrid . appendChild ( overviewCard );
2026-03-12 04:18:26 -07:00
const totalsCard = createCard ( "Entry Totals" );
const totals = sumPassageCounts ( passage , source , displayPreferences );
totalsCard . innerHTML += `
<dl class="alpha-dl">
<dt>Words</dt><dd> ${ totals . words } </dd>
<dt>Letters</dt><dd> ${ totals . letters } </dd>
<dt>Consonants</dt><dd> ${ totals . consonants } </dd>
<dt>Vowels</dt><dd> ${ totals . vowels } </dd>
</dl>
` ;
metaGrid . appendChild ( totalsCard );
2026-03-10 14:55:01 -07:00
if ( displayPreferences . capabilities . hasAnyExtras ) {
const extraCard = createCard ( "Extra" );
extraCard . classList . add ( "alpha-text-extra-card" );
2026-03-09 23:27:03 -07:00
2026-03-10 14:55:01 -07:00
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 );
2026-03-09 23:27:03 -07:00
}
2026-03-10 14:55:01 -07:00
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 );
2026-03-09 23:27:03 -07:00
}
2026-03-10 14:55:01 -07:00
metaGrid . appendChild ( extraCard );
}
2026-03-09 23:27:03 -07:00
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 ;
}
2026-03-14 00:45:15 -07:00
function createPlainVerse ( verse , source , displayPreferences , options = {}) {
2026-03-12 04:18:26 -07:00
const translationText = verse . text || "" ;
const verseCounts = getTextCounts ( extractVerseCountText ( verse , source , displayPreferences , translationText ));
2026-03-14 00:45:15 -07:00
const isHighlighted = options . highlight !== false && isHighlightedVerse ( verse );
2026-03-09 23:27:03 -07:00
const article = document . createElement ( "article" );
article . className = "alpha-text-verse" ;
2026-03-14 00:45:15 -07:00
article . classList . toggle ( "is-highlighted" , isHighlighted );
2026-03-09 23:27:03 -07:00
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 } ` : "" );
2026-03-12 04:18:26 -07:00
const stats = document . createElement ( "span" );
stats . className = "alpha-text-verse-counts" ;
stats . textContent = formatCountSummary ( verseCounts );
head . append ( reference , stats );
2026-03-10 14:55:01 -07:00
article . append ( head );
2026-03-14 00:45:15 -07:00
appendVerseTextLines ( article , verse , source , displayPreferences , translationText , isHighlighted ? state . searchQuery : "" );
2026-03-09 23:27:03 -07:00
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 ();
}
2026-05-14 21:53:24 -07:00
function getVerseTranslationText ( verse ) {
const translationText = normalizeTextValue ( verse ? . text );
if ( translationText ) {
return translationText ;
}
return buildTokenTranslationText ( verse ? . tokens , "" );
}
2026-03-14 00:45:15 -07:00
function appendVerseTextLines ( target , verse , source , displayPreferences , translationText , highlightQuery = "" ) {
2026-03-10 14:55:01 -07:00
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 } ` ;
2026-03-14 00:45:15 -07:00
appendHighlightedText ( text , line . text , highlightQuery );
2026-03-10 14:55:01 -07:00
target . appendChild ( text );
});
}
2026-03-14 00:45:15 -07:00
function createTokenVerse ( verse , lexiconId , displayPreferences , source , options = {}) {
2026-05-14 21:53:24 -07:00
const translationText = getVerseTranslationText ( verse );
2026-03-12 04:18:26 -07:00
const verseCounts = getTextCounts ( extractVerseCountText ( verse , source , displayPreferences , translationText ));
2026-03-14 00:45:15 -07:00
const isHighlighted = options . highlight !== false && isHighlightedVerse ( verse );
2026-03-09 23:27:03 -07:00
const article = document . createElement ( "article" );
2026-03-10 14:55:01 -07:00
article . className = "alpha-text-verse" ;
article . classList . toggle ( "alpha-text-verse--interlinear" , Boolean ( displayPreferences ? . showInterlinear ));
2026-03-14 00:45:15 -07:00
article . classList . toggle ( "is-highlighted" , isHighlighted );
2026-03-09 23:27:03 -07:00
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 } ` : "" );
2026-03-12 04:18:26 -07:00
const stats = document . createElement ( "span" );
stats . className = "alpha-text-verse-counts" ;
stats . textContent = formatCountSummary ( verseCounts );
2026-03-09 23:27:03 -07:00
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 );
});
2026-03-12 04:18:26 -07:00
head . append ( reference , stats );
2026-03-10 14:55:01 -07:00
article . append ( head );
2026-03-14 00:45:15 -07:00
appendVerseTextLines ( article , verse , source , displayPreferences , translationText , isHighlighted ? state . searchQuery : "" );
2026-03-10 14:55:01 -07:00
if ( displayPreferences ? . showInterlinear ) {
article . appendChild ( tokenGrid );
}
2026-03-09 23:27:03 -07:00
return article ;
}
2026-03-14 00:45:15 -07:00
function createReaderNavigation ( passage ) {
2026-03-10 14:55:01 -07:00
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 );
}
2026-03-14 00:45:15 -07:00
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 ) {
2026-03-10 14:55:01 -07:00
card . appendChild ( navigation );
}
2026-03-09 23:27:03 -07:00
return card ;
}
2026-03-14 00:45:15 -07:00
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 ;
}
2026-03-09 23:27:03 -07:00
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" ;
2026-03-15 16:20:43 -07:00
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 );
2026-03-09 23:27:03 -07:00
if ( state . searchLoading ) {
summary . textContent = `Searching ${ scopeLabel } for \" ${ state . searchQuery } \"...` ;
2026-03-15 16:20:43 -07:00
card . append ( summary , actions );
2026-03-09 23:27:03 -07:00
return card ;
}
if ( state . searchError ) {
summary . textContent = `Search scope: ${ scopeLabel } ` ;
2026-03-15 16:20:43 -07:00
card . append ( summary , actions , createEmptyMessage ( state . searchError ));
2026-03-09 23:27:03 -07:00
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 } ` ;
2026-03-15 16:20:43 -07:00
card . append ( summary , actions );
2026-03-09 23:27:03 -07:00
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 () {
2026-03-12 04:37:26 -07:00
return ( state . activeSearchScope === "global" || state . activeSearchScope === "source" )
2026-03-09 23:27:03 -07:00
&& Boolean ( state . searchQuery )
&& ! state . highlightedVerseId ;
}
function renderDetail () {
const source = getSelectedSource ();
const work = getSelectedWork ( source );
const section = getSelectedSection ( source , work );
2026-03-14 00:45:15 -07:00
const compareEnabled = isCompareModeEnabled ( getSelectedSourceGroup ());
2026-03-09 23:27:03 -07:00
const globalSearchOnlyMode = isGlobalSearchOnlyMode ();
2026-03-12 04:37:26 -07:00
setGlobalSearchHeadingMode ( globalSearchOnlyMode );
2026-03-09 23:27:03 -07:00
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"
2026-03-14 00:45:15 -07:00
: buildSourceDetailSubtitle ( source , work );
2026-03-09 23:27:03 -07:00
}
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 ));
2026-03-14 00:45:15 -07:00
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 ));
}
2026-03-09 23:27:03 -07:00
renderLexiconPopup ();
}
2026-03-14 00:45:15 -07:00
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." );
}
}
2026-03-09 23:27:03 -07:00
async function loadSelectedPassage () {
const source = getSelectedSource ();
const work = getSelectedWork ( source );
const section = getSelectedSection ( source , work );
2026-03-14 00:45:15 -07:00
const compareSource = isCompareModeEnabled ( getSelectedSourceGroup ()) ? getCompareSource () : null ;
2026-03-09 23:27:03 -07:00
if ( ! source || ! work || ! section ) {
state . currentPassage = null ;
2026-03-14 00:45:15 -07:00
state . comparePassage = null ;
2026-03-09 23:27:03 -07:00
renderDetail ();
return ;
}
state . currentPassage = null ;
2026-03-14 00:45:15 -07:00
state . comparePassage = null ;
2026-03-09 23:27:03 -07:00
renderDetail ();
2026-03-14 00:45:15 -07:00
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 );
2026-03-09 23:27:03 -07:00
}
}
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 ;
}
2026-03-12 04:37:26 -07:00
if ( normalizedScope === "global" || normalizedScope === "source" ) {
showDetailOnlyMode ();
}
2026-03-09 23:27:03 -07:00
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 ;
}
2026-03-14 00:45:15 -07:00
const sourceGroup = findSourceGroupBySourceId ( result . sourceId );
state . selectedSourceGroupId = sourceGroup ? . id || "" ;
2026-03-09 23:27:03 -07:00
state . selectedSourceId = result . sourceId ;
2026-03-14 00:45:15 -07:00
rememberSelectedSource ( sourceGroup , result . sourceId );
2026-03-09 23:27:03 -07:00
state . selectedWorkId = result . workId ;
state . selectedSectionId = result . sectionId ;
state . highlightedVerseId = result . verseId ;
dismissLexiconEntry ({ restoreFocus : false });
syncSelectionForSource ( getSelectedSource ());
renderSourceList ();
renderSelectors ();
2026-03-12 02:35:02 -07:00
showDetailOnlyMode ();
2026-03-09 23:27:03 -07:00
await loadSelectedPassage ();
2026-03-10 14:55:01 -07:00
clearActiveSearchUi ({ preserveHighlight : true });
renderDetail ();
2026-03-09 23:27:03 -07:00
}
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 ();
}
});
}
2026-03-15 16:20:43 -07:00
if ( globalSearchClearEl instanceof HTMLButtonElement ) {
globalSearchClearEl . addEventListener ( "click" , () => {
clearScopedSearch ( "global" );
renderDetail ();
});
}
2026-03-09 23:27:03 -07:00
if ( localSearchFormEl instanceof HTMLFormElement ) {
localSearchFormEl . addEventListener ( "submit" , ( event ) => {
event . preventDefault ();
void runSearch ( "source" );
});
}
2026-03-15 16:20:43 -07:00
if ( localSearchClearEl instanceof HTMLButtonElement ) {
localSearchClearEl . addEventListener ( "click" , () => {
clearScopedSearch ( "source" );
renderDetail ();
});
}
2026-03-09 23:27:03 -07:00
if ( localSearchInputEl instanceof HTMLInputElement ) {
localSearchInputEl . addEventListener ( "input" , () => {
state . localSearchQuery = String ( localSearchInputEl . value || "" ). trim ();
updateSearchControls ();
if ( ! state . localSearchQuery && state . activeSearchScope === "source" && state . searchQuery ) {
clearSearchState ();
renderDetail ();
}
});
}
2026-03-15 16:20:43 -07:00
if ( showVerseHeadsEl instanceof HTMLInputElement ) {
showVerseHeadsEl . addEventListener ( "change" , () => {
state . showVerseHeads = Boolean ( showVerseHeadsEl . checked );
writeStoredBoolean ( STORAGE_KEYS . showVerseHeads , state . showVerseHeads );
syncReaderDisplayControls ();
});
}
2026-03-14 00:45:15 -07:00
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 ();
});
}
2026-03-09 23:27:03 -07:00
if ( workSelectEl ) {
workSelectEl . addEventListener ( "change" , () => {
state . selectedWorkId = String ( workSelectEl . value || "" );
const source = getSelectedSource ();
syncSelectionForSource ( source );
state . currentPassage = null ;
2026-03-14 00:45:15 -07:00
state . comparePassage = null ;
2026-03-09 23:27:03 -07:00
state . lexiconEntry = null ;
state . highlightedVerseId = "" ;
renderSelectors ();
void loadSelectedPassage ();
});
}
if ( sectionSelectEl ) {
sectionSelectEl . addEventListener ( "change" , () => {
state . selectedSectionId = String ( sectionSelectEl . value || "" );
state . currentPassage = null ;
2026-03-14 00:45:15 -07:00
state . comparePassage = null ;
2026-03-09 23:27:03 -07:00
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 ();
2026-03-12 02:35:02 -07:00
window . TarotChromeUi ? . initializeSidebarPopouts ? .();
window . TarotChromeUi ? . initializeDetailPopouts ? .();
2026-03-09 23:27:03 -07:00
if ( ! sourceListEl || ! detailBodyEl ) {
return ;
}
await ensureCatalogLoaded ();
2026-03-15 16:20:43 -07:00
syncReaderDisplayControls ();
2026-03-09 23:27:03 -07:00
renderSourceList ();
renderSelectors ();
updateSearchControls ();
2026-03-15 16:20:43 -07:00
showSidebarOnlyMode ( false );
2026-03-09 23:27:03 -07:00
if ( ! state . currentPassage ) {
await loadSelectedPassage ();
return ;
}
renderDetail ();
}
function resetState () {
state . catalog = null ;
state . currentPassage = null ;
2026-03-14 00:45:15 -07:00
state . comparePassage = null ;
2026-03-09 23:27:03 -07:00
state . lexiconEntry = null ;
2026-03-14 00:45:15 -07:00
state . selectedSourceGroupId = "" ;
2026-03-09 23:27:03 -07:00
state . selectedSourceId = "" ;
2026-03-14 00:45:15 -07:00
state . selectedSourceIdByGroup = {};
state . compareSourceIdByGroup = {};
state . compareModeByGroup = {};
2026-03-09 23:27:03 -07:00
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
};
})();