Load real LibreBible data in browser preview
This commit is contained in:
+7
-2
@@ -29,6 +29,8 @@ function App() {
|
|||||||
|
|
||||||
async function runSearch(nextQuery = query) {
|
async function runSearch(nextQuery = query) {
|
||||||
setError(null)
|
setError(null)
|
||||||
|
const trimmed = nextQuery.trim()
|
||||||
|
setSelectedStrong(/^[gh]\d+$/i.test(trimmed) ? trimmed.toUpperCase() : null)
|
||||||
const results = await searchVerses(nextQuery)
|
const results = await searchVerses(nextQuery)
|
||||||
setVerses(results)
|
setVerses(results)
|
||||||
setSelectedId(results[0]?.id ?? null)
|
setSelectedId(results[0]?.id ?? null)
|
||||||
@@ -68,6 +70,7 @@ function App() {
|
|||||||
selectedVerse.strongs[0]
|
selectedVerse.strongs[0]
|
||||||
)
|
)
|
||||||
}, [selectedStrong, selectedVerse])
|
}, [selectedStrong, selectedVerse])
|
||||||
|
const visibleVerses = useMemo(() => verses.slice(0, 250), [verses])
|
||||||
|
|
||||||
function searchStrong(strong: string) {
|
function searchStrong(strong: string) {
|
||||||
setQuery(strong)
|
setQuery(strong)
|
||||||
@@ -130,8 +133,10 @@ function App() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="result-list" aria-label="Search results">
|
<section className="result-list" aria-label="Search results">
|
||||||
<div className="section-title">{verses.length} verses</div>
|
<div className="section-title">
|
||||||
{verses.map((verse) => (
|
{verses.length} verses{verses.length > visibleVerses.length ? `, showing ${visibleVerses.length}` : ''}
|
||||||
|
</div>
|
||||||
|
{visibleVerses.map((verse) => (
|
||||||
<button
|
<button
|
||||||
key={verse.id}
|
key={verse.id}
|
||||||
className={verse.id === selectedVerse?.id ? 'result active' : 'result'}
|
className={verse.id === selectedVerse?.id ? 'result active' : 'result'}
|
||||||
|
|||||||
+159
-8
@@ -1,5 +1,5 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
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 &
|
type TauriWindow = Window &
|
||||||
typeof globalThis & {
|
typeof globalThis & {
|
||||||
@@ -7,8 +7,51 @@ type TauriWindow = Window &
|
|||||||
}
|
}
|
||||||
|
|
||||||
const browserTagKey = 'libre-study-browser-tags'
|
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,
|
id: 1,
|
||||||
book: 'Genesis',
|
book: 'Genesis',
|
||||||
@@ -142,10 +185,18 @@ function writeBrowserTags(tagsByVerse: Record<number, Tag[]>) {
|
|||||||
localStorage.setItem(browserTagKey, JSON.stringify(tagsByVerse))
|
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()
|
const tagsByVerse = readBrowserTags()
|
||||||
return browserVerses.map((verse) => ({
|
return verses.map((verse) => ({
|
||||||
...verse,
|
...verse,
|
||||||
|
strongs: browserStrongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs,
|
||||||
tags: tagsByVerse[verse.id] ?? [],
|
tags: tagsByVerse[verse.id] ?? [],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -166,7 +217,7 @@ export async function getLibraryState(): Promise<LibraryState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
verses: hydrateBrowserVerses(),
|
verses: await loadBrowserVerses(),
|
||||||
tags: allBrowserTags(),
|
tags: allBrowserTags(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +228,7 @@ export async function searchVerses(query: string): Promise<Verse[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const trimmed = query.trim().toLowerCase()
|
const trimmed = query.trim().toLowerCase()
|
||||||
const verses = hydrateBrowserVerses()
|
const verses = await loadBrowserVerses()
|
||||||
if (!trimmed) return verses
|
if (!trimmed) return verses
|
||||||
|
|
||||||
if (trimmed.startsWith('tag:')) {
|
if (trimmed.startsWith('tag:')) {
|
||||||
@@ -188,8 +239,10 @@ export async function searchVerses(query: string): Promise<Verse[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (/^[gh]\d+$/.test(trimmed)) {
|
if (/^[gh]\d+$/.test(trimmed)) {
|
||||||
|
const strongData = await loadBrowserStrongData()
|
||||||
|
const matchingVerseIds = strongData.verseIdsByStrong.get(trimmed.toUpperCase()) ?? new Set()
|
||||||
return verses.filter((verse) =>
|
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]
|
tagsByVerse[verseId] = existing ? current : [...current, tag]
|
||||||
writeBrowserTags(tagsByVerse)
|
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.`)
|
if (!verse) throw new Error(`Verse ${verseId} was not found.`)
|
||||||
return verse
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,4 +4,9 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
fs: {
|
||||||
|
allow: ['..', 'W:/libre-bible-data'],
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user