From d710035045b9251972ed0dd3cc5cecad0a0427e5 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Jul 2026 09:10:01 -0500 Subject: [PATCH] Render KJV footnotes and lemma margin line --- CHANGELOG.md | 6 ++ package-lock.json | 4 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 119 +++++++++++++++++++++++++++++++++++++- src-tauri/tauri.conf.json | 2 +- src/App.css | 75 +++++++++++++++++++++++- src/App.tsx | 56 ++++++++++++++++-- src/studyClient.ts | 50 +++++++++++++++- src/types.ts | 10 ++++ 11 files changed, 315 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d9f93..e3374ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 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 - Made session page anchors book-aware so pages do not silently cross from one book into another. diff --git a/package-lock.json b/package-lock.json index fece8ca..3c62ed3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "libre-study", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libre-study", - "version": "0.1.6", + "version": "0.1.7", "dependencies": { "@tauri-apps/api": "^2.11.1", "react": "^19.2.7", diff --git a/package.json b/package.json index 2617871..9a98145 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libre-study", "private": true, - "version": "0.1.6", + "version": "0.1.7", "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d9473df..018ae2f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "app" -version = "0.1.6" +version = "0.1.7" dependencies = [ "log", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a9e3b2f..1acb55b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.6" +version = "0.1.7" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index eb35eb8..986689e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -43,6 +43,7 @@ struct Verse { verse: i64, text: String, strongs: Vec, + footnotes: Vec, tags: Vec, } @@ -64,6 +65,17 @@ struct Tag { 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)] struct ImportedVerse { id: String, @@ -81,6 +93,15 @@ struct ImportedStrongLink { strong: String, } +#[derive(Deserialize)] +struct ImportedFootnote { + verse_id: String, + note_index: i64, + caller: String, + reference: String, + text: String, +} + #[derive(Deserialize)] struct LibreBibleIndexCatalog { resources: Vec, @@ -106,6 +127,7 @@ struct LibreBibleResourceCatalog { struct LibreBiblePackageFiles { verses_jsonl: Option, strongs_jsonl: Option, + footnotes_jsonl: Option, entries_jsonl: Option, } @@ -120,6 +142,7 @@ struct LibreBiblePackageSource { package_dir: PathBuf, verses_path: PathBuf, strongs_path: Option, + footnotes_path: Option, lexicon_path: Option, } @@ -243,6 +266,16 @@ fn migrate(conn: &Connection) -> Result<(), String> { 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 ( id INTEGER PRIMARY KEY, 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_strongs; + DELETE FROM verse_footnotes; DELETE FROM lexicon; DELETE FROM verses; DELETE FROM library_sources; @@ -497,6 +531,9 @@ fn load_package_source_from_package_dir( strongs_jsonl: Some(LibreBiblePackageFile { path: "strongs-links.jsonl".to_string(), }), + footnotes_jsonl: Some(LibreBiblePackageFile { + path: "footnotes.jsonl".to_string(), + }), entries_jsonl: None, }; @@ -534,6 +571,11 @@ fn package_source_from_files( .as_ref() .map(|file| package_dir.join(&file.path)) .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 .entries_jsonl .as_ref() @@ -546,6 +588,7 @@ fn package_source_from_files( package_dir, verses_path, strongs_path, + footnotes_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()) } @@ -805,12 +881,14 @@ fn row_to_verse_shell(row: &rusqlite::Row<'_>) -> rusqlite::Result { verse: row.get(3)?, text: row.get(4)?, strongs: Vec::new(), + footnotes: Vec::new(), tags: Vec::new(), }) } fn hydrate_verse(conn: &Connection, mut verse: Verse) -> Result { verse.strongs = load_strongs(conn, verse.id)?; + verse.footnotes = load_footnotes(conn, verse.id)?; verse.tags = load_tags(conn, verse.id)?; Ok(verse) } @@ -849,6 +927,38 @@ fn load_strongs(conn: &Connection, verse_id: i64) -> Result, Str Ok(entries) } +fn load_footnotes(conn: &Connection, verse_id: i64) -> Result, 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( conn: &Connection, table: &str, @@ -887,8 +997,11 @@ fn needs_resource_refresh(conn: &Connection) -> Result { |row| row.get(0), ) .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( @@ -1031,6 +1144,9 @@ mod tests { let strongs_count: i64 = conn .query_row("SELECT COUNT(*) FROM verse_strongs", [], |row| row.get(0)) .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 .query_row("SELECT COUNT(*) FROM library_sources", [], |row| row.get(0)) .map_err(|err| err.to_string())?; @@ -1047,6 +1163,7 @@ mod tests { assert_eq!(verse_count, 31_102); assert_eq!(strongs_count, 348_884); + assert_eq!(footnotes_count, 6_959); assert_eq!(lexicon_count, 14_197); assert!(g25_definition.contains("to love")); assert_eq!(source_count, 1); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 067a90c..339955b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Libre Study", - "version": "0.1.6", + "version": "0.1.7", "identifier": "org.librestudy.desktop", "build": { "frontendDist": "../dist", diff --git a/src/App.css b/src/App.css index a28f9d5..d1dc3ba 100644 --- a/src/App.css +++ b/src/App.css @@ -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; diff --git a/src/App.tsx b/src/App.tsx index 82cbc61..748f0e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( +
+

Lemma Line

+

+ {selectedVerse.strongs.map((entry) => ( + + ))} +

+
+ ) + } + function renderBiblePage(label = 'KJV') { if (!selectedVerse) return null return ( @@ -309,6 +338,7 @@ function App() { {selectedVerse.strongs.length} links

{reference(selectedVerse)}

+ {renderOriginalLanguageLine()}
{selectedVerse.strongs.map((entry) => (