Render KJV footnotes and lemma margin line
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.7 - 2026-07-12
|
||||||
|
|
||||||
|
- Loaded KJV translator footnotes from LibreBible packages and rendered them in the attached footnote strip.
|
||||||
|
- Added a lemma-line view above the Strong's pills in the study margin.
|
||||||
|
- Extended native SQLite import with packaged footnotes.
|
||||||
|
|
||||||
## 0.1.6 - 2026-07-12
|
## 0.1.6 - 2026-07-12
|
||||||
|
|
||||||
- Made session page anchors book-aware so pages do not silently cross from one book into another.
|
- Made session page anchors book-aware so pages do not silently cross from one book into another.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.11.1",
|
"@tauri-apps/api": "^2.11.1",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 127.0.0.1",
|
"dev": "vite --host 127.0.0.1",
|
||||||
|
|||||||
Generated
+1
-1
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
+118
-1
@@ -43,6 +43,7 @@ struct Verse {
|
|||||||
verse: i64,
|
verse: i64,
|
||||||
text: String,
|
text: String,
|
||||||
strongs: Vec<StrongLink>,
|
strongs: Vec<StrongLink>,
|
||||||
|
footnotes: Vec<Footnote>,
|
||||||
tags: Vec<Tag>,
|
tags: Vec<Tag>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +65,17 @@ struct Tag {
|
|||||||
color: String,
|
color: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Footnote {
|
||||||
|
id: i64,
|
||||||
|
verse_id: i64,
|
||||||
|
note_index: i64,
|
||||||
|
caller: String,
|
||||||
|
reference: String,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct ImportedVerse {
|
struct ImportedVerse {
|
||||||
id: String,
|
id: String,
|
||||||
@@ -81,6 +93,15 @@ struct ImportedStrongLink {
|
|||||||
strong: String,
|
strong: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ImportedFootnote {
|
||||||
|
verse_id: String,
|
||||||
|
note_index: i64,
|
||||||
|
caller: String,
|
||||||
|
reference: String,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct LibreBibleIndexCatalog {
|
struct LibreBibleIndexCatalog {
|
||||||
resources: Vec<LibreBibleResourceSummary>,
|
resources: Vec<LibreBibleResourceSummary>,
|
||||||
@@ -106,6 +127,7 @@ struct LibreBibleResourceCatalog {
|
|||||||
struct LibreBiblePackageFiles {
|
struct LibreBiblePackageFiles {
|
||||||
verses_jsonl: Option<LibreBiblePackageFile>,
|
verses_jsonl: Option<LibreBiblePackageFile>,
|
||||||
strongs_jsonl: Option<LibreBiblePackageFile>,
|
strongs_jsonl: Option<LibreBiblePackageFile>,
|
||||||
|
footnotes_jsonl: Option<LibreBiblePackageFile>,
|
||||||
entries_jsonl: Option<LibreBiblePackageFile>,
|
entries_jsonl: Option<LibreBiblePackageFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +142,7 @@ struct LibreBiblePackageSource {
|
|||||||
package_dir: PathBuf,
|
package_dir: PathBuf,
|
||||||
verses_path: PathBuf,
|
verses_path: PathBuf,
|
||||||
strongs_path: Option<PathBuf>,
|
strongs_path: Option<PathBuf>,
|
||||||
|
footnotes_path: Option<PathBuf>,
|
||||||
lexicon_path: Option<PathBuf>,
|
lexicon_path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +266,16 @@ fn migrate(conn: &Connection) -> Result<(), String> {
|
|||||||
FOREIGN KEY (strong) REFERENCES lexicon(strong)
|
FOREIGN KEY (strong) REFERENCES lexicon(strong)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS verse_footnotes (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
verse_id INTEGER NOT NULL,
|
||||||
|
note_index INTEGER NOT NULL,
|
||||||
|
caller TEXT NOT NULL,
|
||||||
|
reference TEXT NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (verse_id) REFERENCES verses(id)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tags (
|
CREATE TABLE IF NOT EXISTS tags (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
label TEXT NOT NULL COLLATE NOCASE UNIQUE,
|
label TEXT NOT NULL COLLATE NOCASE UNIQUE,
|
||||||
@@ -337,6 +370,7 @@ fn reset_resource_cache(conn: &Connection) -> Result<(), String> {
|
|||||||
"
|
"
|
||||||
DELETE FROM verse_tags;
|
DELETE FROM verse_tags;
|
||||||
DELETE FROM verse_strongs;
|
DELETE FROM verse_strongs;
|
||||||
|
DELETE FROM verse_footnotes;
|
||||||
DELETE FROM lexicon;
|
DELETE FROM lexicon;
|
||||||
DELETE FROM verses;
|
DELETE FROM verses;
|
||||||
DELETE FROM library_sources;
|
DELETE FROM library_sources;
|
||||||
@@ -497,6 +531,9 @@ fn load_package_source_from_package_dir(
|
|||||||
strongs_jsonl: Some(LibreBiblePackageFile {
|
strongs_jsonl: Some(LibreBiblePackageFile {
|
||||||
path: "strongs-links.jsonl".to_string(),
|
path: "strongs-links.jsonl".to_string(),
|
||||||
}),
|
}),
|
||||||
|
footnotes_jsonl: Some(LibreBiblePackageFile {
|
||||||
|
path: "footnotes.jsonl".to_string(),
|
||||||
|
}),
|
||||||
entries_jsonl: None,
|
entries_jsonl: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -534,6 +571,11 @@ fn package_source_from_files(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|file| package_dir.join(&file.path))
|
.map(|file| package_dir.join(&file.path))
|
||||||
.filter(|path| path.exists());
|
.filter(|path| path.exists());
|
||||||
|
let footnotes_path = files
|
||||||
|
.footnotes_jsonl
|
||||||
|
.as_ref()
|
||||||
|
.map(|file| package_dir.join(&file.path))
|
||||||
|
.filter(|path| path.exists());
|
||||||
let lexicon_path = files
|
let lexicon_path = files
|
||||||
.entries_jsonl
|
.entries_jsonl
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -546,6 +588,7 @@ fn package_source_from_files(
|
|||||||
package_dir,
|
package_dir,
|
||||||
verses_path,
|
verses_path,
|
||||||
strongs_path,
|
strongs_path,
|
||||||
|
footnotes_path,
|
||||||
lexicon_path,
|
lexicon_path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -694,6 +737,39 @@ fn import_package(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(footnotes_path) = package_source.footnotes_path {
|
||||||
|
let file = File::open(&footnotes_path)
|
||||||
|
.map_err(|err| format!("Unable to open {}: {err}", footnotes_path.display()))?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
|
for (index, line) in reader.lines().enumerate() {
|
||||||
|
let line = line.map_err(|err| err.to_string())?;
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let imported: ImportedFootnote =
|
||||||
|
serde_json::from_str(&line).map_err(|err| err.to_string())?;
|
||||||
|
let Some(verse_id) = verse_ids.get(&imported.verse_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.execute(
|
||||||
|
"INSERT OR REPLACE INTO verse_footnotes
|
||||||
|
(id, verse_id, note_index, caller, reference, text)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||||
|
params![
|
||||||
|
(index + 1) as i64,
|
||||||
|
verse_id,
|
||||||
|
imported.note_index,
|
||||||
|
imported.caller,
|
||||||
|
imported.reference,
|
||||||
|
imported.text
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tx.commit().map_err(|err| err.to_string())
|
tx.commit().map_err(|err| err.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,12 +881,14 @@ fn row_to_verse_shell(row: &rusqlite::Row<'_>) -> rusqlite::Result<Verse> {
|
|||||||
verse: row.get(3)?,
|
verse: row.get(3)?,
|
||||||
text: row.get(4)?,
|
text: row.get(4)?,
|
||||||
strongs: Vec::new(),
|
strongs: Vec::new(),
|
||||||
|
footnotes: Vec::new(),
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hydrate_verse(conn: &Connection, mut verse: Verse) -> Result<Verse, String> {
|
fn hydrate_verse(conn: &Connection, mut verse: Verse) -> Result<Verse, String> {
|
||||||
verse.strongs = load_strongs(conn, verse.id)?;
|
verse.strongs = load_strongs(conn, verse.id)?;
|
||||||
|
verse.footnotes = load_footnotes(conn, verse.id)?;
|
||||||
verse.tags = load_tags(conn, verse.id)?;
|
verse.tags = load_tags(conn, verse.id)?;
|
||||||
Ok(verse)
|
Ok(verse)
|
||||||
}
|
}
|
||||||
@@ -849,6 +927,38 @@ fn load_strongs(conn: &Connection, verse_id: i64) -> Result<Vec<StrongLink>, Str
|
|||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_footnotes(conn: &Connection, verse_id: i64) -> Result<Vec<Footnote>, String> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT id, verse_id, note_index, caller, reference, text
|
||||||
|
FROM verse_footnotes
|
||||||
|
WHERE verse_id = ?1
|
||||||
|
ORDER BY note_index, id
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
let rows = stmt
|
||||||
|
.query_map(params![verse_id], |row| {
|
||||||
|
Ok(Footnote {
|
||||||
|
id: row.get(0)?,
|
||||||
|
verse_id: row.get(1)?,
|
||||||
|
note_index: row.get(2)?,
|
||||||
|
caller: row.get(3)?,
|
||||||
|
reference: row.get(4)?,
|
||||||
|
text: row.get(5)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
entries.push(row.map_err(|err| err.to_string())?);
|
||||||
|
}
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
fn add_column_if_missing(
|
fn add_column_if_missing(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
table: &str,
|
table: &str,
|
||||||
@@ -887,8 +997,11 @@ fn needs_resource_refresh(conn: &Connection) -> Result<bool, String> {
|
|||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)
|
)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
|
let footnote_count: i64 = conn
|
||||||
|
.query_row("SELECT COUNT(*) FROM verse_footnotes", [], |row| row.get(0))
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
Ok(lexicon_count < 10_000 || real_definition_count < 10_000)
|
Ok(lexicon_count < 10_000 || real_definition_count < 10_000 || footnote_count < 6_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_catalog_lexicon_path(
|
fn find_catalog_lexicon_path(
|
||||||
@@ -1031,6 +1144,9 @@ mod tests {
|
|||||||
let strongs_count: i64 = conn
|
let strongs_count: i64 = conn
|
||||||
.query_row("SELECT COUNT(*) FROM verse_strongs", [], |row| row.get(0))
|
.query_row("SELECT COUNT(*) FROM verse_strongs", [], |row| row.get(0))
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
|
let footnotes_count: i64 = conn
|
||||||
|
.query_row("SELECT COUNT(*) FROM verse_footnotes", [], |row| row.get(0))
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
let source_count: i64 = conn
|
let source_count: i64 = conn
|
||||||
.query_row("SELECT COUNT(*) FROM library_sources", [], |row| row.get(0))
|
.query_row("SELECT COUNT(*) FROM library_sources", [], |row| row.get(0))
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
@@ -1047,6 +1163,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(verse_count, 31_102);
|
assert_eq!(verse_count, 31_102);
|
||||||
assert_eq!(strongs_count, 348_884);
|
assert_eq!(strongs_count, 348_884);
|
||||||
|
assert_eq!(footnotes_count, 6_959);
|
||||||
assert_eq!(lexicon_count, 14_197);
|
assert_eq!(lexicon_count, 14_197);
|
||||||
assert!(g25_definition.contains("to love"));
|
assert!(g25_definition.contains("to love"));
|
||||||
assert_eq!(source_count, 1);
|
assert_eq!(source_count, 1);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "Libre Study",
|
"productName": "Libre Study",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"identifier": "org.librestudy.desktop",
|
"identifier": "org.librestudy.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
+74
-1
@@ -322,6 +322,56 @@ textarea:focus-visible {
|
|||||||
font-weight: 800;
|
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 {
|
.linked-word-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -422,16 +472,39 @@ textarea:focus-visible {
|
|||||||
display: flex;
|
display: flex;
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
border-top: 1px solid #e4dece;
|
border-top: 1px solid #e4dece;
|
||||||
background: #fbf8ef;
|
background: #fbf8ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footnote-strip strong {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.footnote-strip span {
|
.footnote-strip span {
|
||||||
color: #657061;
|
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 {
|
.status-panel.error {
|
||||||
border-color: #b42318;
|
border-color: #b42318;
|
||||||
color: #b42318;
|
color: #b42318;
|
||||||
|
|||||||
+52
-4
@@ -118,6 +118,11 @@ function App() {
|
|||||||
const pageRange = pageVerses.length
|
const pageRange = pageVerses.length
|
||||||
? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}`
|
? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}`
|
||||||
: 'No page loaded'
|
: 'No page loaded'
|
||||||
|
const selectedFootnotes = selectedVerse?.footnotes ?? []
|
||||||
|
const pageFootnotes = useMemo(
|
||||||
|
() => pageVerses.flatMap((verse) => verse.footnotes ?? []),
|
||||||
|
[pageVerses],
|
||||||
|
)
|
||||||
const books = useMemo(
|
const books = useMemo(
|
||||||
() => Array.from(new Set(libraryVerses.map((verse) => verse.book))),
|
() => Array.from(new Set(libraryVerses.map((verse) => verse.book))),
|
||||||
[libraryVerses],
|
[libraryVerses],
|
||||||
@@ -266,6 +271,30 @@ function App() {
|
|||||||
return parts.length ? parts : verse.text
|
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') {
|
function renderBiblePage(label = 'KJV') {
|
||||||
if (!selectedVerse) return null
|
if (!selectedVerse) return null
|
||||||
return (
|
return (
|
||||||
@@ -309,6 +338,7 @@ function App() {
|
|||||||
<span>{selectedVerse.strongs.length} links</span>
|
<span>{selectedVerse.strongs.length} links</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="margin-reference">{reference(selectedVerse)}</p>
|
<p className="margin-reference">{reference(selectedVerse)}</p>
|
||||||
|
{renderOriginalLanguageLine()}
|
||||||
<div className="linked-word-list" aria-label="Linked Strong's words">
|
<div className="linked-word-list" aria-label="Linked Strong's words">
|
||||||
{selectedVerse.strongs.map((entry) => (
|
{selectedVerse.strongs.map((entry) => (
|
||||||
<button
|
<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 (
|
return (
|
||||||
<main className="app-shell">
|
<main className="app-shell">
|
||||||
<aside className="tool-rail" aria-label="Main tools">
|
<aside className="tool-rail" aria-label="Main tools">
|
||||||
@@ -472,10 +523,7 @@ function App() {
|
|||||||
<div className={`page-system ${viewMode}`}>
|
<div className={`page-system ${viewMode}`}>
|
||||||
{renderBiblePage('KJV')}
|
{renderBiblePage('KJV')}
|
||||||
{viewMode !== 'page' ? renderStudyMargin() : null}
|
{viewMode !== 'page' ? renderStudyMargin() : null}
|
||||||
<section className="footnote-strip">
|
{renderFootnoteStrip()}
|
||||||
<strong>Footnotes</strong>
|
|
||||||
<span>Dynamic translation notes and cross references will appear here.</span>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+49
-1
@@ -1,5 +1,5 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
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 &
|
type TauriWindow = Window &
|
||||||
typeof globalThis & {
|
typeof globalThis & {
|
||||||
@@ -24,6 +24,15 @@ type ImportedStrongLink = {
|
|||||||
strong: string
|
strong: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImportedFootnote = {
|
||||||
|
id: number
|
||||||
|
verse_id: string
|
||||||
|
note_index: number
|
||||||
|
caller: string
|
||||||
|
reference: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
type ImportedStrongEntry = {
|
type ImportedStrongEntry = {
|
||||||
strongs_number: string
|
strongs_number: string
|
||||||
language: 'hebrew' | 'greek'
|
language: 'hebrew' | 'greek'
|
||||||
@@ -48,6 +57,7 @@ type BrowserStrongData = {
|
|||||||
|
|
||||||
let browserLibraryCache: Promise<Verse[]> | null = null
|
let browserLibraryCache: Promise<Verse[]> | null = null
|
||||||
let browserStrongDataCache: Promise<BrowserStrongData> | null = null
|
let browserStrongDataCache: Promise<BrowserStrongData> | null = null
|
||||||
|
let browserFootnotesCache: Promise<Map<string, Footnote[]>> | null = null
|
||||||
const browserVerseSourceIds = new Map<number, string>()
|
const browserVerseSourceIds = new Map<number, string>()
|
||||||
|
|
||||||
const fallbackBrowserVerses: Verse[] = [
|
const fallbackBrowserVerses: Verse[] = [
|
||||||
@@ -58,6 +68,7 @@ const fallbackBrowserVerses: Verse[] = [
|
|||||||
verse: 1,
|
verse: 1,
|
||||||
text: 'In the beginning God created the heaven and the earth.',
|
text: 'In the beginning God created the heaven and the earth.',
|
||||||
tags: [],
|
tags: [],
|
||||||
|
footnotes: [],
|
||||||
strongs: [
|
strongs: [
|
||||||
{
|
{
|
||||||
strong: 'H7225',
|
strong: 'H7225',
|
||||||
@@ -86,6 +97,7 @@ const fallbackBrowserVerses: Verse[] = [
|
|||||||
verse: 1,
|
verse: 1,
|
||||||
text: 'The LORD is my shepherd; I shall not want.',
|
text: 'The LORD is my shepherd; I shall not want.',
|
||||||
tags: [],
|
tags: [],
|
||||||
|
footnotes: [],
|
||||||
strongs: [
|
strongs: [
|
||||||
{
|
{
|
||||||
strong: 'H3068',
|
strong: 'H3068',
|
||||||
@@ -114,6 +126,7 @@ const fallbackBrowserVerses: Verse[] = [
|
|||||||
verse: 16,
|
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.',
|
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: [],
|
tags: [],
|
||||||
|
footnotes: [],
|
||||||
strongs: [
|
strongs: [
|
||||||
{
|
{
|
||||||
strong: 'G25',
|
strong: 'G25',
|
||||||
@@ -142,6 +155,7 @@ const fallbackBrowserVerses: Verse[] = [
|
|||||||
verse: 28,
|
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.',
|
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: [],
|
tags: [],
|
||||||
|
footnotes: [],
|
||||||
strongs: [
|
strongs: [
|
||||||
{
|
{
|
||||||
strong: 'G2822',
|
strong: 'G2822',
|
||||||
@@ -195,10 +209,15 @@ async function loadBrowserVerses() {
|
|||||||
console.warn('Unable to load browser Strong data.', error)
|
console.warn('Unable to load browser Strong data.', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
const footnotes = await loadBrowserFootnotes().catch((error) => {
|
||||||
|
console.warn('Unable to load browser footnotes.', error)
|
||||||
|
return new Map<string, Footnote[]>()
|
||||||
|
})
|
||||||
const tagsByVerse = readBrowserTags()
|
const tagsByVerse = readBrowserTags()
|
||||||
return verses.map((verse) => ({
|
return verses.map((verse) => ({
|
||||||
...verse,
|
...verse,
|
||||||
strongs: strongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs,
|
strongs: strongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs,
|
||||||
|
footnotes: footnotes.get(browserVerseKey(verse)) ?? verse.footnotes ?? [],
|
||||||
tags: tagsByVerse[verse.id] ?? [],
|
tags: tagsByVerse[verse.id] ?? [],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -288,11 +307,40 @@ async function loadLibreBibleDevVerses(): Promise<Verse[]> {
|
|||||||
verse: verse.verse,
|
verse: verse.verse,
|
||||||
text: verse.text,
|
text: verse.text,
|
||||||
strongs: [],
|
strongs: [],
|
||||||
|
footnotes: [],
|
||||||
tags: [],
|
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> {
|
async function loadBrowserStrongData(): Promise<BrowserStrongData> {
|
||||||
browserStrongDataCache ??= loadLibreBibleDevStrongData()
|
browserStrongDataCache ??= loadLibreBibleDevStrongData()
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export type Verse = {
|
|||||||
verse: number
|
verse: number
|
||||||
text: string
|
text: string
|
||||||
strongs: StrongLink[]
|
strongs: StrongLink[]
|
||||||
|
footnotes?: Footnote[]
|
||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +25,15 @@ export type Tag = {
|
|||||||
color: string
|
color: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Footnote = {
|
||||||
|
id: number
|
||||||
|
verseId: string
|
||||||
|
noteIndex: number
|
||||||
|
caller: string
|
||||||
|
reference: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
export type LibraryState = {
|
export type LibraryState = {
|
||||||
verses: Verse[]
|
verses: Verse[]
|
||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
|
|||||||
Reference in New Issue
Block a user