417 lines
11 KiB
TypeScript
417 lines
11 KiB
TypeScript
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<string, StrongLink[]>
|
|
verseIdsByStrong: Map<string, Set<string>>
|
|
}
|
|
|
|
let browserLibraryCache: Promise<Verse[]> | null = null
|
|
let browserStrongDataCache: Promise<BrowserStrongData> | null = null
|
|
let browserFootnotesCache: Promise<Map<string, Footnote[]>> | null = null
|
|
const browserVerseSourceIds = new Map<number, string>()
|
|
|
|
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<number, Tag[]> {
|
|
const raw = localStorage.getItem(browserTagKey)
|
|
if (!raw) return {}
|
|
|
|
try {
|
|
return JSON.parse(raw) as Record<number, Tag[]>
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
function writeBrowserTags(tagsByVerse: Record<number, Tag[]>) {
|
|
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<string, Footnote[]>()
|
|
})
|
|
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<string, Tag>()
|
|
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<LibraryState> {
|
|
if (inTauri()) {
|
|
return invoke<LibraryState>('get_library_state')
|
|
}
|
|
|
|
return {
|
|
verses: await loadBrowserVerses(),
|
|
tags: allBrowserTags(),
|
|
}
|
|
}
|
|
|
|
export async function searchVerses(query: string): Promise<Verse[]> {
|
|
if (inTauri()) {
|
|
return invoke<Verse[]>('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<Verse>('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<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: [],
|
|
footnotes: [],
|
|
tags: [],
|
|
}
|
|
})
|
|
}
|
|
|
|
async function loadBrowserFootnotes(): Promise<Map<string, Footnote[]>> {
|
|
browserFootnotesCache ??= loadLibreBibleDevFootnotes()
|
|
|
|
return browserFootnotesCache
|
|
}
|
|
|
|
async function loadLibreBibleDevFootnotes(): Promise<Map<string, Footnote[]>> {
|
|
const importedFootnotes = await fetchJsonl<ImportedFootnote>(
|
|
`${libreBibleDevRoot}/kjv-eng-kjv2006/footnotes.jsonl`,
|
|
)
|
|
const footnotes = new Map<string, Footnote[]>()
|
|
|
|
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<BrowserStrongData> {
|
|
browserStrongDataCache ??= loadLibreBibleDevStrongData()
|
|
|
|
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)
|
|
}
|