Load real LibreBible data in browser preview

This commit is contained in:
2026-07-12 08:09:03 -05:00
parent 81ba163207
commit 341c4a5ab7
3 changed files with 171 additions and 10 deletions
+159 -8
View File
@@ -1,5 +1,5 @@
import { invoke } from '@tauri-apps/api/core'
import type { LibraryState, Tag, Verse } from './types'
import type { LibraryState, StrongLink, Tag, Verse } from './types'
type TauriWindow = Window &
typeof globalThis & {
@@ -7,8 +7,51 @@ type TauriWindow = Window &
}
const browserTagKey = 'libre-study-browser-tags'
const libreBibleDevRoot = '/@fs/W:/libre-bible-data/packages/json'
const browserVerses: Verse[] = [
type ImportedVerse = {
id: string
book: string
chapter: number
verse: number
text: string
}
type ImportedStrongLink = {
verse_id: string
position: number
surface: string
strong: string
}
type ImportedStrongEntry = {
strongs_number: string
language: 'hebrew' | 'greek'
lemma?: string
transliteration?: string
pronunciation?: string
strongs_definition?: string
kjv_definition?: string
}
type BrowserLexiconEntry = {
lemma: string
transliteration: string
definition: string
language: 'Hebrew' | 'Greek'
}
type BrowserStrongData = {
linksByVerse: Map<string, StrongLink[]>
verseIdsByStrong: Map<string, Set<string>>
}
let browserLibraryCache: Promise<Verse[]> | null = null
let browserStrongDataCache: Promise<BrowserStrongData> | null = null
let browserStrongData: BrowserStrongData | null = null
const browserVerseSourceIds = new Map<number, string>()
const fallbackBrowserVerses: Verse[] = [
{
id: 1,
book: 'Genesis',
@@ -142,10 +185,18 @@ function writeBrowserTags(tagsByVerse: Record<number, Tag[]>) {
localStorage.setItem(browserTagKey, JSON.stringify(tagsByVerse))
}
function hydrateBrowserVerses() {
async function loadBrowserVerses() {
browserLibraryCache ??= loadLibreBibleDevVerses().catch((error) => {
console.warn('Falling back to browser seed data.', error)
return fallbackBrowserVerses
})
const verses = await browserLibraryCache
void loadBrowserStrongData()
const tagsByVerse = readBrowserTags()
return browserVerses.map((verse) => ({
return verses.map((verse) => ({
...verse,
strongs: browserStrongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs,
tags: tagsByVerse[verse.id] ?? [],
}))
}
@@ -166,7 +217,7 @@ export async function getLibraryState(): Promise<LibraryState> {
}
return {
verses: hydrateBrowserVerses(),
verses: await loadBrowserVerses(),
tags: allBrowserTags(),
}
}
@@ -177,7 +228,7 @@ export async function searchVerses(query: string): Promise<Verse[]> {
}
const trimmed = query.trim().toLowerCase()
const verses = hydrateBrowserVerses()
const verses = await loadBrowserVerses()
if (!trimmed) return verses
if (trimmed.startsWith('tag:')) {
@@ -188,8 +239,10 @@ export async function searchVerses(query: string): Promise<Verse[]> {
}
if (/^[gh]\d+$/.test(trimmed)) {
const strongData = await loadBrowserStrongData()
const matchingVerseIds = strongData.verseIdsByStrong.get(trimmed.toUpperCase()) ?? new Set()
return verses.filter((verse) =>
verse.strongs.some((entry) => entry.strong.toLowerCase() === trimmed),
matchingVerseIds.has(browserVerseKey(verse)),
)
}
@@ -212,7 +265,105 @@ export async function addTagToVerse(verseId: number, label: string, color: strin
tagsByVerse[verseId] = existing ? current : [...current, tag]
writeBrowserTags(tagsByVerse)
const verse = hydrateBrowserVerses().find((item) => item.id === verseId)
const verse = (await loadBrowserVerses()).find((item) => item.id === verseId)
if (!verse) throw new Error(`Verse ${verseId} was not found.`)
return verse
}
async function loadLibreBibleDevVerses(): Promise<Verse[]> {
const importedVerses = await fetchJsonl<ImportedVerse>(
`${libreBibleDevRoot}/kjv-eng-kjv2006/verses.jsonl`,
)
browserVerseSourceIds.clear()
return importedVerses.map((verse, index) => {
const id = index + 1
browserVerseSourceIds.set(id, verse.id)
return {
id,
book: verse.book,
chapter: verse.chapter,
verse: verse.verse,
text: verse.text,
strongs: [],
tags: [],
}
})
}
async function loadBrowserStrongData(): Promise<BrowserStrongData> {
browserStrongDataCache ??= loadLibreBibleDevStrongData().then((data) => {
browserStrongData = data
return data
})
return browserStrongDataCache
}
async function loadLibreBibleDevStrongData(): Promise<BrowserStrongData> {
const [importedLinks, importedEntries] = await Promise.all([
fetchJsonl<ImportedStrongLink>(`${libreBibleDevRoot}/kjv-eng-kjv2006/strongs-links.jsonl`),
fetchJsonl<ImportedStrongEntry>(`${libreBibleDevRoot}/strongs-open-scriptures/entries.jsonl`),
])
const lexicon = new Map<string, BrowserLexiconEntry>(
importedEntries.map((entry) => [
entry.strongs_number,
{
lemma: entry.lemma ?? '',
transliteration: entry.transliteration ?? '',
definition: entry.strongs_definition ?? entry.kjv_definition ?? '',
language: entry.language === 'hebrew' ? 'Hebrew' : 'Greek',
},
]),
)
const linksByVerse = new Map<string, StrongLink[]>()
const verseIdsByStrong = new Map<string, Set<string>>()
for (const link of importedLinks) {
const entry = lexicon.get(link.strong)
const strongLink: StrongLink = {
strong: link.strong,
surface: link.surface,
position: link.position,
lemma: entry?.lemma ?? link.surface,
transliteration: entry?.transliteration ?? '',
definition: entry?.definition ?? '',
language: entry?.language ?? strongLanguage(link.strong),
}
const current = linksByVerse.get(link.verse_id) ?? []
current.push(strongLink)
linksByVerse.set(link.verse_id, current)
const strongMatches = verseIdsByStrong.get(link.strong) ?? new Set<string>()
strongMatches.add(link.verse_id)
verseIdsByStrong.set(link.strong, strongMatches)
}
for (const links of linksByVerse.values()) {
links.sort((a, b) => a.position - b.position)
}
return { linksByVerse, verseIdsByStrong }
}
function strongLanguage(strong: string): 'Hebrew' | 'Greek' {
return strong.startsWith('H') ? 'Hebrew' : 'Greek'
}
function browserVerseKey(verse: Verse) {
return browserVerseSourceIds.get(verse.id) ?? ''
}
async function fetchJsonl<T>(url: string): Promise<T[]> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Unable to load ${url}: ${response.status} ${response.statusText}`)
}
const text = await response.text()
return text
.split('\n')
.filter((line) => line.trim().length > 0)
.map((line) => JSON.parse(line) as T)
}