Render KJV footnotes and lemma margin line
This commit is contained in:
+74
-1
@@ -322,6 +322,56 @@ textarea:focus-visible {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.original-language-verse {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 0 0 14px;
|
||||
padding: 12px;
|
||||
border: 1px solid #dfd6c2;
|
||||
border-radius: 6px;
|
||||
background: #fffdf6;
|
||||
}
|
||||
|
||||
.original-language-verse h3 {
|
||||
margin: 0;
|
||||
color: #657061;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.original-language-verse p {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lemma-token {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
padding: 5px 7px;
|
||||
border-color: #d4c9b3;
|
||||
background: #fbf3df;
|
||||
color: #1f211e;
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lemma-token small {
|
||||
color: #735c0f;
|
||||
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.lemma-token.active {
|
||||
border-color: #2f6f5e;
|
||||
background: #e7f0eb;
|
||||
}
|
||||
|
||||
.linked-word-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -422,16 +472,39 @@ textarea:focus-visible {
|
||||
display: flex;
|
||||
grid-column: 1 / -1;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
min-height: 46px;
|
||||
border-top: 1px solid #e4dece;
|
||||
background: #fbf8ef;
|
||||
}
|
||||
|
||||
.footnote-strip strong {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.footnote-strip span {
|
||||
color: #657061;
|
||||
}
|
||||
|
||||
.footnote-list {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.footnote-list p {
|
||||
margin: 0;
|
||||
color: #3f443d;
|
||||
font-size: 14px;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
.footnote-list p span {
|
||||
display: inline-block;
|
||||
margin-right: 7px;
|
||||
color: #735c0f;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-panel.error {
|
||||
border-color: #b42318;
|
||||
color: #b42318;
|
||||
|
||||
+52
-4
@@ -118,6 +118,11 @@ function App() {
|
||||
const pageRange = pageVerses.length
|
||||
? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}`
|
||||
: 'No page loaded'
|
||||
const selectedFootnotes = selectedVerse?.footnotes ?? []
|
||||
const pageFootnotes = useMemo(
|
||||
() => pageVerses.flatMap((verse) => verse.footnotes ?? []),
|
||||
[pageVerses],
|
||||
)
|
||||
const books = useMemo(
|
||||
() => Array.from(new Set(libraryVerses.map((verse) => verse.book))),
|
||||
[libraryVerses],
|
||||
@@ -266,6 +271,30 @@ function App() {
|
||||
return parts.length ? parts : verse.text
|
||||
}
|
||||
|
||||
function renderOriginalLanguageLine() {
|
||||
if (!selectedVerse?.strongs.length) return null
|
||||
|
||||
return (
|
||||
<section className="original-language-verse" aria-label="Original language lemma line">
|
||||
<h3>Lemma Line</h3>
|
||||
<p>
|
||||
{selectedVerse.strongs.map((entry) => (
|
||||
<button
|
||||
key={`${entry.strong}-${entry.position}-lemma`}
|
||||
type="button"
|
||||
className={entry.strong === selectedStrongEntry?.strong ? 'lemma-token active' : 'lemma-token'}
|
||||
title={`${entry.surface}: ${entry.strong}`}
|
||||
onClick={() => setSelectedStrong(entry.strong)}
|
||||
>
|
||||
<span>{entry.lemma || entry.surface}</span>
|
||||
<small>{entry.strong}</small>
|
||||
</button>
|
||||
))}
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function renderBiblePage(label = 'KJV') {
|
||||
if (!selectedVerse) return null
|
||||
return (
|
||||
@@ -309,6 +338,7 @@ function App() {
|
||||
<span>{selectedVerse.strongs.length} links</span>
|
||||
</div>
|
||||
<p className="margin-reference">{reference(selectedVerse)}</p>
|
||||
{renderOriginalLanguageLine()}
|
||||
<div className="linked-word-list" aria-label="Linked Strong's words">
|
||||
{selectedVerse.strongs.map((entry) => (
|
||||
<button
|
||||
@@ -341,6 +371,27 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
function renderFootnoteStrip() {
|
||||
const visibleFootnotes = selectedFootnotes.length ? selectedFootnotes : pageFootnotes.slice(0, 4)
|
||||
return (
|
||||
<section className="footnote-strip">
|
||||
<strong>Footnotes</strong>
|
||||
{visibleFootnotes.length ? (
|
||||
<div className="footnote-list">
|
||||
{visibleFootnotes.map((note) => (
|
||||
<p key={note.id}>
|
||||
<span>{note.reference}</span>
|
||||
{note.text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span>No footnotes for the selected verse.</span>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<aside className="tool-rail" aria-label="Main tools">
|
||||
@@ -472,10 +523,7 @@ function App() {
|
||||
<div className={`page-system ${viewMode}`}>
|
||||
{renderBiblePage('KJV')}
|
||||
{viewMode !== 'page' ? renderStudyMargin() : null}
|
||||
<section className="footnote-strip">
|
||||
<strong>Footnotes</strong>
|
||||
<span>Dynamic translation notes and cross references will appear here.</span>
|
||||
</section>
|
||||
{renderFootnoteStrip()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
+49
-1
@@ -1,5 +1,5 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { LibraryState, StrongLink, Tag, Verse } from './types'
|
||||
import type { Footnote, LibraryState, StrongLink, Tag, Verse } from './types'
|
||||
|
||||
type TauriWindow = Window &
|
||||
typeof globalThis & {
|
||||
@@ -24,6 +24,15 @@ type ImportedStrongLink = {
|
||||
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'
|
||||
@@ -48,6 +57,7 @@ type BrowserStrongData = {
|
||||
|
||||
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[] = [
|
||||
@@ -58,6 +68,7 @@ const fallbackBrowserVerses: Verse[] = [
|
||||
verse: 1,
|
||||
text: 'In the beginning God created the heaven and the earth.',
|
||||
tags: [],
|
||||
footnotes: [],
|
||||
strongs: [
|
||||
{
|
||||
strong: 'H7225',
|
||||
@@ -86,6 +97,7 @@ const fallbackBrowserVerses: Verse[] = [
|
||||
verse: 1,
|
||||
text: 'The LORD is my shepherd; I shall not want.',
|
||||
tags: [],
|
||||
footnotes: [],
|
||||
strongs: [
|
||||
{
|
||||
strong: 'H3068',
|
||||
@@ -114,6 +126,7 @@ const fallbackBrowserVerses: Verse[] = [
|
||||
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',
|
||||
@@ -142,6 +155,7 @@ const fallbackBrowserVerses: Verse[] = [
|
||||
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',
|
||||
@@ -195,10 +209,15 @@ async function loadBrowserVerses() {
|
||||
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] ?? [],
|
||||
}))
|
||||
}
|
||||
@@ -288,11 +307,40 @@ async function loadLibreBibleDevVerses(): Promise<Verse[]> {
|
||||
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()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export type Verse = {
|
||||
verse: number
|
||||
text: string
|
||||
strongs: StrongLink[]
|
||||
footnotes?: Footnote[]
|
||||
tags: Tag[]
|
||||
}
|
||||
|
||||
@@ -24,6 +25,15 @@ export type Tag = {
|
||||
color: string
|
||||
}
|
||||
|
||||
export type Footnote = {
|
||||
id: number
|
||||
verseId: string
|
||||
noteIndex: number
|
||||
caller: string
|
||||
reference: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export type LibraryState = {
|
||||
verses: Verse[]
|
||||
tags: Tag[]
|
||||
|
||||
Reference in New Issue
Block a user