import { invoke } from '@tauri-apps/api/core' import type { Footnote, LibraryState, StrongLink, Tag, Verse } from './types' type TauriWindow = Window & typeof globalThis & { __TAURI_INTERNALS__?: unknown } const browserTagKey = 'libre-study-browser-tags' const libreBibleDevRoot = '/@fs/W:/libre-bible-data/packages/json' type ImportedVerse = { id: string book: string chapter: number verse: number text: string } type ImportedStrongLink = { verse_id: string position: number surface: string strong: string } type ImportedFootnote = { id: number verse_id: string note_index: number caller: string reference: string text: 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 verseIdsByStrong: Map> } let browserLibraryCache: Promise | null = null let browserStrongDataCache: Promise | null = null let browserFootnotesCache: Promise> | null = null const browserVerseSourceIds = new Map() const fallbackBrowserVerses: Verse[] = [ { id: 1, book: 'Genesis', chapter: 1, verse: 1, text: 'In the beginning God created the heaven and the earth.', tags: [], footnotes: [], strongs: [ { strong: 'H7225', surface: 'beginning', position: 1, lemma: 'רֵאשִׁית', transliteration: 'reshith', definition: 'Beginning, first, chief, the first part.', language: 'Hebrew', }, { strong: 'H430', surface: 'God', position: 2, lemma: 'אֱלֹהִים', transliteration: 'elohim', definition: 'God, gods, judges, rulers; plural form often used of the one true God.', language: 'Hebrew', }, ], }, { id: 2, book: 'Psalm', chapter: 23, verse: 1, text: 'The LORD is my shepherd; I shall not want.', tags: [], footnotes: [], strongs: [ { strong: 'H3068', surface: 'LORD', position: 1, lemma: 'יְהוָֹה', transliteration: 'YHWH', definition: 'The covenant name of the LORD.', language: 'Hebrew', }, { strong: 'H7462', surface: 'shepherd', position: 2, lemma: 'רָעָה', transliteration: 'raah', definition: 'To shepherd, pasture, feed, tend a flock.', language: 'Hebrew', }, ], }, { id: 3, book: 'John', chapter: 3, verse: 16, text: 'For God so loved the world, that he gave his only begotten Son, that whosoever believeth in him should not perish, but have everlasting life.', tags: [], footnotes: [], strongs: [ { strong: 'G25', surface: 'loved', position: 1, lemma: 'ἀγαπάω', transliteration: 'agapao', definition: 'To love, value, welcome, or take pleasure in.', language: 'Greek', }, { strong: 'G4100', surface: 'believeth', position: 3, lemma: 'πιστεύω', transliteration: 'pisteuo', definition: 'To believe, trust, have faith in.', language: 'Greek', }, ], }, { id: 4, book: 'Romans', chapter: 8, verse: 28, text: 'And we know that all things work together for good to them that love God, to them who are the called according to his purpose.', tags: [], footnotes: [], strongs: [ { strong: 'G2822', surface: 'called', position: 2, lemma: 'κλητός', transliteration: 'kletos', definition: 'Called, invited, appointed.', language: 'Greek', }, { strong: 'G4286', surface: 'purpose', position: 3, lemma: 'πρόθεσις', transliteration: 'prothesis', definition: 'Purpose, setting forth, deliberate plan.', language: 'Greek', }, ], }, ] function inTauri() { return Boolean((window as TauriWindow).__TAURI_INTERNALS__) } function readBrowserTags(): Record { const raw = localStorage.getItem(browserTagKey) if (!raw) return {} try { return JSON.parse(raw) as Record } catch { return {} } } function writeBrowserTags(tagsByVerse: Record) { localStorage.setItem(browserTagKey, JSON.stringify(tagsByVerse)) } async function loadBrowserVerses() { browserLibraryCache ??= loadLibreBibleDevVerses().catch((error) => { console.warn('Falling back to browser seed data.', error) return fallbackBrowserVerses }) const verses = await browserLibraryCache const strongData = await loadBrowserStrongData().catch((error) => { console.warn('Unable to load browser Strong data.', error) return null }) const footnotes = await loadBrowserFootnotes().catch((error) => { console.warn('Unable to load browser footnotes.', error) return new Map() }) const tagsByVerse = readBrowserTags() return verses.map((verse) => ({ ...verse, strongs: strongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs, footnotes: footnotes.get(browserVerseKey(verse)) ?? verse.footnotes ?? [], tags: tagsByVerse[verse.id] ?? [], })) } function allBrowserTags() { const unique = new Map() for (const tags of Object.values(readBrowserTags())) { for (const tag of tags) { unique.set(tag.label.toLowerCase(), tag) } } return [...unique.values()].sort((a, b) => a.label.localeCompare(b.label)) } export async function getLibraryState(): Promise { if (inTauri()) { return invoke('get_library_state') } return { verses: await loadBrowserVerses(), tags: allBrowserTags(), } } export async function searchVerses(query: string): Promise { if (inTauri()) { return invoke('search_verses', { query }) } const trimmed = query.trim().toLowerCase() const verses = await loadBrowserVerses() if (!trimmed) return verses if (trimmed.startsWith('tag:')) { const label = trimmed.slice(4) return verses.filter((verse) => verse.tags.some((tag) => tag.label.toLowerCase() === label), ) } if (/^[gh]\d+$/.test(trimmed)) { const strongData = await loadBrowserStrongData() const matchingVerseIds = strongData.verseIdsByStrong.get(trimmed.toUpperCase()) ?? new Set() return verses.filter((verse) => matchingVerseIds.has(browserVerseKey(verse)), ) } return verses.filter((verse) => `${verse.book} ${verse.chapter}:${verse.verse} ${verse.text}` .toLowerCase() .includes(trimmed), ) } export async function addTagToVerse(verseId: number, label: string, color: string) { if (inTauri()) { return invoke('add_tag_to_verse', { verseId, label, color }) } const tagsByVerse = readBrowserTags() const current = tagsByVerse[verseId] ?? [] const existing = current.find((tag) => tag.label.toLowerCase() === label.toLowerCase()) const tag = existing ?? { id: Date.now(), label, color } tagsByVerse[verseId] = existing ? current : [...current, tag] writeBrowserTags(tagsByVerse) 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 { const importedVerses = await fetchJsonl( `${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: [], footnotes: [], tags: [], } }) } async function loadBrowserFootnotes(): Promise> { browserFootnotesCache ??= loadLibreBibleDevFootnotes() return browserFootnotesCache } async function loadLibreBibleDevFootnotes(): Promise> { const importedFootnotes = await fetchJsonl( `${libreBibleDevRoot}/kjv-eng-kjv2006/footnotes.jsonl`, ) const footnotes = new Map() for (const note of importedFootnotes) { const current = footnotes.get(note.verse_id) ?? [] current.push({ id: note.id, verseId: note.verse_id, noteIndex: note.note_index, caller: note.caller, reference: note.reference, text: note.text, }) footnotes.set(note.verse_id, current) } return footnotes } async function loadBrowserStrongData(): Promise { browserStrongDataCache ??= loadLibreBibleDevStrongData() return browserStrongDataCache } async function loadLibreBibleDevStrongData(): Promise { const [importedLinks, importedEntries] = await Promise.all([ fetchJsonl(`${libreBibleDevRoot}/kjv-eng-kjv2006/strongs-links.jsonl`), fetchJsonl(`${libreBibleDevRoot}/strongs-open-scriptures/entries.jsonl`), ]) const lexicon = new Map( 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() const verseIdsByStrong = new Map>() 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() 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(url: string): Promise { 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) }