Load translations from LibreBible catalog
This commit is contained in:
+130
-29
@@ -1,5 +1,14 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { CommentaryEntry, DictionaryEntry, Footnote, LibraryState, StrongLink, Tag, Verse } from './types'
|
||||
import type {
|
||||
CommentaryEntry,
|
||||
DictionaryEntry,
|
||||
Footnote,
|
||||
LibraryState,
|
||||
StrongLink,
|
||||
Tag,
|
||||
TranslationResource,
|
||||
Verse,
|
||||
} from './types'
|
||||
|
||||
type TauriWindow = Window &
|
||||
typeof globalThis & {
|
||||
@@ -8,9 +17,11 @@ type TauriWindow = Window &
|
||||
|
||||
const browserTagKey = 'libre-study-browser-tags'
|
||||
const libreBibleDevRoot = '/@fs/W:/libre-bible-data/packages/json'
|
||||
const defaultTranslationId = 'kjv-eng-kjv2006'
|
||||
|
||||
type ImportedVerse = {
|
||||
id: string
|
||||
translation_id: string
|
||||
book: string
|
||||
chapter: number
|
||||
verse: number
|
||||
@@ -28,11 +39,28 @@ type ImportedFootnote = {
|
||||
id: number
|
||||
verse_id: string
|
||||
note_index: number
|
||||
note_type?: string
|
||||
label?: string
|
||||
caller: string
|
||||
reference: string
|
||||
text: string
|
||||
}
|
||||
|
||||
type ImportedCatalog = {
|
||||
resources: Array<{
|
||||
id: string
|
||||
resource_type: string
|
||||
title: string
|
||||
short_title?: string
|
||||
abbreviation?: string
|
||||
license?: {
|
||||
name?: string
|
||||
attribution?: string
|
||||
restricted_notes?: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
type ImportedStrongEntry = {
|
||||
strongs_number: string
|
||||
language: 'hebrew' | 'greek'
|
||||
@@ -85,9 +113,11 @@ type BrowserStrongData = {
|
||||
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
|
||||
let browserCatalogCache: Promise<ImportedCatalog> | null = null
|
||||
const browserLibraryCache = new Map<string, Promise<Verse[]>>()
|
||||
const browserStrongDataCache = new Map<string, Promise<BrowserStrongData>>()
|
||||
const browserFootnotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
|
||||
const browserStudyNotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
|
||||
let browserCommentaryCache: Promise<Map<string, CommentaryEntry[]>> | null = null
|
||||
let browserDictionaryCache: Promise<Map<string, DictionaryEntry>> | null = null
|
||||
const browserVerseSourceIds = new Map<number, string>()
|
||||
@@ -230,26 +260,33 @@ function writeBrowserTags(tagsByVerse: Record<number, Tag[]>) {
|
||||
localStorage.setItem(browserTagKey, JSON.stringify(tagsByVerse))
|
||||
}
|
||||
|
||||
async function loadBrowserVerses() {
|
||||
browserLibraryCache ??= loadLibreBibleDevVerses().catch((error) => {
|
||||
async function loadBrowserVersesForTranslation(translationId = defaultTranslationId) {
|
||||
if (!browserLibraryCache.has(translationId)) {
|
||||
browserLibraryCache.set(translationId, loadLibreBibleDevVerses(translationId).catch((error) => {
|
||||
console.warn('Falling back to browser seed data.', error)
|
||||
return fallbackBrowserVerses
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const verses = await browserLibraryCache
|
||||
const strongData = await loadBrowserStrongData().catch((error) => {
|
||||
const verses = await browserLibraryCache.get(translationId)!
|
||||
const strongData = await loadBrowserStrongData(translationId).catch((error) => {
|
||||
console.warn('Unable to load browser Strong data.', error)
|
||||
return null
|
||||
})
|
||||
const footnotes = await loadBrowserFootnotes().catch((error) => {
|
||||
const footnotes = await loadBrowserFootnotes(translationId).catch((error) => {
|
||||
console.warn('Unable to load browser footnotes.', error)
|
||||
return new Map<string, Footnote[]>()
|
||||
})
|
||||
const studyNotes = await loadBrowserStudyNotes(translationId).catch((error) => {
|
||||
console.warn('Unable to load browser study notes.', 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 ?? [],
|
||||
studyNotes: studyNotes.get(browserVerseKey(verse)) ?? verse.studyNotes ?? [],
|
||||
tags: tagsByVerse[verse.id] ?? [],
|
||||
}))
|
||||
}
|
||||
@@ -264,24 +301,41 @@ function allBrowserTags() {
|
||||
return [...unique.values()].sort((a, b) => a.label.localeCompare(b.label))
|
||||
}
|
||||
|
||||
export async function getLibraryState(): Promise<LibraryState> {
|
||||
export async function getTranslations(): Promise<TranslationResource[]> {
|
||||
if (inTauri()) return []
|
||||
|
||||
const catalog = await loadBrowserCatalog()
|
||||
return catalog.resources
|
||||
.filter((resource) => resource.resource_type === 'translation')
|
||||
.map((resource) => ({
|
||||
id: resource.id,
|
||||
title: resource.title,
|
||||
shortTitle: resource.short_title ?? resource.title,
|
||||
abbreviation: resource.abbreviation ?? resource.short_title ?? resource.title,
|
||||
licenseName: resource.license?.name ?? '',
|
||||
attribution: resource.license?.attribution ?? '',
|
||||
restrictions: resource.license?.restricted_notes ?? '',
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getLibraryState(translationId = defaultTranslationId): Promise<LibraryState> {
|
||||
if (inTauri()) {
|
||||
return invoke<LibraryState>('get_library_state')
|
||||
}
|
||||
|
||||
return {
|
||||
verses: await loadBrowserVerses(),
|
||||
verses: await loadBrowserVersesForTranslation(translationId),
|
||||
tags: allBrowserTags(),
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchVerses(query: string): Promise<Verse[]> {
|
||||
export async function searchVerses(query: string, translationId = defaultTranslationId): Promise<Verse[]> {
|
||||
if (inTauri()) {
|
||||
return invoke<Verse[]>('search_verses', { query })
|
||||
}
|
||||
|
||||
const trimmed = query.trim().toLowerCase()
|
||||
const verses = await loadBrowserVerses()
|
||||
const verses = await loadBrowserVersesForTranslation(translationId)
|
||||
if (!trimmed) return verses
|
||||
|
||||
if (trimmed.startsWith('tag:')) {
|
||||
@@ -292,7 +346,7 @@ export async function searchVerses(query: string): Promise<Verse[]> {
|
||||
}
|
||||
|
||||
if (/^[gh]\d+$/.test(trimmed)) {
|
||||
const strongData = await loadBrowserStrongData()
|
||||
const strongData = await loadBrowserStrongData(translationId)
|
||||
const matchingVerseIds = strongData.verseIdsByStrong.get(trimmed.toUpperCase()) ?? new Set()
|
||||
return verses.filter((verse) =>
|
||||
matchingVerseIds.has(browserVerseKey(verse)),
|
||||
@@ -306,7 +360,12 @@ export async function searchVerses(query: string): Promise<Verse[]> {
|
||||
)
|
||||
}
|
||||
|
||||
export async function addTagToVerse(verseId: number, label: string, color: string) {
|
||||
export async function addTagToVerse(
|
||||
verseId: number,
|
||||
label: string,
|
||||
color: string,
|
||||
translationId = defaultTranslationId,
|
||||
) {
|
||||
if (inTauri()) {
|
||||
return invoke<Verse>('add_tag_to_verse', { verseId, label, color })
|
||||
}
|
||||
@@ -318,7 +377,7 @@ export async function addTagToVerse(verseId: number, label: string, color: strin
|
||||
tagsByVerse[verseId] = existing ? current : [...current, tag]
|
||||
writeBrowserTags(tagsByVerse)
|
||||
|
||||
const verse = (await loadBrowserVerses()).find((item) => item.id === verseId)
|
||||
const verse = (await loadBrowserVersesForTranslation(translationId)).find((item) => item.id === verseId)
|
||||
if (!verse) throw new Error(`Verse ${verseId} was not found.`)
|
||||
return verse
|
||||
}
|
||||
@@ -352,9 +411,14 @@ export async function getDictionaryEntriesForTerms(terms: string[]): Promise<Dic
|
||||
return entries
|
||||
}
|
||||
|
||||
async function loadLibreBibleDevVerses(): Promise<Verse[]> {
|
||||
async function loadBrowserCatalog(): Promise<ImportedCatalog> {
|
||||
browserCatalogCache ??= fetchJson<ImportedCatalog>(`${libreBibleDevRoot}/catalog.json`)
|
||||
return browserCatalogCache
|
||||
}
|
||||
|
||||
async function loadLibreBibleDevVerses(translationId = defaultTranslationId): Promise<Verse[]> {
|
||||
const importedVerses = await fetchJsonl<ImportedVerse>(
|
||||
`${libreBibleDevRoot}/kjv-eng-kjv2006/verses.jsonl`,
|
||||
`${libreBibleDevRoot}/${translationId}/verses.jsonl`,
|
||||
)
|
||||
|
||||
browserVerseSourceIds.clear()
|
||||
@@ -369,6 +433,7 @@ async function loadLibreBibleDevVerses(): Promise<Verse[]> {
|
||||
text: verse.text,
|
||||
strongs: [],
|
||||
footnotes: [],
|
||||
studyNotes: [],
|
||||
tags: [],
|
||||
}
|
||||
})
|
||||
@@ -442,16 +507,39 @@ async function loadLibreBibleDevDictionary(): Promise<Map<string, DictionaryEntr
|
||||
)
|
||||
}
|
||||
|
||||
async function loadBrowserFootnotes(): Promise<Map<string, Footnote[]>> {
|
||||
browserFootnotesCache ??= loadLibreBibleDevFootnotes()
|
||||
async function loadBrowserFootnotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
|
||||
if (!browserFootnotesCache.has(translationId)) {
|
||||
browserFootnotesCache.set(translationId, loadLibreBibleDevFootnotes(translationId))
|
||||
}
|
||||
|
||||
return browserFootnotesCache
|
||||
return browserFootnotesCache.get(translationId)!
|
||||
}
|
||||
|
||||
async function loadLibreBibleDevFootnotes(): Promise<Map<string, Footnote[]>> {
|
||||
async function loadBrowserStudyNotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
|
||||
if (!browserStudyNotesCache.has(translationId)) {
|
||||
browserStudyNotesCache.set(translationId, loadLibreBibleDevStudyNotes(translationId))
|
||||
}
|
||||
|
||||
return browserStudyNotesCache.get(translationId)!
|
||||
}
|
||||
|
||||
async function loadLibreBibleDevFootnotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
|
||||
const importedFootnotes = await fetchJsonl<ImportedFootnote>(
|
||||
`${libreBibleDevRoot}/kjv-eng-kjv2006/footnotes.jsonl`,
|
||||
`${libreBibleDevRoot}/${translationId}/footnotes.jsonl`,
|
||||
)
|
||||
|
||||
return mapImportedNotes(importedFootnotes)
|
||||
}
|
||||
|
||||
async function loadLibreBibleDevStudyNotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
|
||||
const importedFootnotes = await fetchJsonl<ImportedFootnote>(
|
||||
`${libreBibleDevRoot}/${translationId}/study-notes.jsonl`,
|
||||
)
|
||||
|
||||
return mapImportedNotes(importedFootnotes)
|
||||
}
|
||||
|
||||
function mapImportedNotes(importedFootnotes: ImportedFootnote[]): Map<string, Footnote[]> {
|
||||
const footnotes = new Map<string, Footnote[]>()
|
||||
|
||||
for (const note of importedFootnotes) {
|
||||
@@ -463,6 +551,8 @@ async function loadLibreBibleDevFootnotes(): Promise<Map<string, Footnote[]>> {
|
||||
caller: note.caller,
|
||||
reference: note.reference,
|
||||
text: note.text,
|
||||
label: note.label,
|
||||
noteType: note.note_type,
|
||||
})
|
||||
footnotes.set(note.verse_id, current)
|
||||
}
|
||||
@@ -470,15 +560,17 @@ async function loadLibreBibleDevFootnotes(): Promise<Map<string, Footnote[]>> {
|
||||
return footnotes
|
||||
}
|
||||
|
||||
async function loadBrowserStrongData(): Promise<BrowserStrongData> {
|
||||
browserStrongDataCache ??= loadLibreBibleDevStrongData()
|
||||
async function loadBrowserStrongData(translationId = defaultTranslationId): Promise<BrowserStrongData> {
|
||||
if (!browserStrongDataCache.has(translationId)) {
|
||||
browserStrongDataCache.set(translationId, loadLibreBibleDevStrongData(translationId))
|
||||
}
|
||||
|
||||
return browserStrongDataCache
|
||||
return browserStrongDataCache.get(translationId)!
|
||||
}
|
||||
|
||||
async function loadLibreBibleDevStrongData(): Promise<BrowserStrongData> {
|
||||
async function loadLibreBibleDevStrongData(translationId = defaultTranslationId): Promise<BrowserStrongData> {
|
||||
const [importedLinks, importedEntries] = await Promise.all([
|
||||
fetchJsonl<ImportedStrongLink>(`${libreBibleDevRoot}/kjv-eng-kjv2006/strongs-links.jsonl`),
|
||||
fetchJsonl<ImportedStrongLink>(`${libreBibleDevRoot}/${translationId}/strongs-links.jsonl`),
|
||||
fetchJsonl<ImportedStrongEntry>(`${libreBibleDevRoot}/strongs-open-scriptures/entries.jsonl`),
|
||||
])
|
||||
|
||||
@@ -551,3 +643,12 @@ async function fetchJsonl<T>(url: string): Promise<T[]> {
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.map((line) => JSON.parse(line) as T)
|
||||
}
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to load ${url}: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user